许多技术人员只把JVM当成黑盒,要想改善Java应用的性能和扩展性无疑是一项艰巨的任务。若要提高Java性能调优的能力,就必须对现代JVM有一定的认知。
HotSpot VM是JDK 1.3版本之后默认的虚拟机,目前是使用最广泛的Java虚拟机。本文主要介绍HotSpot VM(即HotSpot Java虚拟机)的架构。HotSpot VM有三个主要组件:VM运行时(Runtime)、JIT编译器(JIT Compiler)以及内存管理器(Memory Manager)。
HotSpot VM的架构概述
HotSpot VM架构极富特点且功能强大,可以满足高性能和高可扩展性需求。HotSpot VM基本架构如下图所示:
上图中,JIT编译器和垃圾收集器都是可插拔的。HotSpot VM运行时系统为JIT编译器和垃圾收集器提供服务和通用API。此外,它还为VM提供启动、线程管理、JNI(Java本地接口)等基本功能。
早期的HotSpot VM是32位JVM,内存地址空间限制为4G。且实际Java 堆的大小还进一步受限于底层操作系统。对Linux来说,HotSpot VM的最大可用Java堆大约为2.5G到3G。实际消耗的最大内存地址空间随给定的Java应用和JVM版本而有所不同。
随着操作系统的内存不断增大,64位HotSpot VM应运而生。它增大了Java堆,使得这些系统可以使用更多内存。此外,64位JVM可以使用更多的CPU寄存器,这也有助于改善系统的性能。更多的CPU寄存器可以避免寄存器卸载(Register Spilling)。当活跃状态数超过CPU寄存器数,多出的活跃状态只能存放在内存中,就会发生寄存器卸载。寄存器卸载时,某些活跃状态必须从CPU寄存器"卸载"到内存中。因此避免寄存器卸载可以让程序执行得更快。
HotSpot VM运行时
HotSpot VM各组件中,垃圾收集器和JIT编译器最受关注,而VM运行时环境则通常被忽略,虽然它提供的恰恰是HotSpot VM的核心功能。HotSpot VM运行时环境担当许多职责,包括命令行选项解析、VM生命周期管理、类加载、字节码解释、异常处理、同步、线程管理、Java本地接口、VM致命错误处理和C++(非Java)堆管理。
命令行选项
HotSpot VM运行时系统解析命令行选项,并据此配置HotSpot VM。其中一些选项供HotSpot VM启动器使用,如指定选择哪个JIT编译器、选择何种垃圾收集器等,还有一些供启动后的HotSpot VM使用,如指定Java堆的大小。
命令行选项主要有三类:标准选项(Standard Option)、非标准选项(Nonstandard Option)和非稳定选项(Developer Option)。标准选项是JVM Specification要求所有Java虚拟机都必须实现的选项,它们在发行版本之间保持稳定,但也有可能在后续的发行版本中被废除。非标准选项(以-X为前缀)不保证也不强制所有JVM实现都必须支持,它可能未经通知就在Java SDK发行版本之间发生改变。非稳定选项(以-XX为前缀)通常是为了特定需要而对JVM的运行进行校正,并且可能需要有系统配置参数的访问权限。和非标准选项一样,非稳定选项也可能不经通知就会在发行版本之间发生变动。
VM生命周期
HotSpot VM运行时系统负责启动和停止HotSpot VM。启动HotSpot VM的组件是启动器。HotSpot VM有若干个启动器。Linux上最常用的是java可执行程序。也可通过JNI接口JNI_CreateJavaVM启动内嵌的JVM。还有就是网络启动器javaws(Java Web Start),Web浏览器用它来启动Applet。
启动器启动HotSpot VM时会执行一系列操作,步骤概述如下:
(1) 解析命令行选项。启动器会直接处理一些命令行选项,如-client或-server,它们决定加载哪个JIT编译器,其他参数则传给HotSpot VM。
(2) 设置堆的大小和JIT编译器。如果命令行没有明确设置堆的大小和JIT编译器,启动器会通过自动优化进行设置。自动优化的默认配置因底层系统配置和操作系统而有所不同。
(3) 设置环境变量,如CLASSPATH,等。
(4) 如果命令行有-jar选项,启动器则从指定JAR的manifest中查找Main-Class,否则从命令行读取Main-Class。
(5) 使用标准Java本地接口(Java Native Interface, JNI)方法JNI_CreateJavaVM在新创建的线程中创建HotSpot VM。
(6) 一旦创建并初始化好HotSpot VM,就会加载Java Main-Class,启动器也会从Java Main-Class中得到Java main方法的参数。
(7) HotSpot VM通过JNI方法CallStaticVoidMethod调用Java main方法,并将命令行选项传给它。
至此,HotSpot VM开始正式执行命令行指定的Java程序了。
一旦Java程序或Java Main方法执行结束,HotSpot VM就必须检查和清理所有程序或者方法执行过程中生成的未处理异常。此外,方法的退出状态和程序的退出状态也必须返回它们的调用者。调用Java本地接口方法DetachCurrentThread将Java main方法与HotSpot VM脱离。
类加载
HotSpot VM和Java SE类加载库共同负责类加载。HotSpot VM负责解析常量池符号,这个过程需要加载、链接,然后初始化Java类和Java接口。类加载用以描述类名或接口名映射到类对象的整个过程。JVM Specification明确定义了类加载的三个阶段:加载、链接和初始化。类加载的最佳时机是在解析Java字节码类文件中常量池符号的时候。Java API如Class.forName()、ClassLoader.loadClass()、反射API和JNI_FindClass都可以引发类加载。HotSpot VM自身也会触发类加载。HotSpot VM启动时,除了加载普通类,也会加载诸如java.lang.Object和java.lang.Thread这些核心类。加载类时,需要加载它的所有Java超类和所有Java超接口。此外,作为链接阶段的一部分,类文件验证也需要加载一些其他类。总结来说,加载阶段是HotSpot VM和特定类加载器如java.lang.ClassLoader之间相互协作的过程。
1.类加载阶段
对于给定的Java类或接口,类加载时会根据它的名字找到对应的二进制文件,定义Java类,然后创建代表这个类或接口的java.lang.Class对象。如果没有找到合适的Java类或接口的二进制文件,就会抛出NoClassDefFound。此外,类加载阶段会对类的格式进行语法检查,如果出错,则会抛出ClassFormatError或UnsupportedClassVersionError。Java类加载前,HotSpot VM必须先加载它的所有超类和超接口。
链接的第一步是验证,检查类文件的语义、常量池符号以及类型。如果检查出错,就会抛出VerifyError。链接的下一步是准备,它会创建静态字段,初始化为标准默认值,以及分配方法表。请注意,此时还没有执行任何Java代码。接下来解析符号引用,这一步是可选的。然后初始化类,运行类构造器。这是目前,类中运行的第一段Java代码。值得注意的是,初始化类需要先初始化超类(不会初始化超接口)。
处于性能优化的考虑,通常直到类初始化时HotSpot VM才会加载和链接类。这意味着,类A引用类B,加载A并不一定导致加载B。执行B的第一条指令会导致初始化B,从而加载和链接B。
2.类加载器委派
当请求类加载器查找和加载某个类时,该类加载器可以转而请求别的类加载器来加载。这被称为类加载器委派。类的首个类加载器称为初始化类加载器(Initiating Class Loader),最终定义类的类加载器称为类加载器(Defining Class Loader)。就字节码解析来说,某个类的初始类加载器是指对该类进行常量池符号解析的类加载器。
类加载器之间是层级化关系,每个类加载器都可以委派给上一级类加载器。这种委派关系定义了二进制类的查找顺序。Java SE类加载器的层次查找顺序为启动类加载器、扩展类加载器以及系统类加载器。系统类加载器是默认的应用程序类加载器,它加载Java类的main方法并从classpath上加载类。应用程序类加载器可以是Java SE系统自带的类加载器,或者由应用程序开发人员提供。扩展类加载器则由Java SE系统实现,它负责从JRE的lib/ext目录下加载类。
3.启动类加载器
启动类加载器由HotSpot VM实现,负责加载BootClassPath路径中的类,如包含Java SE类库的rt.jar。为了加速启动速度,Client模式的HotSpot VM可以通过类数据共享的特性使用已经预加载的类。这个特性默认是开启的,可由-Xshare:off关闭。注意,Server模式是否支持该特性还需进一步确认。
4.类型安全
Java类或接口的名字为全限定名(包括包名)。Java的类型由全限定名和类加载器唯一确定。换言之,类加载器定义了命名空间,这意味着两个不同的类加载器加载的类,即便全限定名完全一样,仍是两个不同的类型。当类A调用B.someMethodName()时,HotSpot VM会追踪并检查类加载器约束,从而确保A和B的累加载器所看到的someMethodName()方法签名是一致的。
5.HotSpot类元数据
类加载时,HotSpot VM会在永久代创建类的内部表示instanceKlass或arrayKlass。instanceKlass引用了与之对应的java.lang.Class实例。HotSpot VM内部使用称为KlassOop的数据结构访问instanceKlass。后缀"Oop"表示普通对象指针,所以KlassOop是引用java.lang.Class的HotSpot内部抽象,它是指向Klass(Java类对应的内部表示)的普通对象指针。
6.内部的类加载数据
类加载过程中,HotSpot VM维护了三张散列表。SystemDictionary包含已加载的类,它将建立类名/类加载器(包括初始类加载器和定义类加载器)与KlassOop对象之间的映射。PlaceholderTable包含当前正在加载的类,它用于检查ClassCircularityError,多线程类加载器并行加载类时也会用到它。LoaderConstraintTable用于追踪类型安全检查的约束条件。这些散列表都需要加锁以保证访问安全。
字节码验证
Java虚拟机无法确保字节码是由可信的javac编译器产生,所以在链接时必须进行字节码验证以保证类型安全。
目前有两种判断指令操作类型和个数的字节码分析方法。常用的方法称为类型推导(Type Inference),它对每个字节码进行抽象解释并在目标分治或者异常处理器上合并类型状态。它对字节码进行迭代分析指导发现稳定的类型。如果没有发现稳定的类型,或者结果类型与某些字节码约束冲突,则会抛出VerifyError。
第二种验证方法是类型检查(Type Verification)。Java编译器将每个目标分支或异常分支中的类型信息设置在code属性的StackMapTable中。StackMapTable包含若干个栈映射帧,每个栈映射帧都会用字节码偏移量表示表达式栈和局部变量表中元素的类型状态。Java虚拟机验证字节码时只需要扫描一次,就可验证类型的正确性。对于字节码验证,这个方法比常用的类型推导来得快,也更为轻巧。
类数据共享
类数据共享是Java 5引入的特性,可以缩短Java应用的启动时间,同时也能减少其占用的内存空间。使用Java HostSpot JRE安装程序安装Java运行环境时,安装程序会加载系统jar中的部分类,变成私有的内部表示并转储成文件,称为共享文档。之后调用Java虚拟机时,共享文档会映射到JVM内存中,从而减少加载这些类的开销,也使得这些类的大部分JVM元数据能在多个JVM进程间共享。
类数据共享可以从两个方面减少新建JVM实例的内存开销。首先,共享文档中的一部分以只读方式映射到内存,并在多个JVM进程间共享,而以前这些数据需要在各个JVM实例中复制一份。其次,HotSpot VM可以直接使用共享文档中的类数据,不必再从Java SE核心库的jar中获取原始类信息,节约的内存可以让更多的程序在同一台机器上运行。
HotSpot VM的类数据共享在永久代中引入了新的Java子空间,用以包含共享数据。HotSpot VM启动时,共享文档classes.jsa作为内存映射被加载到永久代。随后HotSpot VM的内存管理子系统接管该共享区域。
解释器
HotSpot VM解释器是一种基于模板的解释器。JVM启动时,HotSpot VM运行时系统利用内部TemplateTable中的信息在内存中生成解释器。TemplateTable包含于每个字节码对应的机器代码,每个模板描述一个字节码。
解释器使得HotSpot VM运行时系统能够执行复杂的操作,特别是那些本来用汇编语言处理起来很复杂的操作,如常量池的查找。对所有程序来说,大量时间主要花费在一小部分代码的执行上。HotSpot VM没有逐个方法进行“即使”或“提前”编译,而是直接用解释器运行程序,并在运行时分析代码并监控程序中的热点(Hot Spot)代码。然后用全局机器代码优化器(Global Machine Code Optimizer)集中优化这些热点。
异常处理
当与Java的语义约束冲突时,Java虚拟机会用异常通知程序。如试图获取数组范围之外的元素就会引发异常。异常处理由HotSpot VM解释器、JIT编译器和其他HotSpot VM组件一起协作实现。异常处理主要有两种情形,同一方法中抛出和捕获异常,或由调用方法捕获异常。后一种情况更为复杂,需要退栈才能找到合适的异常处理器。异常可以有抛出字节码、VM内部调用返回、JNI调用返回或Java调用返回所引发。当VM遇到抛出的异常时,就会调用HotSpot VM运行时系统查找该异常最近的处理器。
同步
同步是一种并发操作机制,用来预防、避免对资源不正当的交替使用(也即竞争),保障交替使用资源的安全。Java用称为线程的结构来实现并发。互斥是同步的特殊情况,即同一时间最多只允许一个线程访问受保护的代码或数据。HotSpot VM用monitor对象来保障线程运行代码之间的互斥。只有获得monitor对象的所有权后,线程才可以进入它所保护的临界区。Java中临界区由同步块表示,代码中用synchronized语句表示。
HotSpot VM吸收了非竞争和竞争性同步操作的最先进的技术,极大地提高了同步性能。多数同步操作为非竞争性同步,可以在常量时间内实现。Java 5 HotSpot VM引入了偏向锁(命令行选项–XX:+UseBiasedLocking),最好情况下成本甚至为零。大多数对象在其生命周期中最多只会被一个线程锁定,就可以通过–XX:+UseBiasedLocking允许线程使用偏向锁。一旦开启偏向锁,这类线程不需要借助昂贵的原子指令就可以对该对象进行锁定和解锁操作。
线程管理
线程管理涉及从线程创建到终止的整个生命周期,以及HotSpot VM线程间的协调。线程管理包括Java代码创建的线程、直接与HotSpot VM关联的本地线程,以及HotSpot为其他目的而创建的内部线程。虽然线程管理的多数内容独立于平台,但实现细节依然依赖于底层的操作系统。
1.线程模型
HotSpot VM的线程模型中,Java线程(java.lang.Thread实例)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程,当该Java线程终止时,这个操作系统线程也会被回收。Java线程的优先级和操作系统线程的优先级之间关系复杂,各个系统之间不尽相同。
2.线程创建和销毁
HotSpot VM有两种引入线程的方式,执行Java代码时调用java.lang.Thread对象的start()方法,或者用JNI将已存在本地线程关联到HotSpot VM上。
当java.lang.Thread启动时,HotSpot VM创建与之相关联的JavaThread和OSThread对象,最后是本地线程。所有的HotSpot VM状态(如线程本地存储和分配缓存、同步对象等)准备好后,启动本地线程。本地线程初始化后开始执行启动方法,执行java.lang.Thread对象的run()方法,当它返回时,先处理所有未捕获的异常,之后终止该线程,然后与HotSpot VM交互,检查终止该线程是否就要终止整个HotSpot VM。终止线程会释放所有已分配的资源,并从已知线程列表中移除JavaThread,然后调用OSThread和JavaThread的析构函数,当它的初始启动方法完成后,最终停止运行。
HotSpot VM使用JNI的AttachCurrentThread与本地线程关联,并创建与之关联的OSThread和JavaThread实例,然后执行基本的初始化。接下来,必须为关联的线程创建java.lang.Thread对象。依旧线程关联时的参数,反射调用Thread类构造函数的Java代码,从而创建该对象。一旦关联,线程就可以通过其他JNI方法调用任何它所需要的Java代码。最后,当本地线程不再需要关联HotSpot VM时,它可以调用JNI的DetachCurrentThread方法将它与HotSpot VM解除关联,释放资源,去除对java.lang.Thread实例的引用,销毁JavaThread和OSThread对象,等等。
3.线程状态
HotSpot VM使用多种不同的内部线程状态来表示线程正在做什么。这有助于协调线程间的交互和出错时提供有用的调试信息。执行不同操作时,线程状态会发生迁移,迁移时会检查线程在该点处理请求的动作是否合适。从HotSpot VM的角度看,主线程可以有以下状态:
(1) 新线程:线程正在初始化的过程中。
(2) 线程在Java中:线程正在执行Java代码。
(3) 线程在VM中:线程正在HotSpot VM中执行。
(4) 线程阻塞:线程因某种原因(获取锁、等待条件满足、休眠和执行阻塞式I/O操作等)而被阻塞。
4.VM内部线程
简单的Java "Hello World"程序的执行会导致HotSpot VM创建大量的线程。这些线程由HotSpot VM内部线程和HotSpot VM库线程所产生,如引用处理器、finilizer线程。HotSpot VM内部线程如下所示:
(1) VM线程:是C++单例对象,负责执行VM操作。
(2) 周期任务线程:是C++单例对象,也称Watcher Thread,模拟计时器中断使得在HotSpot VM内可以执行周期性操作。
(3) 垃圾收集线程:这些线程有不同类型,支持串行、并行和并发垃圾收集。
(4) JIT编译器线程:这些线程进行运行时编译,将字节码编译成机器码。
(5) 信号分发线程:这个线程等待进程发来的信号并将它们分发给Java的信号处理方法。
上述所有的线程都是HotSpot内部C++线程类的实例,执行Java代码的所有线程都是HotSpot C++内部Java Thread的实例。
5.VM操作和安全点
HotSpot VM内部的VMThread监控称之为VMOperationQueue的C++对象,等待该对象中出现VM操作,然后执行这些操作。因为这些操作通常需要HotSpot VM打到安全点后才能执行,所以它们会直接传递给VMThread。简单来说,当HotSpot VM叨叨安全点时,所有的Java执行线程都会被阻塞,在安全点时,任何执行本地代码的线程都不能返回Java代码。这意味着HotSpot VM操作可以执行的前提是,没有线程正在修改其Java栈,线程的Java栈也没有被更改,以及所有线程的Java栈都能被检测。
C++堆管理
除了HotSpot VM内存管理器和垃圾收集器所维护的Java堆以外,HotSpot VM还用C/C++堆存储HotSpot VM的内部对象和数据。从基类Arena衍生出来的一组C++类负责管理HotSpot VM C++堆的操作,这些类只供HotSpot VM使用,并不会暴露给HotSpot VM的使用者。Arena及其子类是常规C/C++内存管理函数malloc/free之上的一层,可以进行快速C/C++内存分配。使用Arena分配内存而不是直接使用C/C++内存管理函数malloc/free是为了更好的性能。由于C/C++内存管理函数malloc/free可能需要获取全局OS锁,这会影响扩展性并对性能有影响。
Arena是线程本地对象,会预留一定量的内存,这使得fast-path分配不需要全局共享锁。与此类似,当Arena的free操作将内存释放会Chunk时,也不需要通常释放内存时所用的锁。
Java本地接口
Java本地接口(Java Native Interface, JNI)是本地编程接口,它允许在Java虚拟机中运行的Java代码中利用其他语言(如C、C++和汇编语言)编写的程序和库进行写作。当应用不能完全用Java编写时,编程人员可以使用JNI编写本地方法处理这种情况。
JNI可以用来创建、检测及更新Java对象、调用Java方法、捕获并抛出异常、加载类并获取类信息以及执行运行时类型检查。JNI可以和Invocation API一起使用,以便任意本地应用都可以内嵌Java VM。这使得编程人员可以很容易将已有的应用变成可以使用Java的应用,还不需链接VM源代码。
注意,一旦在应用中使用JNI,就意味着丧失了Java平台的两个好处。第一,依赖JNI的Java应用难以在多种异构的硬件平台上运行。即便应用中Java语言编写的部分可以移植到多种硬件平台,采用本地编程语言的部分也需要重新编译。也就是说,一旦使用JNI就失去了Java的特性,即"一次编写,到处运行"。第二,Java是强类型和安全的语言,C或C++则不是。因此,Java开发者用JNI编写应用时必须格外小心。
VM致命错误处理
HotSpot VM的设计者认为有一点非常重要,即能为它的用户和开发者提供足够多的信息,用以诊断和修复VM致命错误。OutOfMemoryError是常见的VM致命错误。当HotSpot VM因致命错误而崩溃时,会生成HotSpot错误日志文件,名为hs_err_pid<pid>.log,这里<pid>是崩溃HotSpot VM进程的id。
另一种常用于诊断VM致命错误根源的做法是,添加HotSpot VM命令行选项:-XX:OnError = cmd1 args…;cmd2…。当HotSpot VM崩溃时,就会执行这个HotSpot VM命令行选项传递给它的命令列表。
当HotSpot VM遇到致命错误时,内部使用VMError类收集信息并导出成hs_err_pid<pid>.log。当遇到不可识别的信号或异常时,特定的操作代码就会调用VMError类。需要仔细编写HotSpot VM致命错误的处理程序,避免自身错误(如StackOverflow)或持有关键锁时发生的致命错误。
可以在启动Java应用时,引入-XX:OnOutOfMemoryError=<cmd>参数,保证抛出第一个OutOfMemoryError时,执行一条命令。也可以指定-XX:+HeapDumpOnOutOfMemory,保证OnOutOfMemory出现时,可以生成堆的转储信息。还可以指定-XX:HeapDump-Path=<pathname>让用户指定堆转储的存放路径。
虽然编程人员极力避免死锁,但错误在所难免,当发生死锁时,在Linux上,可以通过发送SIGOUIT信号给Java进程id来生成Java级别的线程栈追踪信息并打印到标准输出。基于线程的栈追踪信息,可以分析死锁根源。
HotSpot VM垃圾收集器
Java堆中存储的对象由自动内存管理系统(也即常说的垃圾收集器)负责收集,不可以被显式销毁。垃圾收集器的运行方式和执行效率对应用的性能和响应性有极大的影响。
分代垃圾收集
HotSpot VM使用分代垃圾收集器,之所以使用这个垃圾收集算法,主要是基于以下两个观察事实:
(1) 大多数分配对象的存活时间很短。
(2) 存活时间久的对象很少引用存活时间短的对象。
上述两个观察事实统称为弱分代假设(Weak Generational Hypothesis),就Java应用来说,这个假设通常成立。基于此假设,HotSpot VM将堆空间分成两个物理区:新生代(Young Generation)和老年代(Tenured Generation)。默认情况下,新生代占据1/3的堆内存空间,老年代占据2/3的堆内存空间。
其中,新生代又细分为较大的Eden空间和两块较小的Survivor空间,默认Eden和Survivor的大小比例是8:1。此外,在堆区之外还有一个代就是永久代(Permanet Generation)。其图形表示如下:
新生代:大多数新创建的对象被分配在新生代中,与整个Java堆相比,通常新生代的空间比较小,且收集频繁。新生代中大部分对象的存活时间很短,所以通常来说,新生代收集(也称为次要垃圾收集,记作Minor GC)之后存活的对象很少。因为Minor GC关注小而且有大量垃圾对象的空间,所以通常垃圾收集的效率很高。
老年代:新生代中长期存活的对象最后会被提升(Promote)或晋升(Tenure)到老年代。通常来说,老年代的空间比新生代大,而空间占用的增长速度比新生代慢。因此,相比Minor GC而言,老年代收集(也称主要垃圾收集或完全垃圾收集,记作Full GC)的执行频率较低,且一旦发生,执行时间较长。注意,Full GC是对整个堆内存进行回收的操作,包括新生代和老年代。
永久代:永久代不会占用堆空间,而是直接占用HotSpot VM内存。主要存储存储类的元数据信息,如常量、静态变量、即时编译器编译后的代码等。在Java 8及以后的版本,永久代被元空间(Metaspace)所取代。元空间是Java 8使用本地内存来替代永久代,元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
分代收集的一大优点是,每个分代都可以依旧其特性使用最适当的垃圾收集算法。新生代通常当使用速度快的垃圾收集器,因为Minor GC频繁。这种垃圾收集器会浪费一定的空间,但新生代通常只是Java堆中的一小部分,所以问题不大。另一方面,老年代通常使用空间效率高的垃圾收集器,因为老年代要占用大部分Java堆。这种垃圾收集器不会很快,不过Full GC不会很频繁,所以对性能也不会有很大影响。
新生代
HotSpot VM新生代分为三个独立空间:一个Eden区,两个Survivor区。默认Eden和Survivor的大小比例是8:1。其中Eden区分配大多数新对象(不是所有,因为大对象可能直接分配到老年代)。Minor GC后Eden几乎总是空的。Survivor区存放的对象至少经历了一次Minor GC,它们在提升到老年代之前还有一次被收集的机会。
Minor GC后,Eden中的存活对象被复制到未使用的Survivor。被占用Survivor里不够老的存活对象,也被复制到未使用的Survivor。最后,被占用Survivor里"足够老"(默认超过15代)的存活对象被提升到老年代。
Minor GC之后,两个Survivor交换角色。Eden完全为空,且仍然只使用一个Survivor。因为收集过程中复制存活对象,所以这种垃圾收集器称为复制垃圾收集器。
在Minor GC中,如果Survivor中的存活对象溢出,多余的对象将被移到老年代。这称为过早提升。这会导致老年代中短期存活对象增加,可能会引发严重的性能问题。如果在Minor GC过程中,老年代满了,Minor GC之后通常会进行Full GC,这将导致遍历整个Java堆。这称为提升失败(Promotion Failure)。
快速内存分配
对象内存分配器的操作需要和垃圾收集器紧密配合。垃圾收集器必须记录它回收的空间,而分配器在重用堆空间之前需要找到可以满足其分配需求的空闲空间。垃圾收集器在Eden中运用指针碰撞(Bump-the-Pointer)的技术来有效地分配空间。这种技术追踪最后一个分配的对象,当有新的分配请求时,分配器只需检查最后一个分配对象和Eden末端之间的空间是否能容纳。如果能容纳,最后一个分配的对象的指针则调到新近分配对象的末端。
Java应用支持多线程场景,因此内存分配的操作需要考虑多线程安全。如果只用全局锁,则很容易引入性能瓶颈。HotSpot VM采用线程本地分配缓存区(Thread-Local Allocation Buffer, TLAB)的技术,为每个线程设置各自的缓冲区,以此改善多线程分配的吞吐量。注意,当线程的TLAB填满,HotSpot VM就需要采用多线程安全的方式了,但是这种情况较少,除非不合理的使用TLAB或实际业务量超过规格内设计。
垃圾收集器
Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定。基于JDK1.7 Update 14 之后的HotSpot虚拟机,所包含的收集器如下图所示:
可见,HotSpot采用了七种种垃圾收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。HotSpot之所以实现如此多的收集器,是因为目前并无完美的收集器出现,只能选择对具体应用最适合的收集器。
Serial收集器
Serial(串行)收集器是最基本、发展历史最悠久,基于复制算法的新生代收集器,是JDK 1.3.1之前新生代收集器的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(Stop The World)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接受的。Serial收集器的运行示意图如下:
尽管"Stop The World"会导致服务不可用,且HotSpot开发团队为消除或减少停顿而不断努力(从Parallel收集器到Concurrent Mark Sweep收集器,再到Garbage first收集器),在桌面级别应用场景,待收集内存不会太大,停顿时间完全可控制在几十毫秒,最多一百多毫秒。而且,作为单线程收集器,Serial收集器可以获得最高的单线程收集效率。
ParNew收集器
ParNew同样用于新生代,是Serial的多线程版本,并且在参数、算法(同样是复制算法)上也完全和Serial相同。Par是Parallel的缩写,但它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作,然后多线程执行垃圾回收。ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
ParNew收集器相比Serial收集器,仅实现基于多线程的GC,但它却是Server模式下的首选新生代收集器。其中一
个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器配合工作。
ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。
Parallel Scavenge 收集器
Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。支持参数控制,以及自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
Serial Old收集器
Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用"标记-整理"(Mark-Compact)算法。此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
(1)在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
(2)作为CMS收集器的后备预案,在并发收集发生Concurrenƒt Mode Failure时使用。
它的工作流程与Serial收集器相同。
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法。该收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:
CMS(Concurrent Mark Sweep)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。CMS收集器工作的整个流程分为以下4个步骤:
(1)初始标记(Initial mark):仅标记GC Roots能直接关联到的对象,速度很快(准确式内存),需要“Stop The World”。
(2)并发标记(Concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长,并发执行。
(3)重新标记(Remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
(4)并发清除(Concurrent sweep):对已标记的垃圾进行GC,并发执行。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS收集器的运作步骤中并发和需要停顿的时间:
CMS是一款优秀的收集器,起主要优点体现在:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。但是CMS收集器存在以下缺点:
(1)对CPU资源非常敏感。因面向并发设计程序,所以对CPU资源比较敏感。在多核时代,这个缺点已转换成优点。但在单核处理器场景下,则要慎重考虑。
(2)无法处理浮动垃圾(Floating Garbage)。 “浮动垃圾”是指在CMS并发清理阶段,用户线程运行产生的新垃圾。这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。“浮动垃圾”会导致出现“Concurrent Mode Failure”,进而引发另一次Full GC。同时,由于垃圾收集阶段用户线程还需运行,所以必须预留足够的内存空间给用户线程使用。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
(3)标记-清除算法导致的空间碎片。CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来不便。可以通过设置参数,决定执行合并整理的时机。
G1收集器
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的,可预测停顿时间的垃圾收集器。G1具备如下特点:
(1)并行与并发。G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间。
(2)分代收集。分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
(3)空间整合。G1从整体来看是基于“标记-整理”算法的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
(4)可预测的停顿。这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)垃圾收集器的特征。
横跨整个堆内存。G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离,而都是一部分Region(不需要连续)的集合。
建立可预测的时间模型。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描——Remembered Set。G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运作大致可划分为以下几个步骤:
(1)初始标记(Initial Mark)。仅标记 GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region中创建对象,此阶段需要停顿线程(Stop the World),但耗时很短。
(2)并发标记(Concurrent Mark)。从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final Mark)。为修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程(Stop the World),但可并行执行。
(4)筛选回收(Live Data Count and Evacuation)。 对各个Region中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
垃圾收集器比较
对比各个垃圾收集器
使用垃圾收集算法、使用场景(新生代,还是老年代)、组合对象、优缺点、…
HotSpot VM JIT编译器
在介绍HotSpot VM JIT之前,先讲解一般意义上的代码生成及JIT编译器所做的权衡,这有助于更好的理解HotSpot VM的JIT编译器。
编译是从高级语言生成机器码的过程。传统编译器从源语言(如C或C++)开始,将每个源文件编译成二进制目标文件,最后链接成库文件或可执行文件。Java从另一个角度使用编译器,它会先用编译器javac将Java源代码转换成类文件,然后将它们打包成jar文件,供Java虚拟机使用。所以Java虚拟机总是从原始程序的字节码开始,动态地转换成机器码。
所有编译器的结构大体相同,都必须要有前端接受源代码,然后转换成中间代码(Intermediate Representation,IR)。中间代码有许多种形式,实际上,编译器也会在编译的不同阶段使用不同的中间代码。一种常见的IR风格是静态单赋值(Static Single Assignment, SSA),特点是每个变量只赋值一次,指令要直接使用这些值。这种做法的好处是指令所用的值对它来说是直接可见的。另一种常用的是命名形式,概念上类似于源语言将值赋给变量或名字,而指令直接使用这些名字。
前端生成的IR通常是编译器优化最集中的地方,支持的优化范围很广,优化的动力是程序执行时间,而选择哪种优化是优化占用的编译时间决定的。最基本的优化有简单恒等变换、常量折叠、公共子表达式消除以及函数内联。更复杂的优化通常集中在改善循环的执行上,包括范围检查消除、展开以及循环不变代码迁移。
经过这些高级优化之后的IR,会被编译器后端接收,并转换成机器代码。这阶段包括指令选择和寄存器分配。指令选择有多种实现方式。可以直接使用指令或结合机器码与相关规则自动进行指令选择。
一旦完成了指令选择,就必须将寄存器指派给程序中的所有变量,并依据机器的调用约定生成代码。大多数情况下,存活变量的数目会超过机器寄存器的个数,所以生成代码只能将一部分变量同时分配给寄存器,通过在寄存器和栈之间来回移动变量,腾出寄存器空间,以容纳其他变量。将值移动到栈中称为值卸载或寄存器卸载。
经典的寄存器分配策略是图着色算法,通常可以使机器寄存器的使用率达到最高,而且多余的值很少会卸载到栈中。图表示的是同时有哪些变量在使用,以及哪些寄存器可以存放这些变量。如果同时存活的变量数超过了可用的寄存器数,重要性最低的变量将被移到栈中,使得其他变量可以使用寄存器。
类型继承关系分析
在面向对象的语言中,代码经常会被划分为小方法,将这些方法进行智能内联是获得高性能的重要手段。Java因为默认情况下父类方法(protected、public)是可被子类覆盖的,所以只看局部类型信息并不足以了解哪个方法可以内联。HotSpot VM解决这个问题的办法是类型继承关系分析(Class Hierarchy Analysis, CHA)。编译器利用CHA进行即时分析,判断加载的子类是否覆盖了特定方法。这种分析方法的关键在于,HotSpot VM只考虑已经加载的子类,而不用关系任何其他还不可见的子类。此外,CHA也被用来在已加载的类中识别只有一个接口或抽象类实现的情况。
编译策略
由于JIT没有时间编译程序中的所有方法,因此所有代码最初都是在解释器中运行。一旦方法被调用的次数变多,就可能变成编译。这个过程是由HotSpot VM中与每个方法关联的计数器来控制的。每个方法都有两个计数器:方法调用计数器和回边计数器。方法调用计数器在每次进入方法时加一,回边计数器在控制流每次从行号靠后的字节码回跳到靠前的字节码时加一。用回边计数器可以检查包含循环的方法,能使这些方法更早地转为编译。方法调用计数器的阈值是 C o m p i l e T h r e s h o l d CompileThreshold CompileThreshold,回边计数器的阈值公式复杂一些,是 C o m p i l e T h r e s h o l d ∗ O n S t a c k R e p l a c e P e r c e n t a g e / 100 CompileThreshold * OnStackReplacePercentage / 100 CompileThreshold∗OnStackReplacePercentage/100。
当发起编译请求时,这个编译请求会进入一个被一个或多个编译器线程监视的队列。如果编译器线程不忙,就会从编译请求队列中移出一个编译请求并开始编译。一旦编译完成,编译代码就会和该方法关联,然后下次调用时,就会使用该编译代码。通常来说,不等编译完成仍然继续执行是个好方法,因为执行和编译可以继续并行。如果想让解释器等编译完成,可以使用HotSpot VM命令行选项-Xbatch或-XX:-BackgroundCompilation阻塞执行,等待编译完成。
当解释器执行长期运行的Java循环时,HotSpot VM会选择一种称为栈上替换(On Stack Replacement, OSR)的特殊编译。当回边计数器溢出后,解释器会 发起编译请求,这次编译从回边的字节码开始而不是从方法的首个字节码开始。然后以解释器帧作为输入生成代码,并从此状态开始执行。在这种情况下,长时间运行的循环可以充分利用编译代码。这种以解释器帧作为输入执行的代码生成技术称为栈上替换。
逆优化
HotSpot VM中的"逆优化"是指那些经过若干级内联而来的编译帧转换为等价的解释器帧的过程。它可以将编译代码从多种乐观优化中回退回来,特别是从类型继承关系分析假设中回退回来。Server编译器在遇到"罕见陷阱"(Uncommon Trap)时也会使用逆优化。
JIT编译器的逆优化会在每个安全点上记录一些元数据,这些元数据描述了当时字节码的执行状态。因为安全点已经包含描述当前执行状态的方法链和字节码索引,所以像异常的栈追踪信息和安全检查所需要的栈遍历就都可以实现。对于逆优化,编译器还会记录局部变量和表达式栈中引用值的位置以及获得的锁。这是解释器帧状态在当时的抽象展现,足以构建一组解释器帧使得程序可以在解释器中继续执行。
Client JIT编译器概览
Client JIT编译器的目标是为了更快的启动时间以及快速编译。早期的Client JIT编译器是一个简单而快速的代码生成器,没有太多复杂性,而Java应用的性能也比较合适。它在概念上接近于解释器,会为每种字节码都生成一个模板,同时也维护了一个栈布局,类似于解释器帧。此外,它仅仅是将类的字段访问方法内联。为了改善整体性能,引入了很多优化技术,如Java 1.4添加了对CHA、逆优化的支持,Java 6将IR改成SSA风格,简单局部寄存器分配被替换成了线性扫描寄存器分配,支持用SSE进行浮点数计算,等。
Server JIT编译器概览
Server JIT编译器的目标是使Java应用的性能达到极致,吞吐量也达到最高,所以它设计的重点就是不遗余力的进行优化。它极力内联,这常常会造成大方法,而方法越大花费的时间也越多。它使用扩展的优化技术,涵盖了大量的极端情况,而要满足这些情况,就必须为每个它可能遇到的字节码都生成优化代码。
静态单赋值–程序依赖图
Server JIT编译器的中间代码(IR)是一个基于SSA(Static Single Assignment)的IR,但它使用不同的方式展现控制流,称为程序依赖图(Program Dependency Graph)。这种方法试图捕获每次操作执行过程中的最小约束,使得可以对操作进行激进重排和全局值计数,以此减少冗余计算。它有一个富类型系统可以捕获Java类型系统的所有细节,并将这些知识反馈给优化器。
所有基于Java字节码的JIT编译器都需要处理卸载或未初始化的类。Server JIT编译器的处理方式是,当它包含无法解析的常量池条目时,就会把路径标记为不可达。这种情况下,它会为这段字节码生成罕见陷阱并停止解析通过该方法的路径。罕见陷阱请求HotSpot VM运行时系统对当前已经编译好的方法采取逆优化,退回到解释器中继续执行,之前未能解析的常量池条目可以在解释器中继续处理并能正确的解析。
罕见陷阱也用来处理不可达的路径,使得编译器不会为方法中从未使用过的部分生成代码,最终生成更小的代码,并且有更多易于优化的直线型代码块。如果方法中经常发生罕见陷阱,HotSpot VM就会认为这种情况并非真的罕见,而抛弃先前编译生成的这部分代码,并放弃之前错误的假设,重新生成代码。生成罕见陷阱的好处是,后面的代码都能看到内联版本的情况,最终生成的代码质量就比调用一个对内存状态副作用不明的方法要好。
Server JIT编译器的生成代码对循环做了大量的优化,包括循环判断外提(Loop Unswitching)、循环展开(Loop Unrolling)以及用迭代分离(Iteration Splitting)进行的范围检查消除(Range Check Elimination)。迭代分离将循环转换成:预循环、主循环和后循环。它的思想是计算每个循环的边界,可以证明主循环不需要任何范围检查。预循环和后循环处理迭代的边界条件,需要进行范围检查。
一旦移除了循环的范围检查,就有可能将它展开。循环展开式相对简单的循环体,在循环中创建多个副本,从而减少循环的迭代次数。这种方法除了可以弥补一部分循环控制流的开销,还常常使循环体变得更简单,可以在较少的时间内完成更多的工作。在某些情况下,重复展开甚至可以完全消除循环。
循环展开的另一种优化,称为超字(Superword),它是循环向量化的一种形式。循环展开在循环体中创建可并行的操作,如果这些是在连续内存之上的操作,就可以被合并为一个矢量上的操作,使得单条指令在同样的时间内可以执行多个操作。
混合式JIT编译器
混合式JIT(Just-In-Time)编译器,采用分层编译(Tiered Compilation),融合了Client JIT编译器和Server JIT编译器的主要特性。分层编译是Java 7及之后的版本引入,并作为Java 8的默认编译模式。分层编译可以像Client JIT编译器(C1)那样快速启动,以及更多Server JIT编译器(C2)的高级优化技术,持续改善应用的性能。Java分层编译将Java虚拟机的执行状态分为了五个层次,包括:
(1) 解释执行:Java虚拟机在启动时首先以解释模式执行Java字节码,此时不进行任何优化。
(2) 执行不带profiling的C1代码:当Java虚拟机发现某些代码被频繁执行时,会将这些代码编译成不带profiling信息的C1代码(由C1编译器生成)。这种代码的执行速度比解释执行快,但可能不是最优的。
(3) 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码:Java虚拟机继续监控代码的执行情况,并收集方法调用次数和回边执行次数等profiling信息。基于这些信息,Java虚拟机会再次将这些代码编译成带有部分profiling信息的C1代码。这种代码的执行速度更快,但仍然不是最优的。
(4) 执行带所有profiling的C1代码:当Java虚拟机收集到足够的profiling信息后,会将这些信息用于指导C1编译器的优化。此时生成的C1代码带有完整的profiling信息,执行速度更快。
(5) 执行C2代码:对于热点代码(即频繁执行且占用大量CPU时间的代码),Java虚拟机会将其编译成由C2编译器生成的优化后的机器码(C2代码)。这种代码的执行速度最快,但编译过程也最耗时。
Java分层编译模式通过动态调整编译策略和优化级别,使得Java虚拟机在执行Java程序时能够更快地响应并优化热点代码,从而提高程序的执行效率和性能。同时,由于分层编译模式采用了多种编译策略和优化级别,因此可以适应不同的应用场景和需求。
HotSpot VM自适应调优
Java 5 HotSpot VM引入新特性,可以依据JVM启动时的底层平台和系统配置自动选择垃圾收集器、配置Java堆以及选择运行时JIT编译器。此外,该特性还使得Throughput收集器可以依据程序运行的情况,自适应调整Java堆和对象分配的速率。自动选择平台相关的默认值和适应调整Java堆可以减少手工对垃圾收集进行调优的工作量,这称为自动调优(Ergonomics)。
自适应Java堆调整
JVM的自动优化开启Throughput收集器时,会开始另一个自适应堆调整(Adaptive Heap Sizing)的特性。通过评估应用中对象的分配速率和生命周期,自适应堆调整试图优化HotSpot VM新生代和老年代的空间大小。HotSpot VM监控Java应用中对象的分配速率和生命周期,然后决定如何调整新生代空间,使得短生命周期的对象在尚未被提升到老年代之前就能被收集,同时允许存活时间长的对象适时地被提升,避免在Survivor区之间进行不必要的复制。
在没有HotSpot VM工程师的指导下,更常见的做法是关闭自适应堆调整,显式指定新生代的大小,包括Eden和Survivor空间。对于大多数使用Throughput收集器的Java应用来说,通过-XX:+UseParallelGC或-XX:+UseParallelOldGC开启自适应堆调整,就可以更好地优化新生代空间。
超越自动优化
追求应用性能的过程中,经常会遇到性能需求超出HotSpot VM自动优化所能达到的改善范围。一个例外是自适应堆调整,这个参数在使用Throughout收集器时会自动开启。对大多数Java应用来说,新生代空间的自动自适应堆调整可以工作得很好。
参考
《Java性能优化权威指南》 Charlie Hunt, Binu John 著, 柳飞, 陆明刚 译
https://blog.51cto.com/u_15162069/2729306 从Java虚拟机规范看HotSpot虚拟机的内存结构和变迁
https://www.cnblogs.com/1024Community/p/honery.html JVM的垃圾回收机制
https://mp.weixin.qq.com/s/feJKRqYJTVEIxl6jvjevAg 从头到尾说一次 Java 的垃圾回收
https://www.oracle.com/webfolder/technetwork/tutorials/mooc/JVM_Troubleshooting/week1/lesson1.pdf Troubleshooting Memory Issues in Java Applications
https://www.jianshu.com/p/50d5c88b272d 深入理解JVM(5) : Java垃圾收集器