寒假读完《深入理解Java虚拟机》的总结

01类加载机制

1.1 平台无关性如何实现

.java文件–>(javac编译生成字节码)–>.class文件---->JVM解析,转换成特定平台的机器指令---->
java不直接将源码解析成机器码执行原因:省去了每次直接解析成机器码需要执行各种词法语法编译

1.2 java如何加载.class文件

虚拟机通过Classloader加载.class文件到内存(Runtime Data Area), 由Excution Engine解析文件里的字节码,再交给操作系统去执行。另外,本地库接口也在加载期间提供一些java的原生方法(如Class.forName())
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jfom8BK-1582447361841)(JVM虚拟机_files/index.htm)]

1.3 Java反射机制

反射是让java在运行期间,可以动态地获取任意类的方法和属性,或动态地调用对象方法的功能
写一个例子:

public class Robot{//定义一个有属性有方法的类来反射
    private String name;
    public void sayHi(String hi){System.out.println(hi+""+name);}
    private String throwHi(String tag){return "Hi"+tag;}
}
public class reflectSample{//获取前面类的方法和属性
    public static void main(String[] args){
        Class rc = Class.forName("com...Robot");//robot类全路径
        Robot r = (Robot)rc.newInstance(); //创建Class对象rc的实例.并强转成Robot类型对象
        rc.getName();
        r.sayHi(String hello);//调用public方法,hello就可以
    }
}

从例子也可看出类从编译到执行的过程:
1.javac将Robot.java源文件编译成Robot.class字节码文件
2.ClassLoader将字节码转换为JVM中的Class对象
3.JVM利用Class对象实例化Robot对象

1.5 ClassLoader的分类

启动类加载器(Bootstrap ClassLoader)

由 C++ 语言实现,是虚拟机自身的一部分。负责将存在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类库加载到虚拟机内存中。

其他类加载器

由 Java 语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader,由Sun包里的Launcher类来加载。扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录中的,或系统变量指定路径中的类库,开发者可以直接使用。父类加载器为null

  • 应用程序类加载器 (Application ClassLoader):它负责加载用户路径(ClassPath)所指定的类库,开发者可以直接使用,父类加载器为ExtClassLoader。如果应用程序中没有自定义类加载器,这个就是程序默认的类加载器。
  • 自定义类加载器,父类一定是Application ClassLoader 类加载器的作用就是把.class文件加载到内存。
    类加载器的作用就是把.class文件加载到内存。
    类加载器父子关系

1.6 双亲委派模式

即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
具体实现:加载时,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。以下是ClassLoader.java源码

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }
                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己尝试加载
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

为什么用双亲委派?1.可以避免类的重复加载(名字相同的类文件被不同的类加载器加载产生的是两个不同的类)2.也保证了 Java 的核心 API 不被篡改。

1.7 类加载过程系统加载

JVM必问知识点:类加载过程
Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。

02 JVM内存布局

概念:Java用来确保多线程下内存结果正确的一套抽象规范,Java5假设每个线程都有自己的工作内存,里面存主内存的副本互不干扰,线程之间变量的传递通过主内存进行。
JVM内存区详解

2.1运行时数据区布局------即 Java内存模型【书2.2】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yAMKNHdj-1582447361851)(index_files/dbeadb14-f3ae-44b6-b441-f3895d0f23c6.jpg)]
1 方法区(共享区域)
也称“永久代”,Java程序编译成的字节码文件首先就加载到方法区,主要存放静态数据。JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
这块内存在程序整个运行期间都一直存在
方法区存储了什么
2 堆(共享区域)
存放对象实例的不连续的内存区域,JVM种最大的一块内存。【内存泄漏优化的关键存储区,引申GC】
3 栈(Java方法栈)
是存放方法的局部变量和字节码操作数的一块连续内存区域。执行方法时会分配栈帧来存内容,方法执行结束时这些存储单元就会自动被注释掉。大小是由操作系统决定的,先进后出,进出完成不会产生碎片,运行效率高且稳定。
4 本地方法栈
与Java栈类似,区别是负责Native方法
5 程序计数器
用于记录字节码当前执行的行号,以便线程切换后都能回到正确位置。如果线程执行Java方法,PC记录当前字节码指令地址,如果执行Native方法则为空。是唯一一块无任何OOM情况的区域。

Class demo{
static int d = 0;
static int add(int a, int b) {return a + b;}
native int read() {}
public static void(String[] args) {
   int a = 1; //栈区存储局部变量a,线程独占
   String b = new String(); //堆区分配String对象内存
   String c = "hello"; //方法区存储常量"hello"
   d = 2; //方法区存储静态变量d,线程共享
   int e = add(1, 2); //线程执行add,栈帧负责push方法和形参,pop返回值
   int f = b.length();//同上,PC记录执行位置
   int g = read(); //本地方法区执行read
  } 
}

2.2Java 内存模型与 happens-before 关系【网课13】

JMM模型是Java抽象出来保证多线程情况下内存结果正确的一套规范。为了应用程序不受数据竞争的干扰,Java 假设每个线程都有自己的工作内存,线程间变量的传递需要通过主内存来完成。
JMM最为重要的概念便是 happens-before 关系。happens-before 关系是用来描述两个
操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。javan内存模型课:操作 X happens-before 操作 Y,使得操作 X 之前的字节码的结果对操作 Y 之后的字节码可见。

