垃圾收集器与内存分配策略
主要针对堆的对象的回收,由于java虚拟机栈的栈帧的内存分配在编译期就已经确定了(尽管JIT编辑器会对其做一些相应的优化,但是在概念模型上,大体认为是编译器可知的),所以主要考虑堆中对象的回收
如何判断对象已死
引用计数器法
实现原理:给对象引入一个引用计数器,当该对象被引用时,计数器值加一,引用失效时,计数器值减一
缺点:当对象之间存在循环引用的时候,其实相互之间已经没有对象存在的实际意义了,已经是“垃圾对象”了,但是却因为计数器值不为0导致对象无法回收
根搜索算法
实现思路:从“GC Root”节点向下搜索,搜索所走过的路径称为引用链,当一个对象到“GC Root”节点没有任何引用链时,则说明该对象是不可达(即不存在被引用关系的对象),此时它们将会判定为不可回收的对象。根据对象的不同的引用关系,分为强引用、软引用、弱引用和虚引用,每种引用的回收策略都不一样。
垃圾收集算法
标记-清除算法
实现思路:标记出需要收集的对象,当标记完成后统一回收掉所有被标记的对象
缺点:标记和清除的效率都很低;另一个就是空间问题,在标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致程序在以后的运行中需要分配较大对象时无法找到足够的连续空间不得不提前触发一次垃圾回收,更可怕的是当碎片越来越多时,空间变得越来越零散,最终导致的结果就是一个较大的对象分配不到足够的内存空间导致内存溢出问题。
复制算法
实现思路:为了解决标记-清除算法的缺点引入了复制收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对一块内存进行回收避免了内存碎片的问题,同时效率也提高不少。
缺点:把内存分为2块相当于把可内存缩小了一半,效率提高了很多(只需要移动堆顶指针改变对象的引用地址),代价太大;其次当对象存活率大时,复制操作需要大量进行,导致效率低下。
适合:回收新生代中的内存(因为新生代的对象很多都是朝生夕死的,所以需要复制的对象不是很多,但是老年代的对象则大多是很难“死”的)
标记-整理算法
实现思路:基于标记-清除算法改进后的到了标记-整理算法,区别在于对标记好需要回收的对象不是直接的清除而是通过将存活的对象向一端移动,然后直接清理掉端边界以外的内存
适合:回收老年代的内存
分代收集算法
实现思路:根据对象的存活周期将内存划分为几块,一般把Java堆划分新生代和老年代,然后根据每个年代的特点采用最合适的收集算法。在新生代中,每次垃圾回收之前都有大量的对象死去,只有少量存活,所以使用复制算法,在老年代中有大量对象存活所以使用标记-整理算法或者标记-清除算法来回收。
垃圾收集器
各种垃圾收集器的组合:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
根据实现不同的垃圾收集算法,产生了不同的垃圾收集器。不同的垃圾收集器有不同的作用场景。下图是各垃圾收集器的不同搭配(有直接连线的说明可以一起搭配):
Serial收集器
- 单线程的收集器
- 在它进行垃圾回收时需要暂停用户线程(暂停时间长,主要是使用的标记-清除时效率低下导致)
缺点:收集器工作时,需要暂停用户进程导致用户等待,用户体验差;使用标记-清除算法有内存碎片产生
适合:client模式下的新生代收集器
ParNew收集器
- Serial收集器的多线程版本,支持多线程垃圾回收
适合:运行在server模式下的虚拟机中首选的新生代收集器
Parallel Scavenge收集器
- 新生代收集器,使用复制算法实现,多线程并行执行对象收集工作
优点:让系统能够达到一个可控制的吞吐量(高吞吐量:最大效率的利用CPU执行时间,通常用于计算,不适合做交互)
吞吐量=用户运行代码时间/用户运行代码时间+垃圾收集时间复制代码
适合:做新生代收集器
使用:打开-XX:+UseAdaptiveSizePolicy开关,通过设置MaxGCPauseMillis参数或GCTimeRatio给JVM设立一个优化目标,让JVM根据对应用的监控信息动态调整参数以提供最合适的停顿时间或最大的吞吐量。
-XX:+MaxGCPauseMillis=100 (单位毫秒)
--XX:+GCTimeRatio=20 (垃圾收集时间占CPU运行时间的百分比,默认1,即1%)
Serial Old收集器
- serial收集器的老年代版本,是个单线程收集器,使用标记-整理算法
适合:client模式下的虚拟机使用,可与Parallel Scavenge搭配使用
Parallel Old收集器
- Parallel Scavenge的老年代版本,使用标记-整理算法实现
- 适合:需要达到高吞吐量的系统(前提是系统硬件高端),通常与Parallel Scavenge收集器结合使用
CMS收集器
- 基于标记-清除算法实现
优点:并发收集、低停顿(追求响应速度)
过程:初始标记(暂停用户线程)--并发标记--重新标记(暂停用户线程)--并发清除
缺点:对CPU资源敏感(如果CPU数量达不到要求,将导致CPU在处理用户线程紧张时还要进行垃圾回收,使得响应速度减慢);无法处理浮动垃圾,在并发清除时由于和用户线程并发执行,导致清除时仍然有垃圾产生,这类垃圾是无法清除的(假设这个时候CPU在高速计算,产生的垃圾很多,这样一次并发清除下来又有很多垃圾产生,导致加速下一次GC的到来);基于标记-清除算法,导致会有空间碎片产生
G1收集器
- 基于标记-整理算法实现的收集器
- 可以精准的控制停顿,即能将垃圾回收控制在一定时间内
- 通过将Java堆划分为多个大小固定的独立区域,在后台维护一个记录各个区域垃圾堆积程度的优先级列表,通过这个列表对垃圾堆积多的区域优先清除,使得垃圾的堆积程度始终处于一个稳定状态,让用户线程始终有足够的内存可使用(mixed gc)
- 收集过程:goung gc——mixed gc——full gc
- 优点:在多处理器和大容量内存环境中,在满满足高吞吐的同时尽可能的满足垃圾回收时的低停顿
使用 java -XX:+PrintCommandLineFlags -version 查看当前 jvm 使用的是什么垃圾收集器
内存分配策略
- 对象优先在 Eden区分配
- 大对象直接进入老年代(只适用于ParNew和serial收集器)
- 长期存活的对象将进入老年代
- 动态对象年龄判定(在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半)
- 空间分配担保(在对象晋升到另一个Survivor时该区域无法容纳该对象担保进入老年代)
优化案例
- 虚拟机JVM进程奔溃——南方公众号,原因:调用南网内部接口由于超时导致等待的线程数和socket连接数越来越多导致超出虚拟机的能力而奔溃
- 系统卡死,无响应—— 电子资料宣传机, 原因:序列化后的文档对象全部进入老年代,gc停顿导致。
参考网站:
G1垃圾收集器: