JVM调优攻略

Rothschil 2020-05-21 15:00:00
Java

1. 写在前面

本章节涉及JVM调优演示所使用的环境为JDk1.8IDEA2020Maven3.5。因为在讲解JVM调优过程中,还会涉及到一些内存方面的知识,这里只简要概括下,如果在看的过程中觉得有吃力的地方,需要再去补充!

2. 内存

2.1. 堆与栈

栈与堆 是应用程序运行的关键。

堆与栈

栈是运行时单位,解决程序的运行问题,即程序如何执行,或者说如何处理数据;而堆是存储的单位,即数据怎么放、放在哪儿。

2.2. 内存模型

JDK1.8内存模型

2.2.1. 程序计数器

线程私有的,互不影响、独立存储,是当前线程执行的字节码的行号指示器。让字节码解释器工作时可以通过改变这个计数器的值来选取下一执行的字节码的指令,分支、循环、跳转、异常、恢复都需要这个计数器来完成。

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域。

2.2.2. 虚拟机栈

线程私有的,JAVA方法执行的动态内存模型,每个方法执行都会创建一个栈帧,伴随着方法创建到执行完成,用于存储局部变量表、操作数栈、动态链接、方法出口信息等组成。

2.2.3. 本地方法栈

虚拟机栈 类似,只不过它是虚拟机用到的 Native方法服务的,也有栈帧,同样的也会抛出与 虚拟机栈一样的异常。

2.2.4. 堆

这是虚拟机所管理的内存中最大的一块,所有对象实例均存储在这块区域,因此也被人称作GC堆,也是垃圾回收器重点工作的区域。分为:年轻代、老年代。

2.2.4.1. 年轻代

新生代也称年轻代(Young Gen)这里二者没有区别,所以在篇幅中我都会混着用。年轻代主要存放新创建的对象,内存大小相对较小,垃圾回收比较频繁,将年轻代 再细分,它有 Eden和两个Suvivor(也可以叫做fromto),二者之间默认比例为8:1,至于为啥默认会是这样,后面会聊到。

2.2.4.2. 年老代

年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

2.2.4.3. 内存申请流程

内存申请流程

以上内容的重点是年轻代中的GC,JVM总要保证 Suvivor(fromto)中 to 是空的。在 Minor GC 后,将Eden存活的对象复制到 to,而在 from中的对象,根据年龄来决定,已到阈值的对象,直接移动到老年代;没有到达阈值的则被复制到 to,这时候清空 Edenfromfromto 的角色将调换,重复上述动作。

年轻代中的GC

2.3. 对象的内存布局

对象在JVM中布局,主要分为两种:普通对象、一种是数组对象。

2.3.1. 普通对象

普通对象在内存布局结构

分为两种情况,开启指针压缩的情况下,类型指针(4 bytes)加上 对齐填充(4 bytes),所以一共 16 bytes;未开启指针压缩的情况下,类型指针(8 bytes)加上 对齐填充(40 bytes),所以一共 16 bytes。

虽然最终大小一致,但是还是有区别。

2.3.2. 数组对象

数组对象在内存布局结构

同理,这里也分 指针压缩 是否开启两种情况,原理同上!

2.4. 计算空对象大小

new 一个空对象,查看它占用的空间大小。这里会用一个依赖包jol,加入依赖pom

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

利用ClassLayout.parseInstance($OBJECT$).toPrintable() 查看对象信息。

2.4.1. 计算空对象大小

2.4.1.1. 关闭指针压缩情况下

未开启指针压缩的情况下,一个空对象占用空间大小是 16 bytes。

1
2
3
4
5
6
7
8
xyz.wongs.jvm.JvmObjSize object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 40 70 06 00 (01000000 01110000 00000110 00000000) (421952)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2.4.1.2. 开启指针压缩情况下

MARK Word:8 bytes
类型指针:4 bytes(kclass Pointer)
数组长度:0 bytes
实例数据:0
对齐填充:4 bytes

1
2
3
4
5
6
7
8
xyz.wongs.jvm.JvmObjSize object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 40 70 06 00 (01000000 01110000 00000110 00000000) (421952)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2.4.2. 数组对象

只有数组对象,才有对齐填充

2.4.3. 普通对象

2.4.3.1. 开启指针压缩

MARK Word:8 bytes
类型指针:4 bytes(kclass Pointer)
数组长度:0 bytes
实例数据:4 bytes
对齐填充:0 bytes

