Java堆内存设置

查看系统默认内存设置

在Linux操作系统下,输入如下命令:

1
java -XX:+PrintFlagsFinal -version | grep HeapSize

在Windows操作系统下,输入如下命令:

1
java -XX:+PrintFlagsFinal -version | findstr HeapSize

JVM内存区域

image-20210928190347307

概要介绍:

  • 程序计数器是jvm执行程序的流水线,存放一些跳转指令。

  • 虚拟机栈是jvm执行java代码所使用的栈。

  • 本地方法栈是jvm调用操作系统方法所使用的栈。

  • 方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。

  • 虚拟机堆是jvm执行java代码所使用的堆。

程序计数器

占据一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此未来线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们成这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;

如果正在执行的是Native方法,这个计数器则为空(undefined)。

简单的说

就是代码执行的标号,比如代码执行完1跳转到5。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

  线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。

  其中64位长度的long和double类型的数据会占用2个局部变量表空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对此区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常
  • 如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别

  • 虚拟机栈为虚拟机执行Java方法(字节码)服务

  • 而本地方法栈则为虚拟机中使用到的Native方法服务

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。

方法区(永久代/元空间)

和堆一样所有线程共享,主要用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

在JDK1.7发布的HotSpot中,已经把字符串常量池移除方法区了。

绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError: PermGen space这个异常。这里的 PermGen space其实指的就是方法区。

不过方法区和PermGen space又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 PermGen space,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有PermGen space

由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

PermGen space既然是JVM的实现,那么也就存在了不同的实现

  • JDK1.7是PermGen(永久代)
  • JDK1.8 HotSpot 已经没有 PermGen space这个区间了,取而代之是一个叫做 Metaspace(元空间)

JDK1.7

移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。

但永久代仍存在于JDK1.7中,并没完全移除,

譬如

  • 符号引用(Symbols)转移到了Native Heap;
  • 字面量(interned strings)转移到了Java Heap;
  • 类的静态变量(class statics)转移到了Java Heap。

JDK1.8

JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,

但可以通过以下参数来指定元空间的大小

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

永久代=>元空间

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。

不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。

  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

常量池

JDK1.8中分类

  • 类文件常量池:又称为静态常量池,存储区域在堆中,编译时产生对应的class文件,主要包含字面量和符号引用;

  • 运行时常量池:存在元数据(Meta Space)空间,JVM运行时,在类加载完成后,将每个class常量池中的符号引用转换为直接引用

  • 字符串常量池:存在堆内存中,类在加载、验证、准备完成后在堆中生成字符串对象实例,然后将该字符串对象实例的引用只存储到Sting Pool中,String Pool是一个StringTable类,是哈希表结果,里面存储的是字符串引用,具体的实例对象存储在堆中,这个stringtable表在每个hotspot中的实例只有一份,被所有类共享。

注意

  1. JDK1.7 之前版本字符串常量池运行时常量池都位于方法区。
  2. JDK1.7 版本字符串常量池位置从方法区搬到了中; 运行时常量池还在方法区
  3. JDK1.8 HotSpot永久代被元空间(Metaspace)取代, 字符串常量池位置还在中, 运行时常量池位置变成了元空间(Metaspace)

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Java虚拟机对class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被jvm认可。但对于运行时常量池,Java虚拟机规范没做任何细节要求。

运行时常量池有个重要特性是动态性,Java语言不要求常量一定只在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制。当常量池无法再申请到内存时会抛出outOfMemeryError异常。

String.intern() 是一个 Native 方法,它的作用是:

如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用

如果没有,

  • JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用;

  • JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。

JDK1.6及以下版本

运行时常量池,由于运行时常量池在方法区中,我们可以通过jvm参数:-XX:PermSize-XX:MaxPermSize来设置方法区大小,从而间接限制常量池大小。

假设jvm启动参数为:-XX:PermSize=2M -XX:MaxPermSize=2M,然后运行如下代码:

1
2
3
4
5
6
7
8
9
//保持引用,防止自动垃圾回收
List<String> list = new ArrayList<String>();

int i = 0;

while(true){
//通过intern方法向常量池中手动添加常量
list.add(String.valueOf(i++).intern());
}

程序立刻会抛出:Exception in thread "main" java.lang.outOfMemoryError: PermGen space异常。

PermGen space正是方法区,足以说明常量池在方法区中。

