java垃圾回收

java垃圾回收

JVM内存模型

在这里插入图片描述

如何判断是否垃圾

引用计数法

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
引用计数法存在的问题:

  1. 效率问题,引用和去引用伴随着加法与减法,影响性能
  2. 对于循环引用问题,无法解决
    在这里插入图片描述

可达性分析法

从GCRoots节点一直往下走,如果走不通,说明走不通的那些对象是不可用的,那么其就可以被垃圾回收期回收。
可以作为GCRoots的对象:

  1. 虚拟机栈(局部变量表中对象)
  2. 方法区的类属性所引用的对象
  3. 方法区中常量所引用的对象
  4. 本地方法栈中引用的的对象

垃圾回收算法

标记清除

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
缺点:

  1. 效率问题,标记和清除两个过程的效率都不高
  2. 空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。甚至会导致无法分配大对象而出现OutofMemory异常

标记整理(标记压缩)

标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法不适用于存活对象较多的场合 如新生代 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

垃圾回收器

在这里插入图片描述
ParNew:可以与老年代中三个垃圾收集器都可以配合
Serial:对应Serial Old
Parallel:对应Parallel Old

Serial(串行回收)

  1. -XX:+UseSerailGC 开启
  2. 最基本,发展最悠久的收集器
  3. 单线程垃圾收集器(Stop-The-World)
  4. 优点:简单。对于单CPU的情况,由于没有多线程交互开销,反而可以更高效,是Client模式下默认的新生代收集器
  5. 缺点:Stop-The-World。

Parnew(并行回收)

  1. 复制算法(新生代收集器)
  2. -XX:+UseParNewGC开启; -XX:ParallellGCThreads指定线程数
  3. 相比于Serial是多线程收集的,那么收集的间隔会降低一些。
  4. 缺点:Stop-The-World,老年代也仍然是单线程手机

Parallel Scavenge(并行回收,可控制吞吐量)

  1. 复制算法(新生代收集器)
  2. -XX:+UseParallelGC开启
  3. 多线程收集
  4. 可以达到一个可控制的吞吐量(cpu用于运行用户代码的时间与cpu消耗的总时间的比值)
  5. -XX:MaxGCPauseMills 垃圾收集器最大停顿时间(如果设置的最大停顿时间短了,则回收的频率就增大了咯)
  6. -XX:GCTimeRatio 吞吐量大小。停顿时间越短就越适合与用户交互的应用程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Cms

  1. Concurrent Mark Sweep的缩写,并发标记清除,大多数互联网公司现在都在使用这个收集器
  2. 标记清除算法
  3. 工作过程:初始标记,并发标记,重新标记;并发清理
  4. 初始标记:该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。需要STW。注意是只是标记GC ROOT直接关联的对象那个,而对于间接关联的对象在下一阶段
  5. 并发标记:其是与用户线程并发执行的过程。由初始标记阶段标记过的对象出发,所有可到达的对象都在本阶段标记
  6. 重新标记:修正并发标记期间因用户程序继续运行而导致标记发生改变的那一部分对象的标记。需要STW
  7. 并发清理:与用户线程并发执行的过程,主要收集垃圾
  8. 优点:低停顿
  9. 缺点:占用大量CPU(当CPU数量在4个以上时,并行回收时垃圾收集线程不少于25%的CPU资源,而在不足4个,可能影响更大)。无法处理浮动垃圾(在并发清理阶段用户线程还在运行着,自然会有新的垃圾不断产生,这一部分只能留待下一次GC再清理)。产生空间碎片(因为其使用的是标记清除算法)
  10. -XX:+UseConcmarkSweepGC:开启
  11. -XX:ParallelCMSThreads:手工设置CMS线程个数,CMS默认启动的线程数是(ParalleleGCThreads+3/4)
  12. -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后发出垃圾收集,默认值为68%
  13. -XX:+UseCMSCompactAtFullCollection:由于CMS收集器会产生碎片,此参数设置在垃圾收集后是否需要一次内存碎片整理工程
  14. -XX:+CMSFullGCBeforeCompaction:设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通过UseCMSCompactAtFullCollection一起使用

G1(Garbage First)

在G1算法中,采用了一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存。
在这里插入图片描述
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

Region

堆内存中一个Region的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方,如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实际大小。

GC模式

G1中提供了三种垃圾回收模式,yong gc,mixed gc和full gc,在不同的条件下被触发

young gc

发送在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

mixed gc

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

