《深刻理解Java虚拟机》第3章读书笔记java
本文介绍了如何判断对象是否存活,三种垃圾回收算法,分析比较了几种垃圾收集器的特色。本文并不是原创,是《深刻理解Java虚拟机》第3章的整理、总结和补充。算法
垃圾收集器在对堆进行回收前,要先判断哪些对象“存活”,哪些已经“死去”。json
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。安全
主流的Java虚拟机里面没有选用引用计数算法来管理内存。服务器
优势:实现简单,效率高。微信
缺点:很难解决对象之间相互循环引用的问题。多线程
循环引用问题,以下代码所示,并发
/** * 源代码出自《深刻理解Java虚拟机》P62-63 * 循环引用 **/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/** * 这个成员属性的惟一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */
private byte[] bigSize = new byte[2 * _1MB];
/** * 运行参数 * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M */
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
复制代码
JVM参数设置了新生代为10MB,运行结果显示,在第一次触发GC时,“5120K->576K(9216K)”回收了约4MB内存。意味着虚拟机并无由于两个对象相互引用就不回收它们,这也侧面说明了虚拟机并非经过引用计数算法来判断对象是否存活网站
经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲,就是从GC Roots到这个对象不可达)时,则证实此对象是不可用的。spa
如图,object五、object六、object7 为可回收对象
主流的Java虚拟机使用可达性分析算法
在Java语言中,GC Roots包括如下几种:
最基础的收集算法——“标记-清除”(Mark-Sweep)算法。通常用于老年代。
算法分为“标记”和“清除”两个阶段:
缺点:效率低,空间碎片化。
为了解决效率问题,出现了“复制”算法(Copying)。它将内存分为两块,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,而后再把已使用过的内存空间一次清理掉。
缺点:内存缩小为原来的一半。
JVM虚拟机在新生代使用这种收集算法,并非按照 1:1 的比例来划份内存空间。而是根据新生代中的对象98%是“朝生夕死”这一特色,将内存分为一块较大的 Eden(80%) 空间和两块较小的 Survivor(10%) 空间。每次只使用 Eden 和其中一块 Survivor,当回收时,将 Eden 和 Survivor 中还存活的对象,一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot默认Eden和Survivor的大小比例为8:1,这样就只有10%的内存被“浪费”。当出现超过10%的对象存活时,就会使用老年代作分配担保,把Survivor空间放不下的对象,直接放入老年代。
在Mark-Sweep算法的基础上作了改良,用于解决空间碎片化问题。标记-整理(Mark-Compact)算法在标记后不是简单作清除,而是让全部存活的对象都向一端移动,而后清理掉端边界之外的内存。通常用于老年代。
在作可达性分析时,须要保持分析期间整个系统不会发生变化,这就致使GC进行时必须停顿全部Java执行线程(Stop The World),即便是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也必需要停顿。
程序执行时并不是在全部地方都能停下来开始GC,只有在到达**安全点(Safepoint)**时才能暂停。Safepoint 的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。因此,安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的,例如方法调用,循环跳转,异常跳转等。
如何在GC发生时让线程都跑到最近的安全点再停顿下来?
安全区域(Safe Region)是指在一段代码片断中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。典型的安全区域好比线程处于Sleep状态或者Blocked状态。
在线程执行到Safe Region中的代码时,首先标识本身已经进入了Safe Region。当要发起GC时,就不用管标识为Safe Region状态的线程了。当线程要离开Safe Region时,要检查是否处于GC状态,若是是,就要继续等待,直到收到能够安全离开Safe Region的信号为止。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
收集器 | 串行、并行、并发 | 新生代、老生代 | 算法 | 目标 | 使用场景 |
---|---|---|---|---|---|
Serial | 单线程,串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 单线程,串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后 |
ParNew | 多线程,并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scanvenge | 多线程,并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不须要太多交互的任务 |
Parallel Old | 多线程,并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不须要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 复制算法+标记-整理 | 响应速度优先 | 面向服务端应用,未来替换CMS |
Serial收集器的多线程版本,Service模式下的首选新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工做。
ParNew 收集器是使用 -XX:+UseConcMarkSweepGC
选项后的默认新生代收集器,也可使用 -XX:+UseParNewGC
选项来强制指定它。
运行示意图以下:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于Mark-Sweep算法。运行过程分为四个部分:
其中,初始标记和从新标记仍然须要 Stop The World。初始标记只是标记一下 GC Roots 能直接关联到的对象,速度很快。并发标记就是进行 GC Roots Tracing 的过程,而从新标记阶段则是为了修正并发标记期间用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长一些,但远比并发标记的时间短。
因为整个过程当中耗时最长的并发标记和并发清楚过程收集器线程均可以与用户线程一块儿工做,因此,从整体上来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。
运行示意图以下:
CMS 收集器有以下3个缺点:
对CPU资源很是敏感
由于并发阶段须要占用一个用户线程,若是CPU小于4个,则会致使用户程序的执行速度降低大于25%,若是只有2个CPU,用户程序执行速度则会降低50%,这是让人没法接收的。通常来讲使用CMS收集器的服务器配置至少须要4个CPU。
没法处理浮动垃圾
在并发清理过程当中产生的垃圾称为“浮动垃圾”。这些垃圾只能等待下次垃圾回收。所以,CMS 收集器不能像其余收集器那样等到老年代几乎被彻底填满了再进行收集,须要预留一部分空间提供并发收集时的程序运做使用。
内存空间碎片化
CMS 收集器是基于Mark-Sweep算法,这个算法会产生内存空间碎片。CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection
开关参数(默认为开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程。内存整理的过程是没法并发执行的,空间碎片问题没有了,但停顿时间不得不变长。
如过同时使用了四个组合配置,这是时候就会报错
可是好比 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
这两个配置同时存在就不会报错。
翻看源码可知
有些配置项是能够并存的。
其实,在使用 UseConcMarkSweepGC
配置的时候,虚拟机默认开启了 UseParNewGC
因此在配置JVM时,咱们尽可能显式配置。好比要启用 ParNew + CMS 组合能够配置为
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
复制代码
本文首发于个人我的博客 chaohang.top
做者 张小超
公众号【超超不会飞】
转载请注明出处
欢迎关注个人微信公众号 【超超不会飞】,获取第一时间的更新。