JVM_GC
参考博客: https://blog.youkuaiyun.com/zjkC050818/article/details/78376588
程序计数器、虚拟机栈、本地方法栈 随线程而生,随线程而灭
一、什么是GC?
二、什么时候发生GC?
- 宏观上看:GC发生的时间在整个Java程序的生命周期上是不可预估的,是由系统自身决定,或者程序调用System.GC()。
- 微观上看:
- (分代回收) 现在的虚拟机大多采用分代收集算法,所谓的“分代收集”就是 根据对象的存活周期不同,将内存划分为几块,一般将Java堆分为 新生代 和 老年代 ,并根据各个年代的特点采用 不同的、最优的 回收策略,通常有minor gc 和 full gc。
- (回收策略) minor gc: 使用的是复制算法,针对新生代进行GC。
- 由于Java中的大部分对象通常都是招生夕灭的,新生代是GC垃圾回收最为频繁的一个区域,新生代分为 Eden区和两个Survivor区,其内存大小比例为8:1:1,绝大数新产生的对象会存储在新生代的Eden,大对象会直接放在老年代。
- 每当Eden产生对象时,都会检查Eden区是否可以将这个对象存下,可以存下则存,如果不可以的话就会触发minor gc。 将Eden区和Survivor from区根据复制算法把所有的存活对象复制到 Survivor to区,随后回收Eden区和Survivor区,回收后再存储对象。
- 但如果是极端情况下,经过minor gc后还是无法放下这个对象,JVM就根据空间分配担保机制,如果允许担保就将Survivor无法容纳的对象直接进入老年代。这个时候如果老年代还是分配不下的话触发一次full GC,如果还是不可以的话就会抛出OOM异常。
- 还有就是在新生代每经一次 minor gc 对象的年龄就加一,当对象的年龄大于一定年龄,就会将这个对象移入老年代(默认的是15岁,也可以通过参数设置),但并不是永远要等到对象年龄大于指定年龄才把对象放入老年代,JVM会有个 动态对象年龄判定 机制,如果在 survivor 空间中相同年龄的所有对象的总和大于 Survivor 空间的一半时,就将年龄大于等于该相同年龄的对象直接进入老年代。
- 而且对于比较大的对象则直接分配到老年代。
- (回收策略)full gc: 发生在老年代,使用的是标记算法。
- 老年代的对象一般由新生代转移过来,或者大对象的直接分配。
- 在升到老年代的对象大于老年代剩余空间时发生full gc,对老年代进行清理,清理后再将新生代的对象升入老年代,如果这时老年代空闲空间还是不足以容纳升入老年代的对象时,就会触发OOM异常。
- 或者小于时,由于 空间分配担保机制 强制full gc。在minor gc 之前 检查到minor gc 不安全,而且虚拟机不允许垃圾回收器冒险进行minor gc ,这是就会触发 full gc,尽量在老年代腾出足够大的空间,以防minor gc 出现风险。
- 如果已经为了担保而发生full gc过后,还是无法腾出足够大的空间进行担保,那么只好再发起一次 full gc。但是为了避免频繁 full gc, 大多数情况下还是将 HandlePromotionFailure 参数打开,允许GC 进行冒险地进行minor gc。
- full gc 的过程就是 扫描出存活对象,然后进行回收未标记的对象,回收后对空出的空间进行合并,要么标记出来用于下次进行分配,总之就是要减少内存碎片带来的效率的损耗,如果发生过full gc 后还是无法存放对象,则将抛出OOM异常。
附:
- (回收策略,即垃圾回收算法) 有 标记-清除法 、 复制算法 和 标记整理法。
- 标记-清楚法:首先它先标记出所有需要回收的对象,标记完成后统一将所有标记的对象进行回收。
优缺点:(1) 一个是清除后会产生大量不连续的空间,将内存碎片化,如果要存储一个大的对象,可能虽然有很多未使用的空间,但没有那么大的连续空间,还是无法存储,发生OOM;(2) 第二个是其标记和清除的 效率很低 ,GC时发生GC停顿,再使用低效率的标记清楚法对程序的影响还是很大的。 - 复制算法:将内存划分为两个等大的空间,每次将存活的对象整齐复制到另一空间,并将已使用的空间全部清理。
优缺点:内存分配就没有内存的碎片化问题,但是需要浪费一半的内存空间。 - 标记-整理法:和标记-清除法类似,但是不是对可回收对象进行清理,而是将存活的对象统一移动的内存的一端,然后清理到边界外的内存即可。
优缺点:较标记清除法不会出现内存的碎片化,但是随之带来的是每次GC都要多次执行复制操作,效率会很低。
注: 一般垃圾收集器会使用分代回收,新生代(Eden/Survivor1/Survivor2)采用 复制算法,老年代使用标记(Mark)算法,扫描出存活对象,然后进行回收未标记的对象,回收后对空出的空间进行合并,要么标记出来用于下次进行分配,总之就是要减少内存碎片带来的效率的损耗。
- 标记-清楚法:首先它先标记出所有需要回收的对象,标记完成后统一将所有标记的对象进行回收。
- (空间分配担保机制)
- 新生代分配对象在eden区,当存储不下时会触发Minor gc ,清理Eden区和Survivor from去,并把存活对象转义到Survivor to区,但是经过Minor GC后,Survivor to区还是不足以存下存活的对象,这时候就会将存储不下的对象担保金老年代进行存放,但是如果老年代也存储不下,就会存在Minor GC 的风险。
- 空间分配担保就是 在Minor GC 之前检查 是否存在这种风险,如果不存在则正常Minor GC ,如果存在 则根据 HandlePromotionFailure 的设置,询问JVM允不允许冒险的进行 Minor GC,如果允许则冒险进行 Minor GC,如果不允许冒险,则会将老年代进行 full gc ,在老年代腾出足够大的空间保证Minor GC 是安全的。
- 但是如果为了Minor GC的安全性进行了Full GC,Full GC过后还是无法保证安全性,就会再次出发Full GC,直至保证Minor Gc安全才停止,这样会导致 Full GC的频繁进行,使系统的效率会很低,在一般情况下会打开HandlePromotionFailure 参数,允许风险的Minor gc进行,防止full gc 频繁进行。
- 那么如何判断Minor gc是否存在风险呢?因为风险就是系统不确定如果Minor gc 后新生代还未分配的对象是否能都在老年代存储,这时只需要比较 老年代最大可用的连续空间 和 新生代所有对象的总空间即可,如果老年代最大连续空间还是较大的,说明即使新生代分配不下,在老年代还是可以将新生代的所有对象转移到老年代为新生对象腾出空间。
三、对什么东西进行GC?
- GC对的主要是系统不再需要的对象,而JVM可通过两种算法判断对象是否使用,一种是 引用计数法 ,一种是 可达性分析法。
- 引用计数法: 会给每个对象添加一个计数器,每当被引用,计数器就加一,当失去引用时,计数器就减一;当一个对象的计数器值为0时,就说明没用任何引用再引用它,那么就需要回收它;但是引用计数法难以解决循环引用问题,至使JVM无法识别它是垃圾对象,就无法进行回收。
- 可达性分析法:现在的虚拟机一般都用的可达性分析法来判断对象是否要被回收;具体的算法就是,通过一系列的GC Roots 的对象为起始点,从这些节点往下搜索,搜索到走过的路径为引用连,当一个对象没有在任何GC Roots的引用链上时,也就说明这个对象无用,要被垃圾回收。
- GC Roots:
- 虚拟机栈中引用的对象(虚拟机栈中包含很对栈帧,而还存在的栈帧一定是现在还要使用的,不然会出栈,所以从虚拟机栈中的对象作为GC Roots可达的对象都是有用的对象)
- 方法区的类静态属性引用的变量
- 方法区中常量引用的对象
- 本地方法栈中 (Native方法)引用的对象
finalize
- 对象的自我拯救:GC 标记之后,清理之前会判断对象是否重写了finalize()方法,如果重写了将执行该对象的finalize()方法,可在该方法中将这个对象重新引用给GC Root对象实现这个对象的一次自我救赎。如果该类没有重写该方法,或者重写了但是没有赋给可达对象,这个对象也是会被认为是垃圾对象。而且对象的该方法只会执行一次,如果第二次这个对象被标记为垃圾对象,将不再执行该方法,就会被回收。
- 附: finalize()方法具体细节: 如果一个对象没有一条引用链经过该对象,就会对它进行第一次标记和进行一次筛选。
- 筛选就是看这个对象有没有必要执行finalize方法。当一个对象没有覆盖finalize方法或者已经执行过了 finalize方法,虚拟机都会视为没有必要执行;
- 当虚拟机认为有必要执行时,就会把这个对象放入一个叫F-Queue的队列中,并在稍后由一个虚拟机自动建成的一个优先级很低的线程去执行在这队列中所有对象的finalize方法。
- 因为JVM创建的是一个优先级特别低的线程去执行它,而且也为了确保不因为finalize方法中的错误而影响垃圾回收,所有Java并不会保证finalize方法会被及时执行,而确也不会保证他们会被执行完。
- 所以在平常的开发中,还是要避免使用finalize方法,有需要关闭资源等需求还是尽量使用try-finaly来实现。
- 直接内存的垃圾回收:对于Java的直接内存,是独立于堆内存之外的内存区域,它不由JVM进行管理,而由操作系统来进行管理,但是JVM在Full GC时也会对其进行垃圾回收。但是它不会像新生代和老年代一样,当内存满了会告诉JVM要进行垃圾回收,直接内存只能是跟着堆空间的Full gc顺带着一起垃圾回收,所有在很多时候直接内存会满而没办法及时的回收。解决办法只能是当它抛出异常时,及时catch到,然后立马调用System.gc();通知虚拟机进行全局的垃圾回收。
四、GC时做什么事情?
除了之前说的GC时会清理不适用的对象之外,还有些比较细节的。
- (停止其他线程) 虚拟机在从GC roots搜索可以可达的对象时,是需要将其他线程都挂起,因为在分析GC Roots对象和分析可达对象时对象的引用关系可能发生改变,这样的话分析结果就没有办法得到保证,所以GC进行的时候其他线程都要挂起等待GC结束。(不包括JNI调用的线程,因为其调用的线程所产生的垃圾由调用线程的语言维护)
- 安全点
- 安全区域
- 老年代碎片整理
五、GC调优
- 线程不易的过多
- 内存区域大小使用参数调节
- 直接内存的垃圾回收
六、 扩展
1. 强引用、软引用、弱引用、虚引用
- 强引用: 就代码中普通的引用,只要强引用存在,这个对象就不会被回收;
- 软引用: 一些引用但非必须的对象。系统在内存发出溢出之前就会将其回收;(SoftReference类)
- 弱引用: 非必须的对象,只能生存到下一次GC;(WeakReference类)
- 虚引用: 虚引用有一个很重要的用途就是用来做堆外内存的释放(PhantomReference)
https://blog.youkuaiyun.com/qq_39541319/article/details/89715289
2. 串行GC、并行回收GC、并行GC
- 串行GC
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定 - 并行回收GC
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数 - 并行GC
与旧生代的并发GC配合使用。