在做模型项目的时候遇到一个问题,由于模型服务装载一些大模型,大模型对象的大小在 300M 左右,而一台服务器可能装载多个大模型。在服务启动和模型更新的时候会遇到 young gc 耗时过长的问题,young gc 所采用的垃圾回收器是 ParNew。通过观察 GC 日志可以发现,模型对象一开始是存在于年轻代的,当经过 15次 gc 后,这些对象就会进入到老年代,而之后 young gc 的时间缩短到正常可以接受的时间范围 0.01s ~ 0.02s。而在模型对象尚未进入老年代时,young gc 耗时就会超过 0.3 s,导致线上请求模型会超时。这个原因主要是年轻代回收的过程中,标记过程其实耗时并不长,而长的是 survivor 区复制对象造成的耗时。
既然定位到问题,那么就需要解决问题,解决方案就是让这种大模型立马进入老年代(由于在模型加载的时候通过控制调用方的路由表保证了服务器不对外提供服务,因此在这个阶段可以让模型对象进入老年代后再提供服务)。一种方法是调整最大晋升代的阈值,如果调整过小,担心 old 区增长过快,而导致线上服务 old gc 不可控(因为每天会在非高峰期主动触发 Old GC, System.gc()),于是采用第二种方式,生产对象主动触发 Young GC,把模型对象挤入老年代。当然还有一种方法,不停的 System.gc(),这个过程相对耗时较长,而且线上服务一般会对 full gc 次数做监控,为了避免报警,所以最终没有选择这种简单粗暴的方式。
实现第二个方案需要得知 Eden 区 大小,以及最大晋升代数,例如 Eden 区有 2G,最大代数有 10代,那么就可以通过生产 20G 的对象,把模型挤入老年代,因此需要获得这两个参数。通过观察 gc 日志,以及命令都可以得知这些参数,而在运行期程序内部获得这些参数一直没有实现过。如果这些参数作为 properties,是可以通过 System.getProperties 这种方式来获取参数的。但是,如果不作为系统属性的话,该如何获取呢。这就要通过 ManagementFactory 了。
先上代码:
public class GCUtil {
/**
* 调用 System.gc
*/
public static void systemGC() {
long startTime = System.currentTimeMillis();
LOGGER.info("Call for system gc start...");
System.gc();
LOGGER.info("Call for system gc end, spend {}ms", System.currentTimeMillis() - startTime);
}
/**
* 保证当前年轻代进入老年代的 gc,用于模型更新以及刚上线期间
*/
public static void youngPromoteGC() {
try {
long startTime = System.currentTimeMillis();
LOGGER.info("Call for young promote gc start...");
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
List<String> args = runtimeMXBean.getInputArguments();
// 获取 eden 区大小
long edenMSize = 0;
for (String arg : args) {
LOGGER.info("--------------- Jvm param: {}", arg);
if (arg.startsWith(JVM_XMN)) {
String edenSizeStr = arg.substring(JVM_XMN.length());
if (edenSizeStr.toLowerCase().endsWith("g")) {
long edenGSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1));
edenMSize = edenGSize * 1024;
} else if (edenSizeStr.toLowerCase().endsWith("m")) {
edenMSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1));
} else {
LOGGER.warn("Cannot recognize -Xmn argument: " + JVM_XMN);
}
}
}
if (edenMSize == 0) {
edenMSize = getEdenMemorySize();
}
if (edenMSize <= 0) {
LOGGER.warn("Extract jvm -Xmn failed");
return;
}
// 获取 晋升老年代最大次数
int tenuringThreshold = DEFAULT_TENURING_THRESHOLD;
for (String arg : args) {
if (arg.startsWith(MAX_TENURING_THRESHOLD)) {
String tenuringThresholdStr = arg.substring(MAX_TENURING_THRESHOLD.length() + 1);
tenuringThreshold = Integer.valueOf(tenuringThresholdStr);
if (tenuringThreshold > DEFAULT_TENURING_THRESHOLD) {
tenuringThreshold = DEFAULT_TENURING_THRESHOLD;
}
}
}
LOGGER.info("Start to young gc, -Xmn={}m, -XX:MaxTenuringThreshold={}", edenMSize, tenuringThreshold);
// 手动触发 Young GC
for (int i = 0; i < edenMSize * tenuringThreshold; ++i) {
allocate_1M();
}
// System GC,清理老年代
System.gc();
LOGGER.info("Call for young promote gc end, spend {}ms", System.currentTimeMillis() - startTime);
} catch (Exception e) {
LOGGER.error("Trigger young promote gc failed: ", e);
}
}
/**
* 获取新生代大小,单位 M
*/
private static long getEdenMemorySize() {
List<MemoryPoolMXBean> poolMXBeanList = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean memoryPoolMXBean : poolMXBeanList) {
if (memoryPoolMXBean.getName().toLowerCase().contains("eden")) {
long maxUsage = memoryPoolMXBean.getUsage().getMax();
return maxUsage >> 20;
}
}
return -1;
}
/**
* 生成个 1M 对象
*/
private static void allocate_1M() {
byte[] _1M = new byte[1024 * 1024];
}
private GCUtil() {
}
private static final String MAX_TENURING_THRESHOLD = "-XX:MaxTenuringThreshold";
private static final int DEFAULT_TENURING_THRESHOLD = 15;
private static final String JVM_XMN = "-Xmn";
private static final Logger LOGGER = LoggerFactory.getLogger(GCUtil.class);
}
整体思路就是 ManagementFactory.getRuntimeMXBean().getInputArguments() 获得 List<String> 所有 JVM 参数。
另一种方法就是通过 getMemoryPoolMXBeans() 获取所有memoryPoolMXBean,然后找到 Eden 区参数,解析它的设置。而在年代方面,只能通过 RuntimeMXBean.getInputArguments 获取,如果获取不到,那么就采用默认值 15,这样就能够手动触发 young gc 了。这是一种粗略的方式,并且能够保证一定能够达到效果(触发 young gc 次数不会少于自己想要触发的次数),在触发后再次通过 System.gc() 触发 full GC,来清理老年代不需要的对象,进而保证在开始提供服务时是一个干净的环境,模型都存在于老年代中,不会参与 young gc。
接下来,思考一下,希望以后能够在问题出现之前避免这种问题,而不是事后解决这个问题,所以在功能实现的时候需要清楚自己的对象是否会给 gc 带来麻烦,老的 JVM 并没有那么智能,有时候需要人为提供策略来协助它解决一些问题。