JVM内存分配和参数

打印参数

在虚拟机运行过程中,可以根据一些跟踪系统状态的参数,来排查故障或者参数设置,可以打印系统运行时的一些相关参数,分析实际问题。对参数的配置,主要也就是围绕堆、栈、方法区进行配置。

打印jvm日志的方式

eclipse

20200410181219

控制台得到参数

1
2
3
4
5
6
7
8
9
Heap
PSYoungGen total 35840K, used 2458K [0x00000000d8500000, 0x00000000dad00000, 0x0000000100000000)
eden space 30720K, 8% used [0x00000000d8500000,0x00000000d8766888,0x00000000da300000)
from space 5120K, 0% used [0x00000000da800000,0x00000000da800000,0x00000000dad00000)
to space 5120K, 0% used [0x00000000da300000,0x00000000da300000,0x00000000da800000)
ParOldGen total 81920K, used 0K [0x0000000088e00000, 0x000000008de00000, 0x00000000d8500000)
object space 81920K, 0% used [0x0000000088e00000,0x0000000088e00000,0x000000008de00000)
Metaspace used 2748K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 293K, capacity 386K, committed 512K, reserved 1048576K

IntelliJ IDEA:

于是,运行程序后,GC日志就可以打印出来了,和eclipse测试一样。

解读日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
Heap
新生代分配情况
PSYoungGen total 35840K, used 3072K [0x00000000d8500000, 0x00000000dad00000, 0x0000000100000000)
eden space 30720K, 10% used [0x00000000d8500000,0x00000000d88002b8,0x00000000da300000)
from space 5120K, 0% used [0x00000000da800000,0x00000000da800000,0x00000000dad00000)
to space 5120K, 0% used [0x00000000da300000,0x00000000da300000,0x00000000da800000)
老年代分配情况
ParOldGen total 81920K, used 0K [0x0000000088e00000, 0x000000008de00000, 0x00000000d8500000)
object space 81920K, 0% used [0x0000000088e00000,0x0000000088e00000,0x000000008de00000)
元空间分配情况,jdk8彻底去除了永久代
Metaspace used 3125K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 342K, capacity 388K, committed 512K, reserved 1048576K

堆的分配参数

1、-Xmx -Xms:指定最大堆和最小堆
    -Xmx设置程序能获得最大堆大小,-Xms设置程序启动时初始堆大小
2、-XX:+PrintGC 遇到GC就会打印日志。
3、-XX:+UseSerialGC 配置串行回收器
4、-XX:+PrintGCDetails查看GC详情,包括各个回收区的情况。
5、-XX:+PrintCommandLineFlags:可以将隐式或显示传给JVM的参数输出,就是把配置的参数所改变的信息也打印输出。

-XX这种这是是对jvm系统级别的配置,非-XX配置基本是对应用级别的设置。+启用-禁用

实际中,可以直接将初始的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提到性能。

默认程序堆大小,可以通过Java程序获取

1
2
3
4
5
public static void main(String[] args) {
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); // 系统的最大空间
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); // 系统的空闲空间
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); // 当前可用的总空间
}
Xmx=1694.5M
free mem=113.19983673095703M
total mem=115.0M

Demo测试设置(多个命令一起执行): -XX:+PrintGC -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags

-XX:+PrintGC 打印GC
-Xms40M 设置堆初始大小40M
-Xmx40M 设置堆最大大小40M
-Xmn20M 设置堆中 new Generation 新生代的大小为20M
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+UseSerialGC 设置GC回收器模式是串型垃圾回收器
-XX:+PrintCommandLineFlags  配置的参数所改变的信息也打印输出

Java代码:

1
2
3
4
5
6
7
8
9
10
/**
* -XX:+PrintGC -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
* @param args
*/
public static void main(String[] args) {
byte[] b1 = new byte[4 * 1024 * 1024];
byte[] b2 = new byte[4 * 1024 * 1024];
byte[] b3 = new byte[4 * 1024 * 1024];
byte[] b4 = new byte[8 * 1024 * 1024];
}

分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

初始分配:
Java堆,共有 40M
新生代,有20M
Eden区,16M
Servivor From ,2M
Servior To ,2M
老年代,有20M
当b1/b2/b3执行完时
Eden 被分配12M
当a4执行时
因为需要分配8M,但现在Eden中只有4M空闲,无法满足b4申请;
from和to中也只有2M,也不能满足,所以要触发MinorGC;
MinorGC执行,处理b1/b2/b3的空间,他们都是4M,所以servivor也不够转移,直接回被移到老年代;
MinorGC执行后,分配如下:
新生代,total 20M
Eden区 free 16M
Servivor From free 2M
Servior To free 2M
老年代,total 20M free 8M
再次分配b4
新生代,total 20M
Eden区 free 8M
Servivor From free 2M
Servior To free 2M
老年代,total 20M free 8M