那么什么时候mixed gc会被触发?
当老年代的使用率达到80%时,就会触发一次cms gc。相对的,mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.

mixed gc的执行过程有点类似cms,主要分为以下几个步骤:

  1. initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
  2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC
  3. remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
  4. 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中

full gc

如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.

内存分配策略

优先分配到Eden

大多数情况下,对象在新生代Eden去中分配,但Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

private static final int _1MB = 1024*1024;
byte[] allocation1,allocation2,allocation3,allocation4;
/**
* VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc - Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];
[GC (Allocation Failure) [DefNew: 7060K->1023K(9216K), 1.5357762 secs] 7060K->3651K(19456K), 2.2617522 secs] [Times: user=0.00 sys=0.00, real=2.26 secs] 
[GC (Allocation Failure) [DefNew: 5439K->1K(9216K), 0.0492012 secs] 8067K->7748K(19456K), 0.0492559 secs] [Times: user=0.03 sys=0.00, real=0.05 secs] 
Heap
def new generation   total 9216K, used 4261K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff029020, 0x00000000ff400000)
 from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400570, 0x00000000ff500000)
 to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation   total 10240K, used 7747K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  the space 10240K,  75% used [0x00000000ff600000, 0x00000000ffd90e20, 0x00000000ffd91000, 0x0000000100000000)
Metaspace       used 3562K, capacity 4496K, committed 4864K, reserved 1056768K
 class space    used 382K, capacity 388K, committed 512K, reserved 1048576K

结果分析:
把JVM设置了不可拓展内存20M,其中新生代10MB,老年代10MB,而新生代区域的分配比例是8:1:1,使用Serial/Serial Old组合收集器。其中,allocation1、allocation2、allocation3一共需要6M,而Eden区一共有8M,a1,a2,a3优先分配到Eden,再分配allocation4的时候Eden空间不够,执行了一次Minor GC,由于Survivor只有1M,不够存放a1,a2,a3,所以就直接迁移到了老年代了,后Eden空闲出来了就可以放allocation4了。

大对象直接分配到老年代

虚拟机提供了一个-XX:PretenureSizeThreshold参数来设置大对象的界限,大于此值则直接分配在老年代去了

private static final int _1MB = 1024*1024;
/**
 VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 -XX:PretenureSizeThreshold=3145728
 */
