1. JVM 调优概要
JVM 调优是为了合理利用服务器硬件资源,针对性调整JVM参数,以获得最大的吞吐量或者最快的响应速度。
1.1 如何让程序运行的效率更高?
一个Java程序的运行效率取决于硬件质量、JVM 质量、程序代码质量。服务器硬件质量是公司决定的。作为一个开发者,如何让程序运行的效率更高,能做的有以下几点:
- 合理的架构
- 写出质量高,易于被JVM处理的代码
- 根据应用的特点,合理地使用 JVM 的参数,来使JVM更好地服务我们的应用,这也就是 JVM 调优。
1.2 JVM 调优的重要指标
- 内存占用:程序正常运行占用的内存大小
- 吞吐量:程序垃圾回收占用的时间越少,吞吐量越大
- 延迟:程序垃圾回收停顿的时间越短,延迟越低
2. JVM 内存分配
2.1. 线程共享内存
2.1.1. 堆
堆是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。在JDK 1.8 垃圾回收中,堆被分为两个部分:
- 新生代
新生代存储一些存活时间短的对象,正常情况下,绝大部分对象的存活都比较短。
新生代又分为 Eden, Survivor from 和 Survivor to 三个区。默认情况下内存分配 Eden : from : to = 8 : 1 : 1。
Eden区的对象经过一次GC之后没有被回收就会进入 Survior 区。两个Survivor 区是对等的,为了便于使用复制GC算法设置。 - 老年代
老年代主要存储一些存活时间比较长的对象。
2.1.2. 元数据区
元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。
2.1.3. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
2.2. 线程隔离内存
2.2.1. 程序计数器
线程私有,指向当前线程正在执行的字节码代码的行号。
2.2.2. 虚拟机栈
线程私有,生命周期与线程相同。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
2.2.3. 本地方法栈
功能与Java虚拟机栈十分相同。区别在于,本地方法栈为虚拟机使用到的native方法服务。
3. 垃圾回收
3.1. 引用计数法
引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;计数器为 0 时,对象就会被回收。
- 优点:容易实现,判定效率高。
- 缺点:循环引用的对象无法回收。比如 A 引用了 B, 那么B的计数为1, B引用了A,B的计数也为1,尽管A和B都没有在其他地方被引用,计数永远为1,无法被回收。
3.2. 可达性分析
由于引用计数法无法回收循环引用的对象,所以引入了一种称为可达性分析的算法。该算法的基本思想是从一些根对象出发(称之为 GC roots),从根节点向下搜索(搜索所有引用),在引用链上的对象称之为可达,当一个对象不存在于所有GC roots 的引用链时,则称该对象为不可达。
上图中,object 1-4 都是可达的,不可被回收。object 5-6 是不可达对象,可被回收。
在Java中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
3.2. 标记-清除算法
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
当堆中的有效内存空间(available memory)被耗尽的时候,标记-清除算法就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 优点:容易实现
- 缺点:效率低,需要 stop the world, 容易造成不连续的内存碎片,内存使用变困难。
3.3. 标记-复制算法
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
- 优点: 速度快,不会产生内存碎片
- 缺点:空间浪费,只有一半内存是可用的
3.4 标记-整理法
与标记-清楚法类似,标记-整理法也分为两个阶段: 标记阶段和整理阶段
标记-整理算法首先需要从根节点开始,对所有可达对象做一次标记;然后将所有的存活对象整理到内存的一端,最后清理另一端的所有空间。
特点:
- 相比于标记-复制算法,不需要将内存一分为二,节省了内存,但是它不仅要标记所有存活对象,还要整理所有存活对象的引用地址,效率比标记-复制算法更低,
- 相比于标记-清除算法,不会产生内存碎片。
4. JVM的内存分配和垃圾回收策略
4.1 内存分配
-
对象优先分配到 Eden 区
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。 -
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。当大对象所需要的内存大于某个阈值(tenureSizeThreshold)时,会在老年代分配内存。大对象是代码编写中需要避免的 -
长期存活的对象将进入老年代
每个对象有一个记录其存活时间的age属性(以经历的GC次数为单位)。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,存活下来的对象将被分配到Survivor to,并且 age + 1。当对象的 age 达到某个阈值(MaxTenuringThreshold)时,该对象将被分配到老年代。 -
老年对象的动态年龄界定
MaxTenuringThreshold 并不是界定老年对象的唯一标准。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 要求的年龄。
4.2 垃圾回收
- Minor GC: 使用标记-复制算法,发生在新生代
当Eden区没有足够的空间分配时,触发一次 Minor GC。
年轻代将内存分为三个部分:一个 Eden 区和两个 Survivor 区。两个Survivor 区有一个必定是空,这个区被称作 Survivor to, 另外一个被称作 Survivor from。当 Minor GC发生时,会将 Eden 区和 Survivor from 区存活的对象分配到 Survivor to,并且对象的 age + 1, 然后清理 Eden 区和 Survivor from 区的所有内存,Survivor from 区将变为下一次 Minor GC 的 Survivor to 区。 - Full GC:标记-整理算法,发生在老年代
- 当老年代没有足够的空间分配时,触发一次 Full GC。
- 系统调用System.gc()大概率触发 Full GC
5. 垃圾收集器
5.1. 新生代收集器
-
Serial 收集器:单线程收集器,采用标记-复制算法,进行gc操作的过程中必须 stop the world.
- 优点:额外内存消耗最小,简单而高效((与其他收集器的单线程相比),没有线程上下文切换带来的额外开销
- 缺点:stop the world 带来的停顿时间有时很长,而且不可控制。由于是单线程,无法利用多核处理器
适用于单核机器,对响应时间没有要求的客户端应用。
-
ParNew 收集器:是Serial收集器的多线程并行版本。
- 优点:唯一一款能与 CMS 收集器使用的新生代收集器
- 优点:唯一一款能与 CMS 收集器使用的新生代收集器
-
Parallel Scavenge 收集器:与 ParNew 收集器类似,支持多线程,但是Parallel Scavenge收集器,是一个吞吐量优先收集器。
5.2 老年代收集器
- Serial Old 收集器:Serial 收集器的老年代版本,采用标记-整理算法。
- Parallel Old收集器: Parallel Scavenge 收集器的老年代版本,多线程,采用标记-整理算法
- CMS收集器: 全名 Concurrent Mark Sweep 收集器,采用标记-清除算法,以获取最短回收停顿时间为目标,适合对响应速度要求高的服务端应用。
其运作过程包括四个步骤:- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark):是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(CMS remark):是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
- 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时的
其中初始标记和重新标记仍然需要 stop the world。
堆收集器 Garbage First 收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
主要面向服务端。此前,基于分代垃圾收集思想的收集器,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老
年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
运作过程
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS
指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要
停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际
并没有额外的停顿。 - 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆
里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以
后,还要重新处理SATB记录下的在并发时有引用变动的对象。 - 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留
下来的最后那少量的SATB记录。 - 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回
收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region
构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧
Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行
完成的。
6. JVM 调优方法
JVM 调优首先是选择合适的收集器,然后根据使用的选择器选择合适的参数。
6.1. JVM 基础参数
参数 | 描述 |
---|---|
-Xms | memory size, JVM 启动初始堆大小,一般与 -Xmx 相同,避免垃圾回收后重新分配堆内存 |
-Xmx | memory max, JVM 可用最大堆内存 |
-Xss | stack size, 每个线程虚拟机栈可用内存大小 |
-Xmn | memory new, 新生代内存大小 |
-XX:MaxMetaspaceSize | 元空间最大内存,默认为 -1,受限于服务器内存, JDK 8 可用 |
-XX: MetaspaceSize | 元空间初始大小,每次原空间扩容达到这个值就会触发垃圾收集进行类卸载, JDK8 可用 |
6.2. 收集器选择 (-XX)
参数 | 使用的收集器 | 描述 |
---|---|---|
UseSerialGC | Serial + Serial Old | 虚拟机运行在 Client 模式下的默认值 |
UseParNewGC | ParNew + Serial Old | JDK 9 之后不再支持 |
UseConMarkSweepGC | ParNew + CMS + Serial Old | Serial Old 收集器作为 CMS 发生"Concurrent Mode Failure"失败之后的备选 |
UseParallelGC | Parallel Scavenge + serial old(PS MarkSweep) | JDK 9 之前 Server 端默认模式 |
UseParallelOldGC | Parallel Scavenge + Parallel Old | |
UseG1GC | G1 | JDK 9 之后 Server 端默认模式 |
6.2. 常用调优参数
参数 | 默认值 | 描述 |
---|---|---|
SurvivorRaito | 8 | Edon 区和 Surivor 区的内存比例, 默认为 8 : 1 |
PretenureSizeThreshold | 直接分配到老年代的对象大小阈值 | |
MaxTenuringThreshold | 新生代对象晋升到老年代的年龄 | |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域大小已经晋升到老年代的年龄 | |
HandlePromotionFailure | 是否允许分配担保失败 | |
ParallelGCThreads | 并行收集的线程数 | |
GCTimeRaito | 99 | GC 时间占用程序运行总时间的比例,仅在使用 Parallel Scavenge 时有效 |
MaxGCPauseMillis | GC 的最大停顿时间, 仅在使用 Parallel Scavenge 时有效 | |
CMSInitiatingOccupancyFraction | 68% | 仅 CMS 可用,老年代空间使用多少后触发垃圾收集 |
UseCMSCompactAtFullCollection | 设置 CMS 在垃圾收集之后是否进行一次碎片整理, JDK 9 开发废弃 | |
FullGCsBeforeCompaction | 设置 CMS 在若干次垃圾收集之后进行一次碎片整理,JDK 9 开始废弃 | |
G1HeapRegionSize | 设置 Region 大小,并非终值 | |
MaxGCPauseMillis | 200ms | 设置 G1 收集过程目标时间,并非硬性条件 |
G1NewSizePercent | 5% | 新生代最小值,G1生效 |
G1MaxNewSizePercent | 60% | G1新生代最大值 |