JVM实战与原理
目录
内存回收策略
章节目的:介绍垃圾收集的算法有哪些?JDK提供的垃圾收集器的特点及运作原理?
引言:当内存空间被动态分配时,同样的,我们就需要进行回收,这就是我们常说的GC,垃圾收集技术。
那么我们为什么需要学习内存回收和GC呢?答案:当我们的程序出现内存泄漏或溢出方面的问题,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对垃圾收集技术进行监控和调节。
1. 堆的划分
HotSpot虚拟机将堆空间划分为Eden区,两个Survivor空间,老年代。
对象在堆的生存流程:对象首先在Eden区中分配,当Eden区空间不足时,虚拟机会发起第一次Minor GC(指在新生代发生的垃圾收集动作),将Eden区仍然存活的对象移动到Survivor-0空间,并设置存活对象年龄设定为1,同时清理Eden区,新进对象则继续分配到Eden区,直至Eden区再次空间不足,虚拟机则发起第二次Minor GC,将Eden区与Survivor-0区仍然存活的对象移动到Survivor-1空间,并设置存活对象年龄设定为2,同时将Eden与Survivor-0空间清空。对象在Survivor区中每经过一次Minor GC,年龄就增加1岁,当年龄增加到一定岁数,比如默认15岁,就会被移动到老年代中。而当老年代空间不足时,虚拟机会发起一次Major GC/Full GC,此次GC会清理老年代死亡的对象。
GC分为两步,第一步是根据算法判断对象是否还存活,第二步根据算法对可回收的内存区域进行清理,下面是对这两种算法的介绍。
2. 判断对象是否存活的算法介绍
2.1 引用计数算法
算法原理:给对象添加一个引用计数器,如果有一个地方引用它,计算器值就加1,当引用失效,则计数器值减1。当计数器值为0时,对象就是不可能再被使用的。
例如String str = new String("");,此时堆中空字符串对象实例的计数器值为1,如果后面将str = new String("a"),此时str的引用指向了a,原来空字符串对象的计数器值便减1为0了
算法缺陷:引用计数算法没办法解决相互循环引用的问题,例如objA.instance = objB; objB.instance = objA; 将objA = null; objB = null,此时objA和objB都不可能被访问了,但是objA和objB相互引用对方,两者的计数器值都为1,没办法被回收,故java虚拟机没使用该算法
2.2 可达性分析算法
算法原理:通过一系列“GC Roots”的对象作为起始点,从这些节点往下搜索,若从GC Roots到这个对象不可达,即GC Roots没有任何引用链相连到该对象,则此对象是不可用的。java虚拟机使用该算法对对象存活与否进行判断
3. 可回收的内存区域清理的算法介绍
3.1 标记-清除算法
算法原理:最基础的算法,分为标记和清除两个阶段,首先根据判断对象是否存活的算法,标记出所有可回收的对象,接着将标记的对象进行统一回收。
算法缺陷:
一、效率问题:标记和清除两个过程的效率不高
二、空间问题:标记清除后,会产生大量不连续的内存碎片,导致后续如需分配大对象时,无法找到足够的连续内存
3.2 复制算法
算法原理:将内存分为同等大小的两块,每次只使用一块,当这块内存用完时,将存活的对象复制到另外一块上面,再把原来这块的内存空间一次清理掉。这样内存分配时就不用考虑内存碎片等复杂问题。
算法缺陷:算法将内存缩小为原来的一般,代价太高了。
3.3 标记-整理算法
算法原理:标记过程与“标记-清除”算法一样,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
算法缺陷:同样存在效率问题
3.4 分代收集算法
该算法是当前商业虚拟机都采用的垃圾收集算法
算法原理:根据对象存活周期的不同将内存划分为几块,如Java堆分为新生代和老年代。根据各个年代的特点采用适用的手机算法,在新生代,因为每次垃圾收集都会有大批对象死去,故选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代则因为对象存活率高,则使用“标记-清理”或者“标记-整理”算法来进行回收。
4. 垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下面是对HotSpot包含的所有收集器进行讲解。
4.1 Serial收集器
特点:单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集,且进行垃圾收集时,须暂停其他所有的工作线程,直到收集结束。使用“复制”算法
使用场景:Client模式下的默认新生代收集器。该收集器简单而高效,对于限定单个CPU的环境,该收集器没有线程交互的开销,可以做到最高的单线程收集效率。且桌面应用分配的内存一般不大,停顿时间可控制在一百毫秒以内,故其是Client模式下一个很好的选择。
4.2 ParNew收集器
特点:Serial收集器的多线程版本
使用场景:许多Server模式下的虚拟机中首选的新生代收集器。原因是除了Serial收集器外,只有它能与老年代的CMS收集器配合工作。故老年代在使用CMS收集器时,新生代只能使用Serial与ParNew收集器,单CPU环境下Serial效果更好,随着CPU增加,则ParNew收集器会有很高的效率。
4.3 Parallel Scavenge收集器
特点:与其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间不同,该收集器的目标则是达到一个可控制的吞吐量,如虚拟机运行100分钟,其中垃圾收集耗费一分钟,则吞吐量就是99%。该收集器通过提供参数用于精确控制吞吐量。
使用场景:用于控制具体吞吐量的新生代收集器。
4.4 Serial Old收集器
特点:单线程收集器,Serial收集器的老年代版本,使用“标记-整理”算法
使用场景:一样主要给Client模式下的虚拟机使用
4.5 Parallel Old收集器
特点:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
使用场景:用于控制具体吞吐量的老年代收集器。
4.6 CMS收集器
特点:以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法
使用场景:重视服务的响应速度的应用,如B/S系统的服务端
//TODO
4.7 G1收集器
特点:
1. 利用多cpu或多核缩短Stop-The-World停顿的时间
2. 采用不同的方式去管理整个GC堆
3. 整体基于“标记-整理”算法,局部基于“复制”算法
4. 可预测的停顿
使用场景:未来可替换CMS收集器
//TODO
4.8 ZGC收集器
//TODO
4.9 Shenandoah GC
//TODO