1
2
3
4
5
6
7
8
9
xyz.wongs.jvm.JvmObjSize02 object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 40 70 06 00 (01000000 01110000 00000110 00000000) (421952)
12 4 int JvmObjSize02.anInt 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

2.4.3.2. 关闭指针压缩

MARK Word:8 bytes
类型指针:4 bytes(kclass Pointer)
数组长度:0 bytes
对齐填充:4 bytes

实例数据:4 bytes
对齐填充:4 bytes

1
2
3
4
5
6
7
8
9
10
11
12
xyz.wongs.jvm.JvmObjSize02 object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) d8 53 4d 7a (11011000 01010011 01001101 01111010) (2051888088)
12 4 (object header) 93 02 00 00 (10010011 00000010 00000000 00000000) (659)
16 4 int JvmObjSize02.anInt 2
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Process finished with exit code 0

2.5. 指针压缩

1
2
3
4
5
C:\Users\WONGS>java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=399880768 -XX:MaxHeapSize=6398092288 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
java version "11.0.2" 2019-01-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.2+9-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS, mixed mode)

2.5.1. 实现原理

2.5.2. 真实地址如何计算出来

2.5.3. 调优参数

2.5.4. oop,最大值是?

2.5.5. 压缩哪些东西,不压缩哪些?

2.6. 虚拟机栈溢出与调优

问题分析的方法

2.7. JAVA启动参数

参数 描述 例子
Xmx 设置最大堆内存 -Xmx3550m
Xms 设置初始堆内存,很多时候Xms与Xmx设置相同,以避免每次垃圾回收完成后JVM重新分配内存,提高性能。 -Xms3550m
Xss 设置每个线程的栈大小,JDK5以后每个线程栈大小为1M,之前每个线程栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。 -Xss128k
Xmn 设置年轻代大小,在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。 -Xmn2g
XX:NewSize 设置年轻代初始值为1024M -XX:NewSize=1024m
XX:MaxNewSize 设置年轻代最大值为1024M。 -XX:MaxNewSize=1024m
XX:PermSize 设置持久代初始值为256M。 -XX:PermSize=256m
XX:MaxPermSize 设置持久代最大值为256M。 -XX:MaxPermSize
XX:NewRatio 设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。 -XX:NewRatio=4
XX:SurvivorRatio 设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。 -XX:SurvivorRatio=4
XX:MaxTenuringThreshold 表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。 -XX:MaxTenuringThreshold=7
XX:PretenureSizeThreshold 设置持大对象的大小,超过该大小将进入老年代。 -XX:PretenureSizeThreshold=9M
XX:HandlePromotionFailure 空间担保 -XX:+/-HandlePromotionFailure

-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3 组参数都可以影响年轻代的大小,混合使用的情况下,优先级是什么?
如下:

推荐使用 -Xmn 参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成

2.7.1. 垃圾回收参数

2.7.2. 辅助信息参数设置

3. 垃圾回收

3.1. 垃圾回收算法

3.1.1. 按基本回收策略

3.1.1.1. 引用计数(Reference Counting)

原理就是维持对象的一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

3.1.1.2. 标记-清除(Mark-Sweep)

标记-清除图列

此算法执行分两阶段:

3.1.1.3. 标记-整理(Mark-Compact)

标记-整理图例

标记-整理 集合 标记-清除复制 两个算法的优点,也是分两阶段:

3.1.2. 按分区对待的方式

3.1.2.1. 增量收集(Incremental Collecting)

实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。

3.1.2.2. 分代收集(Generational Collecting)

基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

3.1.3. 按系统线程分

3.1.3.1. 串行收集

串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。

3.1.3.2. 并行收集

并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。

3.1.3.3. 并发收集

相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

3.2. 垃圾回收器

行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况,所以生产环境的选择主要是并行收集器和并发收集器。在JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行智能判断。

3.2.1. 串行收集器(Serial Garbage Collector)

3.2.1.1. Serial

Serial 历史悠久,它是为单线程环境而设计的垃圾收集器,适合桌面应用,内存较小的场景,它在工作中会产生停顿,

设置:

1
-XX:+UseSerialGC

3.2.1.2. ParNew

ParNew 其实是也是Serial,唯一的区别就是将Serial单线程改为多线程执行。

设置:

1
-XX:+UseParNewGC

3.2.2. 并发收集器(响应时间优先)

并发收集器 适合 “对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

3.2.2.1. CMS收集器 Concurrent Mark Sweep

