03垃圾回收篇(D3_垃圾收集器的选择及相关参数)

目录

学习前言

一、收集器的选择

二、GC日志参数

三、垃圾收集相关的常用参数

四、内存分配与回收策略

[1. 对象优先在Eden分配](#1. 对象优先在Eden分配)

[2. 大对象直接进入老年代](#2. 大对象直接进入老年代)

[3. 长期存活的对象将进入老年代](#3. 长期存活的对象将进入老年代)

[4. 动态对象年龄判定](#4. 动态对象年龄判定)

[5. 空间分配担保](#5. 空间分配担保)


学习前言

本章主要学习垃圾收集器的选择及相关参数

在实际生产环境上应该如何选择垃圾收集器?相关参数如何设置?

这里只考虑OpenJDK/OracleJDK的HotSpot虚拟机上的垃圾收集器。

没有将Azul和OpenJ9的垃圾收集器加入选择范围,因为不了解。

一、收集器的选择

考虑垃圾收集器的选择时,需要考虑以下事项:

  1. 对于不可能三角(内存占用/吞吐量/延迟),应用的主要功能更关注什么?比如一个OLAP系统,它的大部分功能是各种数据分析和运算,目标是尽快完成计算任务给出结果,那么吞吐量就是主要关注点。而一个OLTP系统,它需要与用户频繁交互,用户频繁地通过页面操作录入数据,那么延迟就是主要关注点。而如果是一个客户端应用或者嵌入式应用,那么内存占用就是主要关注点。
  2. 应用运行的基础设施如何?包括但不限于CPU核数,内存大小?操作系统是Linux还是Windows等等。
  3. 使用的JDK是什么版本?9之前还是9以后?

简单列一个表格,可以参考该表格选择垃圾收集器:

|-------------------------|------|-----------|---------|-------------|--------------|--------------------------------------|
| 垃圾收集器 | 关注点 | 硬件资源 | 操作系统 | JDK版本 | 优势 | 劣势 |
| ZGC | 低延迟 | 大内存,很多核 | 64位操作系统 | JDK15以上 | 延迟真低 | 需要足够的JVM调试能力;不分代,连续高湾大量内存分配时只能增加堆内存。 |
| Shenandoah | 低延迟 | 大内存,很多核 | - | openJDK15以上 | 延迟很低 | 需要足够的JNM调试能力;没有ZGC延迅低。 |
| G1 | 低延迟 | 4G以上内存,多核 | - | JDK9以上 | 延迟相对较低,技术成熟 | 内存占用相对其他经典GC较多,延迟低延迟GC受高: |
| ParNew*CMS | 低延迟 | 4G以下内存,多核 | - | JDK9之前 | 延迟相对较低,技术成熟 | 已经被抛弃,更大的堆内存比不上G1 |
| Parallel(Scavenge*Old) | 吞吐量 | - | - | - | 吞吐量高 | 延迟较高 |
| Serial*Serial Old | 内存占用 | - | - | - | 内存占用低,CPU负荷低 | 延迟高,香吐低 |

简单总结一下就是,主流的,就做一个普通的JavaWeb,或者微服务中的后台服务,

与用户交互较多,那就根据Java版本来,不是 G1 就是 ParNew+CMS:

  • Java版本在9及9以上,那就无脑选择G1,默认即可;
  • Java版本还是8甚至更老,那就ParNew+CMS;

有点追求的,根据情况来:

  • 有足够的JVM调优能力,且对低延迟有追求,可以尝试ZGC或Shenandoah;
  • 主要做后台单纯数据运算的,比如OLAP,可以尝试Parallel(Scavenge+Old);
  • C/S架构的客户端应用,或者嵌入式应用,可以尝试 Serial+Serail Old。

二、GC日志参数

Java9之前,HotSpot没有统一日志框架,参数比较混乱;Java9之后,所有日志功能都归纳到了 -

Xlog 参数上。

Java9的JVM日志使用方式

复制代码
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

说明:

  • selector:选择器,由标签tag和日志级别level组成。标签是功能模块,它指定输出哪个功能模块的日志,GC日志的话,标签就是gc;日志级别从低到高有:Trace、Debug、Info、Warning、Error、Off。
  • decorators:修饰器,指定每行日志的额外输出内容,包括:time,当前时间戳;uptime,VM启动到现在的秒数;timemillis,当前时间毫秒数;uptimemillis,VM启动到现在的毫秒数;timenanos,当前时间纳秒数;uptimenanos,VM启动到现在的纳秒数;pid,进程ID;tid,线程ID;level,日志级别;tags,日志标签。默认是uptime、level和tags。

参考:Java9前后GC相关日志参数对比

|-----------------|-----------------------|-----------------------------------------------------------------------------|
| 功能 | Java9及之后 | Java9之前 |
| 查看GC基本信息 | -Xlog;gc | -XX:+PrintGC |
| 查看GC详细信息 | -X-log:gc* | -XX:+PrintGCDetails |
| 查看GC前后空间变化 | -Xlog:gc+heap=debug | -XX:+PrintHeapAtGC |
| 查看GC中并发时间和停顿时 间 | -Xlog:safepoint | -XX:+Print-GCApplicationConcurrentTime 和 -XX:+PrintGcApplicationstoppedTime |
| 查看GC自适应调节信息 | -Xlog:gc+ergo*=trace | -XX:+PrintAdaptive-SizePolicy |
| 查看GC后剩余对象年龄分布 | -Xlog:gc+age=trace | -XX:+PrintTenuring-Distribution |

还有一个 -verbose:gc 参数,功能与 -Xlog:gc 以及 -XX:+PrintGC 一样。

Java8的 GC 日志的查看,请参考以前的文章:

java8添加并查看GC日志(ParNew+CMS)

三、垃圾收集相关的常用参数

四、内存分配与回收策略

Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及

自动回收分配给对象的内存。

1. 对象优先在Eden分配

大多数情况下,对象在新生代·Eden区中分配。

当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。

HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行

为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

2. 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元

素数量很庞大的数组,例如:创建一个典型的大对象byte[]数组。

在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易 导致内存明明还有不少空间时就

提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高

额的内存复制开销。

HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代

分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操

作。

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他

新生代收集器,如Parallel Scavenge并不支持这个参数,如果必须使用此参数进行调优,可考虑

ParNew加CMS的收集器组合。

3. 长期存活的对象将进入老年代

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存

活对象应当放在新生代,哪些存活对象放在老年代中,为做到这点,虚拟机给每个对象定义了一个

对象年龄(Age)计数器,存储在对象头中。

对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,

该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁,对象在Survivor区中每熬过一次

Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代

中。

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

4. 动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到XX:

MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于

Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:

MaxTenuringThreshold中要求的年龄。

5. 空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总

空间,如果这个条件成立,那这一次Minor GC可以确保是安全的,如果不成立,则虚拟机会先查

看XX:

HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允

许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,

如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:

HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

"冒险"是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中

一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况,最极端的

情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无 法容纳的

对象直接送入老年代,这与生活中贷款担保类似。

老年代要进行这样的担保,前提是老年代 本身还有容纳这些对象的剩余空间,但一共有多少对象

会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的,所以只能取之前每一次回收

晋升到老年代对象容量的平均大小作为经验值,与 老年代的剩余空间进行比较,决定是否进行Full

GC来让老年代腾出更多空间。

取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对

象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实

实 地重新发起一次Full GC,这样停顿时间就很长了。

虽然担保失败时绕的圈子是最大的,但通常情况下 都还是会将-XX:HandlePromotionFailure开关

打开,避免Full GC过于频繁。

相关推荐
书中自有妍如玉几秒前
.net 使用MQTT订阅消息
java·前端·.net
风铃儿~26 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组32 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿42 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA1 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng0061 小时前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
Su米苏2 小时前
Axios请求超时重发机制
java
本郡主是喵3 小时前
并发编程 - go版
java·服务器·开发语言