JVM垃圾回收机制

GC过程代码演示

JDK1.8环境

public class JVMDemo {
    public static void main(String[] args) {
        /*
        JVM中相关的参数:
            -Xms    初始堆内存大小,默认为物理内存的1/64   12G时,196M
            -Xmx    最大分配的堆内存,默认为物理内存的1/4    12G时,3136M
            -XX:+PrintGCDetails     输出详细的GC处理日志
         */

        // 获取Runtime实例
        Runtime runtime = Runtime.getRuntime();

        // 获取默认最大分配内存
        long maxMemory = runtime.maxMemory(); // 单位:字节

        // 获取JVM中的内存问题
        long totalMemory = runtime.totalMemory(); // 单位:字节

        // 打印
        System.out.println("maxMemory = " + maxMemory / (1024.0 * 1024) + "MB");
        System.out.println("totalMemory = " + totalMemory / (1024.0 * 1024) + "MB");

        // 不断产生垃圾
        String str = "GCGCGGC";
        while (true) {
            str += str + new Random().nextInt(888888) + new Random().nextInt(999999);
        }
    }
}

运行环境的设置如下

// Metaspace 元数据区
// 本机物理内存:12G
JVM参数:-Xmx1024m -Xms1024m -XX:+PrintGCDetails
	-Xmx: JVM的初始堆内存,默认为物理内存的1/64
	-Xms: 最大分配的堆内存,默认为物理内存的1/4

-Xmx1024m -Xms1024m -XX:+PrintGCDetails在IDEA的VM Options中配置:

JVM垃圾回收机制_第1张图片

运行结果如下:

