Epsilon:JDK 11中的无为GC

546 阅读3分钟

这篇文章主要介绍JDK11中一个无为式的垃圾回收器,讨论一下不做垃圾回收的优势。本文翻译自Andrew Binstock发布在Oracle期刊的文章

对JDK进行性能调优是一门精细的艺术,往往需要选择合适的垃圾收集器,同时还要调整其配置,以期在满足给定负载要求的前提下对程序造成最小影响。性能调优中一个长期存在的问题就是,确认没有垃圾收集影响的情况下服务性能有多快。为了满足这个需求(还有一些会在文章后面谈到),Java的研发团队在JDK 11中引入了Epsilon垃圾收集器(GC)。这种垃圾收集器,在JEP 318中定义了详细的规范,它只会分配内存但是不会对其回收处理,也就是说,不会进行垃圾回收。

如果要在JVM中使用Epsilon GC,只需要在命令行中指定两个运行时参数:

-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC

分别在启用和关闭Epsilon GC的情况下,执行以下代码,可以明显看出Epsilon的不同之处:

public class EpsilonDemo{
	public static void main(String[] args){
        final int GIGABYTE = 1024*1024*1024;
        final int ITERATIONS = 100;
        
        System.out.println("Starting allocations...");
        
        // 每次分配1G内存
        for(int i = 0; i < ITERATIONS; i++){
            var array = new byte[GIGABYTE];
        }
        
        System.out.println("Completed successfully...");
    }
}

这段代码的目的很简单,尝试分配100 GB内存,然后退出。如果不启用Epsilon GC的话,这段代码的运行结果中会打印首尾的println语句,也就是说默认的GC分配了100个1GB的内存空间,并且为了适应系统的可用内存,又对这些内存空间进行了垃圾回收。

但是,如果按照前面所说指定命令行参数的话,程序运行结果如下:

Starting allocations...
Terminating due to java.lang.OutOfMemoryError: Java heap space

因为前面分配的内存空间不会被Epsilon GC回收,堆内存会耗尽所有可用的存储空间,最终出现OOM错误。

运行这部分代码的时候,需要JDK 11或更新的版本,同时可用堆空间要小于100GB。如果你在配置更高的系统中运行的话,可以通过调整ITERATIONS参数的大小来复现该错误。

除了用于性能调优场景之外,在线上服务中使用Epsilon也有其强大的优势。一般来说,Java团队是不建议这样做的,但是有两个场景例外。短期运行程序,像所有程序一样会在运行结束的时候运行垃圾收集器。但是根据JEP 318中的解释,“使用垃圾回收周期清除堆内存是在浪费时间,因为程序在退出的时候系统肯定会释放堆内存”。

同时,Java团队也预见到,在一些对程序延迟非常敏感的场景下,也是可以使用Epsilon的,引用JEP中的描述:

至于那些对延迟极度敏感的应用程序,开发人员往往会自觉地处理内存分配,并且准确知道程序的内存占用情况,甚至会有(几乎)完全无垃圾的应用程序,此时再采取GC周期管理内存,可能就是一个设计层面的问题了。

但是在其它场景中,都不建议使用Epsilon回收器。即便有些明知只会占用很少内存的程序,如果使用Epsilon回收器在某些存在内存限制的系统中运行,这类程序也有可能导致崩溃。因此,Epsilon回收器需要通过命令行参数来启用,也是为了告知大家这是一个实验性的功能。技术上来讲,它是JDK 11及后续版本的组成部分,但是仍然要使用“实验性”一词来作为提醒。

说回性能调优,如果你想知道垃圾回收器对你的代码性能影响有多大,使用Epsilon也是一种解决方案。