在JDK8中,移除了方法区,转而用Metaspace区域替代,所以我们需要使用新的jvm参数:-XX:MaxMetaspaceSize=2M,依然运行如上代码,抛出:java.lang.OutOfMemoryError: Metaspace异常。同理说明运行时常量池是划分在Metaspace区域中。

按照官方的说法:

Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。

在JVM中堆之外的内存称为非堆内存(Non-heap memory)。

可以看出JVM主要管理两种类型的内存:堆和非堆。

简单来说堆就是Java代码可及的内存,是留给运行时使用的;

非堆就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

img

从上面的图可以看出, JVM区域总体分两类,heap区和非heap区。

heap区又分为:

  • Eden Space(伊甸园)
  • Survivor Space(幸存者区)
  • Old Gen(老年代)

非heap区又分:

  • Code Cache(代码缓存区)
  • Perm Gen(永久代)
  • Jvm Stack(Java虚拟机栈)
  • Local Method Statck(本地方法栈)

堆分布

Java进程运行过程中创建的对象存放在堆中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。

新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

  • 新生代 (Eden空间,From Survivor空间,To Survivor空间)
  • 老年代

堆的内存模型大致为:

Java堆

默认的:

1
2
NewRatio                 = 2  //老生代占2份 1:2
SurvivorRatio = 8 //Eden占8份 也就是 Edem : from : to = 8 : 1 : 1

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
老年代 ( Old ) = 2/3 的堆空间大小。

其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

From Survivor区域与To Survivor区域是交替切换空间,在同一时间内两者中会有一个为空。

GC Root

什么是gc root,JVM在进⾏垃圾回收时,需要找到“垃圾”对象,也就是没有被引⽤的对象,但是直接 找“垃圾”对象是⽐较耗时的,所以反过来,先找“⾮垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引⽤路径找到正常对象,⽽这些“根”有⼀个特征,就是它只会引⽤其他对象,⽽不会被其他对象引⽤,例如:栈中的本地变量、⽅法区中的静态变量、本地⽅法栈中的变量、正在运⾏的线程等可以作为gc root。

在Java语言中,可以作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象;

总结就是:

  • 方法运行时方法中引用的对象;
  • 类的静态变量引用的对象;
  • 类中常量引用的对象;
  • Native方法中引用的对象。

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lsof -i:8902 ###查看端口8902对应应用的pid
jstat -class PID ###类加载统计
jstat -compiler PID ###编译统计
jstat -gc PID ###垃圾回收统计
jstat -gccapacity PID ## 堆内存统计
jstat -gcutil PID ###查看堆比例
jstat -gccause PID ###GC的原因
jstat -gcnew PID ###新生代垃圾回收统计
jstat -gcnewcapacity PID ###新生代内存统计
jstat -gcold PID ###老年代垃圾回收统计
jstat -gcoldcapcacity PID ###老年代内存统计
jstat -gcmetacapacity PID ###元数据空间统计
jstat -printcompilation PID ###JVM编译方法统计
jmap -histo PID ###查看类的实例
jmap -heap PID ###查看堆栈信息
jmap -dump:live,format=b,file=/tmp/m.hprof PID ###保存内存的堆栈为文件
jhat -J-Xmx2048m -port 5000 /tmp/m.hprof ###在线查看堆文件的类,速度比较慢
jcmd PID GC.run ###强制gc

查看内存占用

查看pid

1
lsof -i:8902

查看内存占用

1
jmap -heap pid

结果