byte[] allocation1;
allocation1 = new byte[4 * _1MB];
Heap
 def new generation   total 9216K, used 5012K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  61% used [0x00000000fec00000, 0x00000000ff0e5308, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3511K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 380K, capacity 388K, committed 512K, reserved 1048576K

结果分析:
the space 10240K, 40% used。大于PretenureSizeThreshold定义的阙值,所以直接分配到老年代了。

长期存活的对象会进入老年代

java虚拟机为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。每熬过一次年龄就增加1.当达到(默认是15,有参数可以设置)一定程度,就会被晋升到老年代中。

 private static final int _1MB = 1024*1024;
    
/**
 * VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 * -XX:MaxTenuringThreshold=15 OR 1
 */
public static void testAllocation(){
    byte[] allocation1,allocation2,allocation3;

    allocation1 = new byte[1 * _1MB / 4];
    allocation2 = new byte[4 * _1MB];
    allocation3 = new byte[4 * _1MB];
    allocation3 = null;
    allocation3 = new byte[4 * _1MB];
}

MaxTenuringThreshold = 15

[GC (Allocation Failure) [DefNew: 5104K->1023K(9216K), 0.0053306 secs] 5104K->1822K(19456K), 0.0054776 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 5204K->1K(9216K), 0.0084990 secs] 6003K->5920K(19456K), 0.0085588 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 4152K->1K(9216K), 0.0006743 secs] 10071K->5920K(19456K), 0.0007392 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4297K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff032018, 0x00000000ff400000)
  ***from space 1024K,   0%*** used [0x00000000ff500000, 0x00000000ff500770, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 5918K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  57% used [0x00000000ff600000, 0x00000000ffbc79a0, 0x00000000ffbc7a00, 0x0000000100000000)
 Metaspace       used 3424K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 373K, capacity 388K, committed 512K, reserved 1048576K

MaxTenuringThreshold = 1

[GC (Allocation Failure) [DefNew: 5104K->1024K(9216K), 0.0057521 secs] 5104K->1848K(19456K), 0.0058778 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 5204K->0K(9216K), 0.0066982 secs] 6028K->5944K(19456K), 0.0067501 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 4151K->0K(9216K), 0.0007188 secs] 10095K->5944K(19456K), 0.0007856 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4296K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0321b0, 0x00000000ff400000)
  ***from space 1024K,   0%*** used [0x00000000ff500000, 0x00000000ff5000c8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 5944K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  58% used [0x00000000ff600000, 0x00000000ffbce0b8, 0x00000000ffbce200, 0x0000000100000000)
 Metaspace       used 3457K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

结果分析:
从以上输出信息可以看到,无论是MaxTenuringThreshold=15还是1,执行结果都是一样的,至少跟书本描述的不一致。应该是JDK版本不同而有所差异
当切换到JDK6时执行相同的代码
MaxTenuringThreshold = 15

 [GC [DefNew: 4679K->375K(9216K), 0.0044310 secs] 4679K->4471K(19456K), 0.0044650 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 4635K->375K(9216K), 0.0086340 secs] 8731K->4471K(19456K), 0.0086660 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
Heap
def new generation   total 9216K, used 4635K [0xee330000, 0xeed30000, 0xeed30000)
eden space 8192K,  52% used [0xee330000, 0xee758fe0, 0xeeb30000)
***from space 1024K,  36%*** used [0xeeb30000, 0xeeb8dc68, 0xeec30000)
to   space 1024K,   0% used [0xeec30000, 0xeec30000, 0xeed30000)
tenured generation   total 10240K, used 4096K [0xeed30000, 0xef730000, 0xef730000)
***the space 10240K,  40***% used [0xeed30000, 0xef130010, 0xef130200, 0xef730000)
compacting perm gen  total 16384K, used 1912K [0xef730000, 0xf0730000, 0xf3730000)
the space 16384K,  11% used [0xef730000, 0xef90e3b8, 0xef90e400, 0xf0730000)
No shared spaces configured.

MaxTenuringThreshold = 1

 [GC [DefNew: 4679K->375K(9216K), 0.0037650 secs] 4679K->4471K(19456K), 0.0037960 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 4471K->0K(9216K), 0.0010150 secs] 8567K->4471K(19456K), 0.0010580 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
def new generation   total 9216K, used 4423K [0xee350000, 0xeed50000, 0xeed50000)
eden space 8192K,  54% used [0xee350000, 0xee7a1fa8, 0xeeb50000)
***from space 1024K,   0%*** used [0xeeb50000, 0xeeb50000, 0xeec50000)
to   space 1024K,   0% used [0xeec50000, 0xeec50000, 0xeed50000)
tenured generation   total 10240K, used 4471K [0xeed50000, 0xef750000, 0xef750000)
***the space 10240K,  43***% used [0xeed50000, 0xef1adc50, 0xef1ade00, 0xef750000)
compacting perm gen  total 16384K, used 1912K [0xef750000, 0xf0750000, 0xf3750000)
the space 16384K,  11% used [0xef750000, 0xef92e3b8, 0xef92e400, 0xf0750000)
No shared spaces configured.

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保

在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果这个条件成立,那么Minor GC可以确保安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这个Minor GC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那么这时也要改为进行一次Full GC了。说白了就是虚拟机避免Full GC执行的次数而去做的检查机制。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

另外注意的是,在JDK 6 Update 24之后,虚拟机已经不再使用HandlePromotionFailure参数了,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

垃圾回收触发条件

  1. 一个对象实例化时,先去查看Eden有没有足够的空间
  2. 如果有,不进行垃圾回收,对象直接分配在Eden存储
  3. 如果Eden内存已满,会进行一次Minor GC
  4. 然后再进行判断Eden的内存是否足够
  5. 如果仍然不足,则去看存活区的内存是否足够
  6. 如果内存足够,把Eden部分活跃对象保存再存活区,然后把对象保存在Eden
  7. 如果内存不足,向老年代发出请求,查询老年代的内存是否足够
  8. 如果老年代内存足够,将部分存活区的活跃对象存入老年代,然后把Eden的活跃对象放入存活区,新的对象依旧保存在Eden
  9. 如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 同上.
    10.如果还不足,会抛出 OutOfMemoryError

在这里插入图片描述

参考博客

G1垃圾收集器介绍:https://www.jianshu.com/p/0f1f5adffdc1
《深入理解Java虚拟机》内存分配策略:https://www.cnblogs.com/wcd144140/p/5649553.html

原文地址:https://www.cnblogs.com/liuligang/p/10624077.html