3.2.3. 并行收集器(吞吐量优先)

“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。缺点就是垃圾收集过程中应用响应时间可能加长

Parallel Sacvenge

3.2.3.1. G1收集器

Garbage First 简称G1,完全是为了大型应用而准备的,详细可以参考G1,这篇文章写的不错,值得推荐!

3.2.4. 其它垃圾回收参数

3.3. 空间分配担保

在新生代已经无法实现分配内存的时候,这时候JVM采用将新生代的对象转移到老年代,腾出来的空间让新加入的对象存储在新生代。

乍一看可能不太好理解,我在网上看一例子,觉得挺好,我们就用它来模拟,增强我们的理解。

尝试分配3个2MB的对象和一个4MB的对象,然后我们通过JVM参数 -Xms20M、-Xmx20M、-Xmn10M 把Java堆大小设置为20MB,不可扩展,详细JVM参数如下:

3.3.1. JVM启动参数

注意 这里 JDK版本为1.8,垃圾收集器是 Serial+Serial Old 模式,不同JDk版本以及垃圾回收器,实际效果可能会有差异!!!

1
2
3
4
5
6
7
-verbose:gc
-Xlog:gc*
-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseSerialGC
-XX:SurvivorRatio=8

3.3.2. JAVA代码

1
2
3
4
5
6
7

public static void main(String[] args) {
byte[] bytes1 = new byte[2 * 1024 *1024];
byte[] bytes2 = new byte[2 * 1024 *1024];
byte[] bytes3 = new byte[2 * 1024 *1024];
byte[] bytes4 = new byte[4 * 1024 *1024];
}

3.3.3. GC日志

1
2
3
4
5
6
7
8
9
10
11
12
[GC (Allocation Failure) [DefNew: 6266K->660K(9216K), 0.0047403 secs] 6266K->4756K(19456K), 0.0076457 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
def new generation total 9216K, used 7042K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 77% used [0x00000000fec00000, 0x00000000ff23b5a0, 0x00000000ff400000)
from space 1024K, 64% used [0x00000000ff500000, 0x00000000ff5a5290, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3464K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 379K, capacity 388K, committed 512K, reserved 1048576K


3.3.4. 分析

[GC (Allocation Failure) [DefNew: 6266K->660K(9216K), 0.0047403 secs] 6266K->4756K(19456K), 0.0076457 secs]
其中 [DefNew: 6266K->660K(9216K), 0.0047403 secs] 表示GC前该内存区域已使用容量->GC后该内存区域已使用容量,后面圆括号里面的9216K为该内存区域的总容量;6266K->4756K(19456K), 0.0076457 secs 表示GC前Java堆已使用容量->GC后Java堆已使用容量,后面圆括号里面的19456K为Java堆总容量。

1
2
tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)

3.3.5. 进阶篇

下面来玩个另类的, 以上操作演示我们都是建立 Serial+Serial Old 垃圾回收器,这时候我们换Parallel Scavenge+Serial Old 演示下:

1
2
3
4
5
6
7
-verbose:gc
-Xlog:gc*
-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseParallelGC
-XX:SurvivorRatio=8
1
2
3
4
5
6
7
8
9
10
11
[GC (Allocation Failure) [PSYoungGen: 6266K->792K(9216K)] 6266K->4896K(19456K), 0.0141220 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] 
Heap
PSYoungGen total 9216K, used 7173K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b5a0,0x00000000ffe00000)
from space 1024K, 77% used [0x00000000ffe00000,0x00000000ffec6030,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3462K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 379K, capacity 388K, committed 512K, reserved 1048576K

1
2
3
4
5
6
7
8
9
10
[GC (Allocation Failure) [PSYoungGen: 6266K->760K(9216K)] 6266K->4864K(19456K), 0.0035288 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
PSYoungGen total 9216K, used 6117K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 65% used [0x00000000ff600000,0x00000000ffb3b5a0,0x00000000ffe00000)
from space 1024K, 74% used [0x00000000ffe00000,0x00000000ffebe030,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3464K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 379K, capacity 388K, committed 512K, reserved 1048576K

3.4. 逃逸分析与栈上分配

分析对象的作用域,只有对象的作用的是成员变量,才会发生逃逸。作用域在方法体内,不会发生逃逸。
逃逸的三种情况:

对象的作用域仅在当前方法体内,不会发生逃逸,对象的内存分配在栈内存,这样效率高!

4. 主要参考