Attaching to process ID 19438, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 89128960 (85.0MB)
MaxNewSize = 715653120 (682.5MB)
OldSize = 179306496 (171.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 651689984 (621.5MB)
used = 405279136 (386.5043029785156MB)
free = 246410848 (234.99569702148438MB)
62.18894657739592% used
From Space:
capacity = 5242880 (5.0MB)
used = 4774176 (4.553009033203125MB)
free = 468704 (0.446990966796875MB)
91.0601806640625% used
To Space:
capacity = 14680064 (14.0MB)
used = 0 (0.0MB)
free = 14680064 (14.0MB)
0.0% used
PS Old Generation
capacity = 252182528 (240.5MB)
used = 38105624 (36.340354919433594MB)
free = 214076904 (204.1596450805664MB)
15.110334685835173% used

27080 interned Strings occupying 2811288 bytes.

查看某个进程的对象占用对象最大的命令:

1
jmap -histo pid | head -n 20

查看进程的可用内存占用百分比

1
jstat -gcutil pid

结果

S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
88.75 0.00 90.80 17.78 91.23 87.83 82 0.805 3 0.492 1.297
  • S0: From Survivor
  • S1: To Survivor
  • E: Eden
  • O: Old generation
  • M: 的值为91.23,即Meta区使用率也达到了91.23%

配置优化

查看JDK版本

1
java -version

推荐配置(JDK8)

-XX:MetaspaceSize=128m (元空间默认大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
-Xms1024m (jvm启动时分配的内存)
-Xmx1024m (jvm运行过程中分配的最大内存)
-Xmn256m (新生代大小)
-Xss256k (jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M)
-XX:SurvivorRatio=8 (新生代分区比例 8:2 默认就是这个比例)
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails (打印详细的GC日志)

注意点:

JDK8之后把-XX:PermSize 和 -XX:MaxPermSize移除了,取而代之的是
-XX:MetaspaceSize=128m (元空间默认大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
JDK 8开始把类的元数据放到本地化的堆内存(Native Heap)中,这一块区域就叫Metaspace,中文名叫元空间。
使用本地化的内存有什么好处呢?最直接的表现就是java.lang.OutOfMemoryError: PermGen 空间问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大(貌似容量还与操作系统的虚拟内存有关?这里不太清楚),这解决了空间不足的问题。不过,让Metaspace变得无限大显然是不现实的,因此我们也要限制Metaspace的大小:使用-XX:MaxMetaspaceSize参数来指定Metaspace区域的大小。JVM默认在运行时根据需要动态地设置MaxMetaspaceSize的大小。

启动(JDK7及以下)

1
nohup java -Dfile.encoding=utf-8 -jar -Xms256m -Xmx2048m -XX:PermSize=128M -XX:MaxPermSize=256M /data/123.jar >/data/log.txt &

启动(JDK8及以上)

1
nohup java -Dfile.encoding=utf-8 -jar -Xms256m -Xmx2048m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m /data/123.jar >/data/log.txt &

示例

1
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
  • -XX:NewRatio=4 :设置新生代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则新生代与年老代所占比值为1:4,新生代占整个堆栈的1/5
  • -XX:SurvivorRatio=4 :设置新生代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个新生代的1/6
  • -XX:MaxPermSize=16m :设置持久代大小为16m。
  • -XX:MaxTenuringThreshold=0 :设置垃圾最大年龄。如果设置为0的话,则新生代对象不经过Survivor区,直接进入年老代 。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则新生代对象会在Survivor区进行多次复制,这样可以增加对象再新生代的存活时间 ,增加在新生代即被回收的概论。

配置详解

  1. 堆设置

    • -Xms : 初始堆大小;

    • -Xmx : 最大堆大小;

    • -XX:MaxnewSize: 表示新生代可被分配的内存的最大上限;当然这个值应该小于 -Xmx的值;

    • -XX:NewRatio=3: 设置新生代和年老代的比值。

      如:为3,表示新生代与年老代比值为1:3,新生代占整个新生代年老代和的1/4;

    • -XX:SurvivorRatio=3 : 新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。

      如:3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5;

    • -XX:NewSize=n : 设置新生代大小,应该小于 -Xms的值;

    • -Xmn256m: 至于这个参数则是对新生代 -XX:newSize-XX:MaxnewSize两个参数的同时配置,

      也就是说如果通过-Xmn来配置新生代的内存大小,那么XX:newSize = XX:MaxnewSize = Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的。

    • -Xss256k: jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

  2. 非堆设置

    JDK7及以前

    • -XX:PermSize=128M 表示非堆区初始内存分配大小,其缩写为permanent size(持久化内存)
    • -XX:MaxPermSize=256M 表示非堆区最大内存分配大小

    JDK8及以后

    • -XX:MetaspaceSize=128m (元空间默认大小)
    • -XX:MaxMetaspaceSize=128m (元空间最大大小)

    建议

    1. MetaspaceSizeMaxMetaspaceSize设置一样大;
    2. 具体设置多大,建议稳定运行一段时间后通过jstat -gc pid确认且这个值大一些,对于大部分项目256m即可。
  3. 收集器设置

    • -XX:+UseSerialGC :设置串行收集器
    • -XX:+UseParallelGC :设置并行收集器
    • -XX:+UseParalledlOldGC :设置并行年老代收集器
    • -XX:+UseConcMarkSweepGC :设置并发收集器
  4. 垃圾回收统计信息

    • -XX:+PrintGC
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:filename
  5. 并行收集器设置

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

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