垃圾收集器和内存分配策略
JVM系列文章是基于:《深入理解Java虚拟机:JVM高级特性与最佳实践》-周志明第二版
一、对象存活算法?
- 对象存活算法是判断对象是否存活,或者说判断对象是否为垃圾的算法,这个是垃圾回收的前提。
1.1 引用计数法
- 为对象添加一个引用计数器,如果对象被引用则计数器加1,引用失效则减1,计数器为0则表示该对象为垃圾。不过主流JVM并没有采用该方法。
- 优缺点:
实现简单效率高
无法解决循环引用
1.2 可达性分析算法
- 通过一种GC ROOTS作为起点,从节点开始通过引用链进行搜索,如果能够达到一个对象说明该对象不是垃圾,反之所有的GC ROOTS都无法到达该对象,说明它是垃圾。
- GC ROOTS的选择。如果能够作为GC ROOTS,那么说明它自身肯定不是垃圾,主要包括全局性引用和执行上下文,可以作为GC ROOTS的对象如下:
1.虚拟机栈和本地方法栈中引用的对象;栈中的对象肯定不是垃圾,可以作为GC ROOTS(执行上下文)
2.方法区中的静态属性或者常量引用的对象;静态属性和常量引用的对象不是垃圾,可以作为GC ROOTS(全局性引用)
二、引用类型?
2.1 强引用
- 常用的赋值就是强引用,存在一个对象的强引用的话,该对象永远不会被垃圾回收器回收。
2.2 软引用
- OOM之前,垃圾回收器会尝试将软引用关联的对象回收,简而言之内存即将不足的时候才会回收,可以适合做缓存。
2.3 弱引用
- 弱引用关联的对象,最多活不过下一次垃圾回收。不管内存够不够用,下一次垃圾回收丢会被干掉。(ThreadLocal)
2.4 虚引用
- 虚引用对一个对象的生命周期没有影响,也无法通过虚引用来获得一个对象实例,作用仅仅只是在对象被回收的时候收到一个系统通知。
三、可达性和对象死亡?
3.1 堆区
- 如果可达性分析中,从GC ROOTS开始对象A不可达,那么不并代表A一定会被回收。如下图:
- 由上图可以看到在finalize()方法中可以完成对象的自我拯救,避免被回收,但是不推荐使用该方法来完成一些操作,比如关于资源回收的操作(使用try…catch…finally)。
3.2 方法区
- 方法区的回收条件较为严苛,因此回收的效率也比堆区要低。比如对一个类对象的回收,需要至少满足下面的条件(还至少必要条件):
1.类的实例全部被回收
2.加载类的ClassLoader被回收
3.类的Class对象没有在任何地方被引用,无法通过反射构造对象
- 相关参数:
-XX:TraceClassLoaing: 查看类加载信息
-XX:TraceClassUnLoaing: :查看类卸载信息
四、垃圾回收算法
4.1 复制算法
- 将存活的对象集中复制到一块区域,再清除之前的一块区域。
4.2 标记算法
- 标记清除:标记出全部的垃圾对象,再清除这些对象
- 标记压缩(整理):标记垃圾对象,然后将存活的对象移动到内存的一端
4.3 分代收集
- 堆包括新生代和老年代,新生代包含一个eden和两个survior,默认二者比例是8比1;在空间和性能做一个折中,这样指损失10%的内存
- 默认的新生代老年代比例为1:2(可通过–XX:NewRatio指定),即新生代=1/3的堆空间
- 针对新生代和老年代采用不同的垃圾回收算法
4.4 对比
回收算法 | 优点 | 缺点 | 应用区域 |
---|---|---|---|
复制算法 | 简单、高效 | 浪费空间、存活对象多时效率低 | 新生代 |
标记清除 | 思想简单 | 存在碎片,效率不高 | 老年代 (CMS) |
标记压缩 | 不存在碎片 | 老年代(G1) |
五、垃圾收集器
- 下面是7种垃圾收集器,收集器所在区域代表该收集器锁应用的区域,连线代表收集器可以搭配使用。
5.1 Serial
- 单线程收集器,收集期间用户线程需要停止(STW)。简单高效,是Client模式下新生代的默认收集器,适用于单CPU或者堆不大的时候选择(一二百兆)。
5.2 ParNew
- Serial的多线程版本,收集算法,控制参数、回收策略、对象分配规则等细节和Serial几乎一致,二者也共用了很多代码,不过单CPU性能不如Serial。
- 运行在Server模式下的默认新生代收集算法,一个重要原因是除了Serial只有ParNew可以和CMS搭配使用。
- 老年代使用CMS的话,新生代就会使用ParNew
-XX:ParallelGCThreads=4;设置垃圾收集时并发线程数
- 复制算法
5.3 Parallel Scavenge
- 使用复制算法,关注的是吞吐量而不是GC停顿时间,即关注垃圾收集时间对整个时间的占比。停顿时间短适合即时交互程序,吞吐量适合后台计算交互较少的情况。
-XX:MaxGCPauseMills:控制最大垃圾收集停顿时间(大于0的毫秒数,不过这并不意味着效率的提供,这里会牺牲吞吐量,需要综合考虑)
-XX:GCTimeRatio:设置吞吐量(0到100的数字,比如n代表垃圾收集的时间不超过1/(1+n) )
-XX:+UseAdaptiveSizePolicy:开启之后就不需要它设置新生代大小,新生代比例,晋升年龄等参数了,Parallel Scavenge会使用自适应调节策略来达到预定的吞吐量和最大收集时间。
5.4 Serial Old
- Serial的老年代版本,标记整理算法,其作用有二,1.5之前只有它能够搭配Parallel Scavenge使用,另外作为CMS的替补,为什么需要替补,在接受CMS的时候会提到。
- 也会STW,模式和Serial类似,单线程,但是算法不一样。
5.5 Parallel Old
- Parallel Scavenge的老年代版本,标记整理算法,1.6才有,因此1.5之前需要Serial Old和Parallel Scavenge搭配使用。
- 在吞吐量优先的场景,1.5之前因为只有Serial Old能够搭配Parallel Scavenge使用,但是Serial Old是单线程的,因此不能充分利用CPU多核优势,导致整体性能未必理想,在1.6 Parallel Old出现之后,吞吐量优先的才有了更好的组合。+Parallel Scavenge + Parallel Old,二者都是多线程版本,关注吞吐量。
5.6 CMS(Concurrent Mark Sweep)
- Concurrent Mark Sweep:并发标记清除,注意了它和前面两个老年代的算法不一样,前两个是标记整理(无碎片),因此CMS会有内存碎片,这是它的缺点之一。不过CMS追求的是更短的GC停顿时间,整体而言STW的时间会更短。
- 初始标记(迅速标记直接管理的对象,STW) -> 并发标记(Tracing,耗时最久,并行) -> 重新标记(修正,STW) -> 并发清除(耗时较久)
- CMS:对CPU敏感,会占用25%的CPU,并且无法处理浮动垃圾(第四阶段产生的垃圾),因此不能等到老年代满了才收集垃圾,默认68的时候收集。如果预留的空间不足则会导致Concurrent Mode Failure,因此需要替补来继续收集垃圾
-XX:CMSInitiatingOccupancyFraction:触发CMS GC的内存使用比例。60%表示当内存使用达到60%触发CMS并发收集,太高的话,可能导致失败继而FullGC,太低则频繁触发GC。
-XX:UseCMSCompactAtFullCollection:这个前面已经提过,用于在每一次CMS收集器清理垃圾后送一次内存整理。
-XX:CMSFullGCsBeforeCompaction:设置在几次CMS垃圾收集后,触发一次内存整理。
- 缺点:占用CPU、有浮动垃圾(第四阶段垃圾)、有碎片(标记清除)
5.7 G1
- 初始标记(STW) -> 并发标记 -> 最终标记(STW) -> 筛选回收(STW)
- 特点:
1.能够充分发挥多核优势,减少STW的时间
2.保留了分代收集的思想,但是新生代和老年代不再物理隔离,而是划分为一个一个区域,G1会建立可预测的停顿时间模型,评估不同区域回收垃圾的价值,
优先回收价值最大的区域,以此来提高垃圾回收的效率
3.整体来看是采用标记整理算法,从局部的2个区域来看是采用复制算法,因此没有碎片
4.可预测的停顿模型:能够指定在N毫秒内垃圾收集时间不超过M毫秒
- G1通过区域的划分是为了避免垃圾回收的时候进行全堆扫描,他会为每一个堆维护一个Remembered Set,在程序对引用数据类型进行写操作的时候,它会产生一个写屏障中断写操作,检查这个引用是不是跨越了多个区域,如果是的话,他会把这个引用关系记录到该区域对应的Remembered Set中,这样在垃圾回收的时候,在枚举根节点的时候就能够从该Set中知道这些引用是该区域所持有的引用,就不需要扫描全部的堆来寻找该区域所持有的引用,一次来避免全堆扫描。
- G1在并发标记阶段程序的引用关系还会发生变化(和CMS类似),G1会把该阶段的变化信息记录到Remember Set Logs中,然后在最终标记节点根据这些Logs修正并合并到Remembered Sets中,在筛选回收的时候,根据不同区域的回收价值成本进行排序,优先回收价值大的区域。
六、内存分配与回收策略
6.1 优先Eden分配
- 一个对象创建的时候优先分配在新生代,如果此时新生代空间不足,则会触发Minor GC,Minor GC比较频繁,并且比较快
6.2 大对象进入老年代
- 大对象直接分配到老年代可以避免在新生代频繁的GC,比如新生代还要一部分空间但是因为大对象过大导致不得不Minor GC来提供空间。
- 可以设置阈值,让大于该大小的对象直接分配到老年代
-XX:PretenureSizeThreshold=128145728(3MB)
该参数只对Serial和ParNew有效,对Parallel Scavenge无效
6.3 老对象进入老年代
- 每一个对象包含一个Age计数器,每次经过一个Minor GC如果对象还存活,则Age增1,到达阈值的时候,对象会被放置到老年代
-XX:MaxTenuringThreshould=10(默认15)
6.4 动态年龄判断
- 如果绝对的按照6.3所描述的年龄来将对象晋升到老年代,那么相对不灵活,有一种情况,如果Survivor中相同年龄的对象占据空间大于Survivor的一半,那么大于或者等着该年龄的对象将全部移到老年代
6.5 空间分配担保
- Minor GC的时候,JVM会检查老年代中连续的空间是否大于新生代中存活的对象大小或者是否大于历次晋升的平均大小,如果大于,则进行Minor GC,如果不大于,则进行一次Full GC。
七、收集器选择?
7.1 默认垃圾收集器
- Client模式下,默认是:Serial+Serial Old (-XX:+UseSerialGC)
- Server模式下,默认是:Parallel Scavenge + Serial Old ,也是JDK1.8的默认值 (Parallel Scavenge Mark Sweep -XX:+UseParallelGC)。
JVM选项 | 使用的垃圾收集器 | 备注 | 图示 |
---|---|---|---|
-XX:UseSerialGC | Serial + Serial Old | client模式下的默认值 | 1 |
-XX:+UseParallelGC | Parallel Scavenge + Serial Old | Server模式下的默认值,JDK8默认 | 2 |
-XX:+UseParNewGC | ParNew + Serial Old | 3 | |
-XX:+UseConcMarkSweepGC | ParNew + CMS + Serial Old | CMS的Concurrence Mode Failure后需要Serial Old完成Full GC,关注响应时间 | 4 |
-XX:+UseParallelOldGC | Parallel Scavenge + Parallel Old | 关注吞吐量 | 5 |
-XX:+UseG1GC | G1 | G1 | G1 |
JVM选项 | 使用的垃圾收集器 | ManagementFactory得到的垃圾收集器名字 | 新生代老年代名称 |
---|---|---|---|
-XX:UseSerialGC | Serial + Serial Old | Copy / MarkSweepCompact | DefNew / Tenured |
-XX:+UseParallelGC | Parallel Scavenge + Serial Old | PS Scavenge / PS MarkSweep | PSYoungGen / ParOldGen |
-XX:+UseParNewGC | ParNew + Serial Old | ParNew / MarkSweepCompact | ParNew / Tenured |
-XX:+UseConcMarkSweepGC | ParNew + CMS + Serial Old | ParNew / ConcurrentMarkSweep | ParNew / CMS |
-XX:+UseParallelOldGC | Parallel Scavenge + Parallel Old | PS Scavenge / PS MarkSweep | PSYoungGen / ParOldGen |
-XX:+UseG1GC | G1 | G1 Young Generation / G1 Old Generation |
- 需要注意的是,在-XX:+UseParallelGC 模式下,UseParallelGC收集器架构中本身有 PS MarkSweep 收集器来进行老年代的收集,其实并不是使用的Serial Old,但是PS MarkSweep和 Serial Old 的实现非常接近,很多官方资料都直接使用Serial Old 代替PS MarkSweep,因此可以直接认为UseParallelGC模式下使用:Parallel Scavenge + Serial Old
7.2 不同场景GC策略选择?
- 默认情况下不需要怎么调优:JDK8就是-XX:+UseParallelGC配置,Parallel Scavenge + Serial Old,老年代是单线程收集。
- 吞吐量优先的场景,比如计算场景而不是即时响应的服务,可以使用 Parallel Scavenge + Parallel Old 吞吐量优先的组合,-XX:+UseParallelOldGC。
不过在1.6之后才提供Parallel Old,由此在1.6之前Parallel Scavenge很尴尬,因为他只能和老年代的单线程Serial Old使用,因此整体上性能未必有优势。
- 如果应用的堆大小在100MB以内。 应用在一个单核单线程的服务器或者堆比较小的情况(100MB),考虑使用SerialGC,比如ParNew + Serial Old。
注意单核服务器尽量不要使用CMS,比较占CPU,尤其是核心比较少的时候。
-
追求快速响应的场景,并且不是CPU密集型,服务器CPU核心多,选择CMS很合适
-
堆内存很大或者长期运行的系统,使用G1(G1不会产生碎片-内存压缩算法)
-
下面是不同收集器的对比
垃圾收集器 | 分代 | 串/并行 | 算法 | 场景 |
---|---|---|---|---|
Serial | 年轻代 | 串 | 复制 | CPU单核+小堆 |
ParNew | 年轻代 | 并 | 复制 | CPU多核+小堆 |
Parallel Scavenge | 年轻代 | 并 | 复制 | 吞吐量优先 |
CMS | 老年代 | 并 | 并发标记清除 | CPU多核+响应时间优先 |
Serial Old | 老年代 | 串 | 标记整理 | CPU单核+小堆 |
Parallel Old | 老年代 | 并 | 标记压缩 | 吞吐量优先 |
G1 | 整堆 | 并 | 标记整理+复制 | 大堆+响应时间优先 |