1. 简述java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
2. 内存分配和GC分类
2.1 对象优先在 Eden 区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
2.2 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
2.3 长期存活的对象将进入老年代
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
2.4 空间分配担保
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
2.5 GC分类
部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
3. 死亡对象判断方法
3.1 引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
3.2 可达性分析算法
“GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
3.2.1 哪些对象可以作为 GC Roots 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
3.2.2 对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
4. 垃圾回收算法
4.1 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
缺点:
- 效率问题(标记和清除的效率都很低)
- 空间问题(会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作)
4.2 标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
缺点:需要两倍内存空间。
4.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4.4 分代收集算法
一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
5. 垃圾收集器
5.0 垃圾回收器性能指标
吞吐量:程序运行时间占总运行时间(总运行时间=程序运行时间+垃圾回收时间)的比例,垃圾回收时间越少,吞吐量越高;
暂停时间:STW的时间;
内存占用:Java堆所占的大小。
5.1 垃圾回收器组合
说明:
两个回收器间有连线,说明它们可以搭配使用;
Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案;
G1可用于新生代和老年代;
红色虚线连线:JDK 8将这两组组合声明为废弃,并在JDK 9中完全移除;
绿色虚线连线:JDK 14中,弃用了该组合;
绿色虚线边框:JDK 14中,删除了CMS。
5.2 垃圾回收器介绍
5.2.1 Serial、Serial Old回收器
Serial垃圾回收器为单线程串行回收器。采用标记-复制算法、串行回收和STW机制进行内存回收;
Serial Old垃圾回收器为Serial提供的老年代垃圾回收器,采用标记-整理算法、串行回收和STW机制进行内存回收。
Serial Old在Server模式下主要有两个用途:与新生代的Parallel Scavenge配合使用(弃用);作为老年代CMS回收器的后备方案。
5.2.2 ParNew 收集器(Parallel New)
Serial的多线程版本垃圾回收器。采用标记-复制算法,并行回收和STW机制进行内存回收。
Serial、ParNew搭配Serial Old回收器示意图:
5.2.3 Parallel、Parallel Old回收器
Parallel Scavenge回收器作用于新生代,同样采用标记-复制算法,并行回收和STW机制。
Parallel Old回收器作用于老年代。Parallel采用标记-整理、并行回收和STW机制。
5.2.4 CMS 收集器
CMS作为一款老年代的垃圾回收器,只能和ParNew或者Serial搭配使用。
初始标记:所有用户线程暂停(STW),这个阶段仅仅标记出GC Roots能直接关联到的对象,所以速度非常快,STW时间很短;
并发标记:该阶段从GC Roots直接关联对象开始遍历整个对象链,虽然这个过程耗时较长,但并不需要暂停用户线程,并发执行,没有STW;
重新标记:由于上一步用户线程也在执行,所以这一步用于修正因用户线程继续运行而导致标记发生变动的那一部分对象的标记记录。这个阶段会比初始标记阶段耗时长一点,但远比并发标记阶段低;
并发清除:该阶段清理删除垃圾,回收空间。由于没有移动对象,所以该阶段也不需要STW。
优点:并发收集、低停顿。
缺点:
会产生碎片。因为清理阶段用户线线程还在执行,所以只能采用不移动对象的标记-清除算法,而该算法会产生碎片问题;
对CPU资源敏感。CPU资源除了用于用户线程外,还需分配一部分用于处理垃圾回收,降低了吞吐量;
无法处理浮动垃圾。并发标记阶段,用户线程并未停止,该阶段也会产生垃圾, CMS无法对这些垃圾进行标记,只能留到下次GC时处理。
5.2.5 G1 收集器
G1(Garbage First)回收器把堆内存分割成很多不相关的区域(region,物理上不连续),使用不同区域来表示伊甸园区,幸存者区和老年代。
E表示伊甸园区,S表示幸存者区、O表示老年代,空白表示未使用的内存区域;
一个region在同一时间内只能属于一种角色(一个Regin可能存储的是Eden,Survivor或者Old的内存区域且只能是其中的一个。Regin存储的区域是可变的,这次存的是新生代空间,当Regin内没有存放数据后会被放入空闲队列,重新分配内存区域时可能就不是新生代的空间了);
Humongous,主要用于存放大对象(当一个大对象的大小超过1.5个regin时会被放到Humongous区域,因为大对象会直接分配到老年代,如果临时的对象放入了老年代就会造成内存回收不及时(老年代满了才回收垃圾)因此分配了Humongous存放大数据。)。
G1回收垃圾过程如下图所示:
初始标记:仅仅是标记GC Roots能直接关联的对象,需要STW,但这个过程非常快;
并发标记:从GC Roots出发,对堆中对象进行可达性分析,找出存活对象,该阶段耗时较长,但是可与用户线程并发执行;
最终标记:主要修正在并发标记阶段因为用户线程继续运行而导致标记记录产生变动的那一部分对象的标记记录,需要STW;
筛选回收:将各个region分区的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划。这阶段停顿用户线程,STW。
优点:
并行与并发;
分代收集,可以采用不同的算法处理不同的对象;
空间整合,标记压缩算法意味着不会产生内存碎片;
可预测的停顿时间,能让使用者明确指定一个长度为M毫秒时间片段内,消耗在垃圾回收的时间不超过N毫秒(根据优先列表优先回收价值最大的region)。
缺点:
在小内存环境下和CMS相比没有优势,G1适合大的堆内存;
在用户程序运行过程中,G1无论是为了垃圾回收产生的内存占用,还是程序运行时的额外执行负载都要比CMS高。
5.3 总结
来源:https://snailclimb.gitee.io/javaguide/#/./docs/java/jvm/jvm-garbage-collection
https://zhuanlan.zhihu.com/p/432229244