运行结果分析: 配置项大小结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-XX:InitialHeapSize=41943040 -XX:MaxHeapSize=41943040 -XX:MaxNewSize=20971520 -XX:NewSize=20971520 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
[GC (Allocation Failure) [DefNew: 13599K->553K(18432K), 0.0109283 secs] 13599K->12841K(38912K), 0.0118473 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
新生代
def new generation total 18432K, used 8909K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
eden space 16384K, 51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
from space 2048K, 27% used [0x00000000fea00000, 0x00000000fea8a558, 0x00000000fec00000)
to space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
老年代
tenured generation total 20480K, used 12288K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 60% used [0x00000000fec00000, 0x00000000ff800030, 0x00000000ff800200, 0x0000000100000000)
元空间
Metaspace used 2749K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 293K, capacity 386K, committed 512K, reserved 1048576K

运行结果基本符合推导结果。

6、-Xmn

设置新生代大小

7、-XX:NewRatio 

新生代(eden+2*s)和老年代(不包含永久区)的比值,默认2
例如:4,表示新生代:老年代=1:4,即新生代占整个堆的1/5

8、-XX:SurvivorRatio 设置两个Survivor区和eden的比值,默认8

 例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10

9、-XX:+HeapDumpOnOutOfMemoryError

OOM(Out Of Memory内存溢出)时导出堆到文件,根据这个文件,我们可以看到系统dump时发生了什么。
内存分析工具:Memory Anakyaer,eclipse插件
-XX:+HeapDumpPath        导出OOM的路径
例如:XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
我们可以用VisualVM打开这个dump文件。
注:关于VisualVM的使用,可以参考下面这篇博客:
使用 VisualVM 进行性能分析及调优:http://www.ibm.com/developerworks/cn/java/j-lo-visualvm/
或者使用Java bin目录里自带的Java VisualVM工具也行

10、-XX:OnOutOfMemoryError

在OOM时,执行一个脚本。可以在OOM时,发送邮件,甚至是重启程序。
例:-XX:OnOutOfMemoryError=D:/..../jdk/bin/printstack.bat %p    p代表的是当前进程的pid
上方参数的意思是说,执行printstack.bat脚本,而这个脚本做的事情是:D:/..../jdk/bin/jstack -F %1 > D:/a.txt,即当程序OOM时,在D:/a.txt中将会生成线程的dump。

11、-XX:PermSize -XX:MaxPermSize

设置永久区的初始空间和最大空间。也就是说,jvm启动时,永久区一开始就占用了PermSize大小的空间,如果空间还不够,可以继续扩展,但是不能超过MaxPermSize,否则会OOM。
jdk8开始,已经移除永久代。

12、

-XX:MaxTenuringThreshold 指定新生代经过多少次回收后进入老年代,默认15
-XX:PretenureSizeThreshold 设置对象大小超过指定大小后,直接进入老年代。注意线程TLAB区优先分配空间问题。
TLAB(Thread Local Allocation Buffer)线程本地分配缓存,是为了加速对象 分配而生的。线程私有。
JVM使用这个区来避免多线程冲突问题,提高对象分配的效率,TLAB空间不会太大,当大对象无法再TLAB上分配时,才会直接分配到堆上。
-XX:+UseTLAB 使用TLAB
-XX:+TLABSize 设置大小
-XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象大小,他是一个比值,默认64,如果对象DAU整个空间的1/64则在堆上创建对象。
-XX:+PrintTLAB 查看TLAB信息
-XX:ResizeTLAB 自调整TLABRefillWasteFraction的阈值。

总结:实际中,根基实际情况调整新生代和幸存代的大小;在OOM时,记得把dump文件导出,可以促进排查问题。

栈的分配参数

1、-Xss 指定线程的最大栈空间,决定了函数可以调用的最大深度。

  
每个线程都有独立的栈空间;
局部变量、参数 分配在栈上;
栈空间是每个线程私有的区域。栈里面的主要内容是栈帧,而栈帧存放的是局部变量表,局部变量表的内容是:局部变量、参数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public class Demo2 {
private static int count = 0;
public static void recursion() {
long l1 = 1;
count++;
recursion();
}
public static void main(String[] args) {
try {
recursion();
} catch (Throwable e) {
System.out.println(count);
e.printStackTrace();
}
}
}

这段代码是没有出口的递归,肯定会出现OOM.

如果设置栈大小为:-Xss1m,方法调用26037了次

26037
java.lang.StackOverflowError
    at com.hu.demo.Demo2.recursion(Demo2.java:9)

如果设置栈大小为:-Xss2m,方法调用了48156次
48156
java.lang.StackOverflowError
    at com.hu.demo.Demo2.recursion(Demo2.java:8)

所以:-Xss设置决定函数调用的深度。

方法区参数

和堆一样,方法区是一块所有线程共享的内存,用于保存类的信息,可以保存多少信息,可以通过配置:

-XX:MaxPermSize ,默认64M,实际中,如果系统产生大量的类,就需要设置这个合适的参数,一面出现OOM。
-XX:sPermSize

直接内存配置

特别是在NIO编程中,直接内存跳过了java堆,使其java直接访问内存空间。

-XX:MaxDirechMemorySize,如果不设置,默认为最大堆空间-Xmx,直接内存到上限时就会触发垃圾回收。

垃圾回收的JVM配置

运行的垃圾回收器类型

配置

-XX:+UseSerialGC        串行垃圾回收器
-XX:+UseParallelGC        并行垃圾回收器
-XX:+UseConcMarkSweepGC        并发标记扫描垃圾回收器
-XX:ParallelCMSThreads=        并发标记扫描垃圾回收器 =为使用的线程数量
-XX:+UseG1GC        G1垃圾回收器

GC的优化配置

配置

-Xms        初始化堆内存大小
-Xmx        堆内存最大值
-Xmn        新生代大小
-XX:PermSize        初始化永久代大小
-XX:MaxPermSize        永久代最大容量

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

JDK8相关参数

1、由于jdk8开始,彻底去除永久区,所以永久区的参数就不用了。

-XX:PermSize
-XX:MaxPermSize

2、metaspace,元数据空间,专门用来存元数据的,jdk8里特有的数据结构用来替代perm

(1)CompressedClassSpaceSize参数作用是设置Klass Metaspace的大小,默认1G

Klass Metaspace就是用来存klass的,klass是的class文件在jvm里的运行时数据结构,没有开启压缩指针,就不会有CompressedClassSpaceSize这块内存,但是jdk1.8里应该是默认开启的,并且,如果这块内存会如果没有满会一直增加。

但是-Xmx超过了32G,压缩指针是默认不开启的,而这个参数也就失去了设置的意义。

(2)MaxMetaspaceSize

   默认基本是无穷大,这个参数很可能会因为没有限制而导致metaspace被无止境使用(一般是内存泄漏)而被OS Kill。这个参数会限制metaspace(包括了Klass Metaspace以及NoKlass Metaspace)被committed的内存大小,会保证committed的内存不会超过这个值,一旦超过就会触发GC,这里要注意和MaxPermSize的区别,MaxMetaspaceSize并不会在jvm启动的时候分配一块这么大的内存出来,而MaxPermSize是会分配一块这么大的内存的。

3.MaxDirectMemorySize

此参数主要影响的是非堆内存的direct byte buffer,jvm默认会设置64M,可根据功能适当加大此项参数,因为非堆内存,故而不会被GC回收掉,容易出现java.lang.OutOfMemoryError: Direct buffer memory错误
如出现以上错误,可通过以下参数打印log,之后用工具进行分析

-XX:-HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=logs/oom_dump.log

4、G1收集器参数

-XX:+UseG1GC
使用G1收集器
-XX:MaxGCPauseMillis=200
用户设定的最大gc 停顿时间,默认是200ms.
-XX:InitiatingHeapOccupancyPercent=45
默认是45,也就是heap中45%的容量被使用,则会触发concurrent gc

-XX:NewRatio=n
新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n    eden/survivor
空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n
提升年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n
设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:ConcGCThreads=n
并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
-XX:G1ReservePercent=n
设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
-XX:G1HeapRegionSize=n
使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.

目前jvm支持Client和Server两种运行模式,

使用参数-client可以指定使用client模式。

使用参数-server启动server模式。

查看当前jvm使用的模式java -version

区别:client比server启动较快,server启动较慢,如果是要对其进行复杂的系统性能信息收集和使用更复杂的算法对程序进行优化,一般都会启动server模式,长期运行的性能远远快于client模式。