2.3 堆内存结构(GC主战场)

堆内存可分为eden 区、s0(“From”) 区、s1(“To”) 区都属于新生代,tentired 区属于老年代。**Eden 区远大于 S0,S1 的原因:**因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象。

03 GC

3.1 可回收对象

  即使在可达性分析算法中不可达的对象,也并非是“非死不可”,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;  
第二次标记:第一次标记后接着会进行一次筛选,看此对象是否有必要执行 finalize() 方法。如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记,真正回收。

3.2 GC算法

标记清除算法
标记出所有要回收的对象,然后统一回收标记对象。
优点:不需移动对象,且只处理不存活的对象,在存活对象比较多的情况下极高效。两个不足:(1)标记和清除过程效率不高,因为要使用一个空闲列表来记录所有的空闲区域以及大小。(2)标记清除之后会有大量不连续的内存碎片。如果分配大对象(大数组)时找不到足够的连续内存空间容易OOM
复制算法
将内存空间一分为二,每次用一半空间存对象,要垃圾回收时就把存活对象放到另一半,然后对之前一半进行彻底清空。
优点:(1)标记阶段和复制阶段可以同时进行。(2)每次只对一半内存进行回收且不用考虑内存碎片问题,运行高效。(3)只需移动栈顶指针,按顺序分配内存即可。缺点:可一次性分配的最大内存缩小了一半。
+标记整理算法
基于Compacting算法,第一阶段从根节点开始标记所有被引用对象,第二阶段把存活对象“压缩”到堆的其中一块,清除掉其它空间。
优点:此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
缺点:GC暂停的时间会更长,因为将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
分代收集算法
在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

3.3 堆内存的对象分配与回收策略

  • 对象优先在新生代eden区分配
    当Eden区没有足够的内存空间进行分配时,JVM会发起一次Minor GC

Minor GC和Full GC
Minor GC是回收新生代,对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁。Minor GC 有一个问题:在标记时,扫描到老年代的对象引用了新生代的对象,那么这个引用也被作为 GCRoots。HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。由于 Minor GC 伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。
Full GC是回收新生代和老生代,在内存快满时触发,容易stop the world。Full GC 会清理整个堆中的不可用对象,速度一般会比 Minor GC 的慢 10 倍以上。如果此时 server 收到了很多请求,则会被拒绝服务!

  • 大对象直接进入老年代
    因为新生代用的复制回收算法,大对象的复制消耗过大( 大对象是占大量连续内存空间的对象如字符串和数组)
  • 长期存活对象进入survivor
    Eden 区远大于 S0,S1 的原因,因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象,此时把它们移到 S0 或 S1 绰绰有余)同时对象年龄加一(对象的年龄即发生 Minor GC 的次数),最后把 Eden 区对象全部清理以释放出空间。
  • 动态年龄判断
    Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次
    数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升
    (promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参
    数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
  • 空间担保机制(见书)
    在发生 MinorGC 之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间。
    如果大于,那么Minor GC 可以确保是安全的;
    如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。 如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,
    如果大于则进行 Minor GC,否则可能进行一次 Full GC。

3.4 垃圾收集器

java虚拟机有“client”和“server”两种运行模式,用java -version可查看是Server VM 便是重量级虚拟机,启动慢运行快。垃圾收集器与JVM具体实现场景紧密相,不同JVM提供的选择也不同。

Young GC(均用回收算法)

  • Serial(-XX:UseSerialNewGC)
    使用复制算法因为是单线程收集,垃圾收集时要暂停所有工作线程。Client模式下默认的年轻代收集器。
  • Parnew收集器(-XX:+UseParNewGC)
    使用复制算法,开启的收集线程与CPU核数相同,单核执行效率不如Serial因为存在线程交互开销。
  • Parallel Scavenge收集器(-XX:+UseParallelGC)
    Parallel Scavenge关注点是吞吐量(高效率的利用 CPU)。而CMS 关注点是用户线程的停顿时间(提高用户体验)。吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
    在多核下执行才有优势,Server模式下默认的年轻代收集器

Old GC(均算标记清除算法)

+nSerial Old 收集器
Serial 收集器的老年代版本,同样是单线程。作为 CMS 收集器的后备方案。
+nParallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器配合 Parallel Old 收集器。

  • CMS 收集器(Concurrent Mark Sweep)
    CMS最短关注回收停顿时间,也就是注重用户体验感。是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。整个过程分为四个步骤:
    初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
    并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

G1 收集器(Garbage-First)

一款面向服务器的垃圾收集器,主要针对配备多处理器,大容量内存的机器. 满足GC停顿时间要求的同时,还具备高吞吐量。特点如下:
(JDK1.7 中 HotSpot提出目的就是代替CMS,现已成功)
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个CPU来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
可预测的停顿:G1 收集器在后台维护了一个优先列表,将堆内存划分为多个相同的Region。每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GF 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

3.6 四种引用

一般进行回收的只有软引用和弱引用?

线程并发时,两个对象共用一段内存的问题?

解决技术是TLAB(Thread Local AllocationBuffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)
每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB。
这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。如果该线程 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的TLAB。

发布了21 篇原创文章 · 获赞 4 · 访问量 1366

猜你喜欢

转载自blog.csdn.net/smile001isme/article/details/104462552