maxMemory = 981.5MB
totalMemory = 981.5MB
[GC (Allocation Failure) [PSYoungGen: 221069K->35670K(305664K)] 221069K->87828K(1005056K), 0.0250783 secs] [Times: user=0.05 sys=0.03, real=0.03 secs] 
[GC (Allocation Failure) [PSYoungGen: 249327K->35606K(305664K)] 440552K->226831K(1005056K), 0.0150943 secs] [Times: user=0.02 sys=0.01, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 185038K->776K(305664K)] 793463K->678734K(1005056K), 0.0275678 secs] [Times: user=0.16 sys=0.02, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 776K->0K(305664K)] [ParOldGen: 677958K->348374K(699392K)] 678734K->348374K(1005056K), [Metaspace: 3499K->3499K(1056768K)], 0.0463971 secs] [Times: user=0.31 sys=0.03, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 144231K->32K(305664K)] 631672K->626540K(1005056K), 0.0182470 secs] [Times: user=0.20 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(305664K)] [ParOldGen: 626508K->278841K(699392K)] 626540K->278841K(1005056K), [Metaspace: 3499K->3499K(1056768K)], 0.0343299 secs] [Times: user=0.20 sys=0.00, real=0.03 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(262656K)] 556975K->556975K(962048K), 0.0018310 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(262656K)] [ParOldGen: 556975K->417908K(699392K)] 556975K->417908K(962048K), [Metaspace: 3499K->3499K(1056768K)], 0.0489941 secs] [Times: user=0.33 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(302592K)] 417908K->417908K(1001984K), 0.0012516 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(302592K)] [ParOldGen: 417908K->417888K(699392K)] 417908K->417888K(1001984K), [Metaspace: 3499K->3499K(1056768K)], 0.0564153 secs] [Times: user=0.33 sys=0.00, real=0.06 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at cn.yeats.JVMDemo.main(JVMDemo.java:35)
Heap
 PSYoungGen      total 302592K, used 7584K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 254464K, 2% used [0x00000000eab00000,0x00000000eb268250,0x00000000fa380000)
  from space 48128K, 0% used [0x00000000fd100000,0x00000000fd100000,0x0000000100000000)
  to   space 46592K, 0% used [0x00000000fa380000,0x00000000fa380000,0x00000000fd100000)
 ParOldGen       total 699392K, used 417888K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 59% used [0x00000000c0000000,0x00000000d98181f8,0x00000000eab00000)
 Metaspace       used 3530K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 386K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 1

注意从“Heap”往下的部分!

JVM堆内存不足的原因

通常有以下原因:

  • JVM堆内存设置不足

    -Xms与-Xmx设置太小

  • 程序中创建了大量对象,并且长时间没有被回收(驻留在堆内存中)

    这里的对象指:GC链能够到达GC ROOTS顶点的对象,也就是活跃对象,有用的对象。并非指无用的垃圾对象。

    -Xms JVM堆内存初始大小,默认为物理内存的1/64。

    -Xmx	JVM最大堆内存,默认为物理内存的1/4。 		
    		最大堆内存,通常会设置为物理的   `1/2~3/4`,但还是要根据实际情况而定。
    

注意:

​ 什么样的对象,能够经过15轮GC过程,还依然存活在JVM堆内存中?通常就是些 池对象

存活是指对于GC ROOTS顶点集,其GC链仍然可达。

JVM内存不足时,就需要执行垃圾清理过程,即GC过程。

GC垃圾回收过程

以JDK1.7为例。

GC过程主要针对的是JVM堆内存,对于JDK1.7的Perm区(JDK1.8的Metaspace区)几乎不存在GC过程。

JVM垃圾回收机制_第2张图片

  • 商业版的JVM,各区域的内存大小:

    Eden : S0 : S1 = 8 : 1 : 1

  • JVM规范中,规定了Eden区与Survior区域大小是1:1,并且要求S0:S1=1:1

GC过程

  • 新生对象,放入Eden区
    注意:通常来说,新生对象是存入Eden区的,但是如果生成的对象是大对象,对象的描述最终是一个字符串,大对象就是指这一串字符的长度比较长,如几千。(QQQQ: 具体的数字,还有待探究!)

  • Eden区内存不足时,开始执行GC过程(复制清除算法,MinorGC)

    把Eden区中有用的对象向S0区复制,然后将Eden区中留下的无用对象全部清除。

  • S0区内存不足时,执行GC过程(复制清除算法)

    把S0区中有用的对象向S1区复制,然后将S0区中留下的无用对象全部清除。

  • S1区内存不足时,执行GC过程(复制清除算法)

    此时,进入S0区与S1区间的切换阶段。

    把S1区中有用的对象向S0区复制,然后将S1区中留下的无用对象全部清除。

    之后,如果存在S0或者S1区的内存不足时,就针对这两个区域执行GC过程(复制清除算法)。

    对象年龄达到15时,如果需要再次切换区域,则将对象称入到年老区

    • 如果年老区未满,则直接移入。
    • 如果年老区已满,则执行full GC过程,清理年老区。

JVM垃圾回收机制_第3张图片

垃圾判定算法

JVM中,什么是“垃圾”?

可以参考一下Obejct类中的finalize()方法的注释:

/**Called by the garbage collector on an object when garbage collection
  * determines that there are no more references to the object.
  * A subclass overrides the {@code finalize} method to dispose of
  * system resources or to perform other cleanup.

(1) 引用计数算法; (2)可达性算法

引用计数算法

JDK1.2及之前,可以认为“没有引用指向的对象,是垃圾”。 采用的是“引用计数算法

  • 引用计数算法

    给每个对象定义一个引用计数器count,当有一个引用指向该对象时,count就加1,当有一个引用不再指向该对象时,count减1。最终判断该对象是否为"垃圾",只需判断其引用计数器count是否大于0。
    
  • 引用计数算法,在循环引用中会有问题:明明一个对象已经是”垃圾“了(比如:obj = null;),但它的”引用计算器“可能不为0,这时JVM就不会将它当作垃圾进行回收。

// 循环引用的代码示例
public class Demo{
    public static void main(String[] args){
        // 定义一个对象,默认为null
        public Demo instance; 
        
        // 创建两个对象
        Demo d1 = new Demo();
        Demo d2 = new Demo();
        
        // 循环引用
        d1.instance = d2; // d1指向d2
        d2.instance = d1; // d2指向d1
        
        // 设置对象为空,即置其为"垃圾"
        d1 = null;
        d2 = null;
    }
}

其内存图:

JVM垃圾回收机制_第4张图片

可达性算法

JDK1.2之后,”没有引用可达的对象,是垃圾“。 采用的是”可达性算法

由JVM选择 GC ROOTS(GC根节点,多个)顶点,其他对象去指向GC ROOTS顶点,如果能够到达GC ROOTS顶点,该对象就不是"垃圾",否则就是"垃圾"。

两个概念:

  • GC ROOTS GC顶点

    什么样的内容,可以作为”GC ROOTS顶点“?(《深入理解JAVA虚拟机》P65)

    1. 虚拟机栈(栈桢中本地变量表所指向的对象)
    2. 方法区中类的静态属性所指向的对象
    3. 方法区的常量
    4. native方法中引用的对象
    
  • GC链

    从对象出发,指向GC ROOTS的路由,称为”GC链“。

举例:还是以上面的代码为例

JVM垃圾回收机制_第5张图片

如果只断掉指向d1节点与d2节点其中的一条路,那么两个对象因为存在有循环引用,它们仍然都可以到达GC ROOTS。

  • 注意:GC ROOTS是由JVM自己去选择的,通常都是多个,即使你的程序中只有一个对象。至于JVM到底是如何选择GC ROOTS的,可继续深入研究。

对象引用的4种方式

按引用力度由高到低: 强引用 -> 软引用 -> 弱引用 -> 虚引用
Q1: 什么叫“引用力度”?
Q2: 四种引用,各自是什么意思?

  • 强引用 Strong Reference

    就是指普通的对象引用,如:Person p = new Person();

  • 软引用 Soft Reference

    当JVM内存足够时,该对象不会受到任何影响,发生GC过程,它也不会受到影响;

    当JVM内存不足时,这个对象就会被自动回收。

  • 弱引用 Weak Reference

    只要发生GC过程,无论JVM内存是否充足,该对象都会被回收。

  • 虚引用 Phantom Reference

    为每一个对象设置虚引用的唯一目的就是能在这个对象被GC回收时收到一个系统通知,告知程序,此对象已被Garbage Collector回收了。

垃圾收集算法

JVM垃圾回收机制_第6张图片

标记-清除算法

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

JVM垃圾回收机制_第7张图片

复制-清除算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

JVM垃圾回收机制_第8张图片

标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。

JVM垃圾回收机制_第9张图片

分代收集算法

当前虚拟机的垃圾手机都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清楚”或“标记-整理”算法进行垃圾收集。

延伸面试问题: HotSpot为什么要分为新生代和老年代?

根据上面的对分代收集算法的介绍回答。

垃圾收集器

JVM垃圾回收机制_第10张图片

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非了挑选出一个最好的收集器。因为知道现在位置还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。

Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。
JVM垃圾回收机制_第11张图片

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。
JVM垃圾回收机制_第12张图片

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

并行和并发概念补充:

  • 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。

Parallel Scavenge收集器

Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?

-XX:+UseParallelGC 

    使用Parallel收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用Parallel收集器+ 老年代并行

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。
JVM垃圾回收机制_第13张图片

Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
  • 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。

JVM垃圾回收机制_第14张图片

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对CPU资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.

被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

合**:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

你可能感兴趣的:(JVM)