运行时数据区概述
本文所有代码和介绍,基于 JDK 1.8.0.25
放上这个总结性的图,这个针对 hotspot 虚拟机运行时数据区所绘制的简图:
JVM 运行时数据区 - 多图预警、万字内存模型解读本文要介绍的就是这个图中的 运行时数据区 ,也就是常说的内存模型。
对于 java 程序员来说,在虚拟机自动内存管理机制的帮助下,不容易出现内存泄漏和内存溢出。
有虚拟机管理内存,这一切看起来都很美好。但是,也正因为java把内存控制的权力给了java虚拟机,一旦出现内存泄漏和溢出方面的问题。
如果不了解虚拟机是怎么样使用内存的,那么排查错误将会成为一项异常艰难的工作。
先把几个重要概念放上:
- 堆和栈算是Java内存模型中最重要的两部分,栈是运行时单位(解决程序执行问题),堆是存储单位(数据存储问题) 。
- PC 寄存器是用来存储指向下一条指令的地址,也就是即将要执行的指令代码。
- 虚拟机栈和本地方法栈分别管理 Java 方法和本地方法。 虚拟机每调用一个方法将会在栈中压入一个对应方法的栈帧 ,内部包含局部变量表、操作数栈、动态链接和方法返回地址。
- 虚拟机栈存在两种常见异常 StackOverflowError 和 OutOfMemoryError 。
- 堆分为年轻代(Eden区、Survivor 0/1 区),老年代。对象在 GC 发生时,在堆内各个区上分配空间和移动。
JVM 配置参数如下:
- -Xss 用于设置虚拟机栈空间大小,例如:java -Xss512M ,默认大小是 1M
- -Xms 用于表示堆区(年轻代+老年代)的起始内存大小,等价于 -XX:InitialHeapSize 。默认值为电脑物理内存大小 / 64 。
- -Xmx 用于表示堆区(年轻代+老年代)的最大内存,等价于 -XX:MaxHeapSize 。默认值为电脑物理内存大小 / 4 。
- -XX:NewRatio=2 表示年轻代和老年代占比分配为 1 : 2 ,这是默认配置比例。若修改为 3,则表示 老年代/年轻代 = 3。
- -XX:SurvivorRatio=8 表示年轻代中 Eden 区/一个Survivor区 ,默认占比为 8 : 1 : 1 。
- -XX :MaxTenuringThreshold=<N> 表示对象从 Survivor 区晋升至老年代的 age 阈值。
线程与内存模型
在 Hotspot JVM 里,每个线程都与操作系统的本地线程直接映射。
- 当一个 Java 线程准备好执行以后,此时一个操作系统的本地线程也同时创建。
- Java线程执行终止后,本地线程也会回收。
- 可以看看这篇,Java线程和操作系统线程的关系:Java Thread线程基础机制,源码解读
在运行时数据区区分了线程共享和线程私有。
至于原因嘛,后面会写到,到这先明确 虚拟机栈、本地方法栈、程序计数器 是线程私有的,所以生命周期和线程相同。
PC 寄存器
PC 寄存器(Program Counter Register),也就是上图中的程序计数器。
这个叫法更顺口,因为 Register 的命名源自 CPU 的寄存器,它存储指令相关的现场信息。
JVM 运行时数据区 - 多图预警、万字内存模型解读PC寄存器的作用:用来存储指向下一条指令的地址,也就是即将要执行的指令代码。由执行引擎读取吓一跳指令。
- PC 寄存器是一块很小的内存空间,几乎可以忽略不记,也是运行速度最快的存储区域。
- 在 JVM 规范中,每个线程都有它自己的 PC 寄存器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。 PC 寄存器会存储当前线程正在执行的Java方法的 JVM 指令地址:或者,如果是在执行 native 方法,则是未指定值( undefined)。
比如在字节码反编译文件中:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
复制代码
上述中左侧序号 0、3、5、8 就是指令地址,这些就是 PC 寄存器中存储的结构。
右侧则是虚拟机栈内的指令,这个以后再说。。。
PC 寄存器常见问题
- PC 寄存器没有 GC 和 OOM
PC 寄存器是唯一没有 OOM 的内存区域,没有 GC 的除了它还有虚拟机栈和本地方法栈。
- 使用 PC 寄存器存储字节码指令地址有什么用呢?为什么使用 PC 寄存器记录当前线程的执行地址呢?
因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM 的字节码解释器就需要通过改变 PC寄存器 的值来明确下一条应该执行什么样的字节码指令。
- PC 寄存器为什么会被设定为线程私有?
由于 CPU 时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
每个线程在创建后,都会产生自己的程序计数器和栈帧;这样的话,在线程中断或恢复中,程序计数器在各个线程之间可以互不影响。
虚拟机栈
首先看下总体性的概念:
- 虚拟机栈是什么 : 栈是线程私有的,每个线程在创建的时候都会创建一个虚拟机栈,其内部保存着一个个栈帧(Stack Frame),对应着一次次的 Java 方法调用。
- 虚拟机栈的作用 :主要管理 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
- 栈帧的创建时间 :当方法被执行的时候,虚拟机将会创建栈帧。
- JVM 对虚拟机栈的操作只有入栈(方法执行)和出栈(执行结束)。所以,栈也不存在垃圾回收。
虚拟机栈运行原理
在一条活动的线程中,一个时间点上,只会有一个活动的栈帧。
其实从上面那个图中很容易可以理解。
Java 方法有两种返回函数的方式(正常函数返回,使用 return 指令;抛出异常),不管****那种****方式,都会导致栈帧将执行结果返回上一个栈帧,并且当前栈帧被弹出。
下面开始分别讲解栈帧的内部结果。
局部变量表
局部变量表(local variables)也叫做局部变量数据、本地变量表。
它是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量;这些数据类型包括各类基本数据类型、对象引用(reference),以及 retuenAddress 类型。
到这里,就可以解释以前在多线程部分的一个提问:为什么局部变量不会存在线程安全问题?
太详细的就不解释了,写几个关键词示意一下:
虚拟机栈是线程私有、一个方法对应一个栈帧、方法内局部变量保存在虚拟机栈的局部变量表中、不同线程的栈不允许相互通信。
好嘞,然后把需要记的内容列一下,全是概念性的东西:
- 局部变量表所需的容量大小是在编译期就确定下来的,在运行期间是不会改变的。
- 方法嵌套调用的次数由栈的大小决定。栈越大,方法嵌套调用最大次数越多。
- 对一个方法而言,参数和局部变量越多,使得局部变量表膨胀,栈帧就越大;一个栈帧将占更多的栈空间,导致嵌套调用次数减少。
- 局部变量在使用前必须显示赋值。类变量会在加载过程的链接阶段经历准备阶段,这个阶段将会置为默认值,所以就算没有在初始化阶段进行复制,也不有问题。但是局部变量是在方法调用时创建,并没有默认赋值。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
操作数栈(Operand Stack,其实就是一个数组),在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈。
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
关于操作数栈的知识要点:
- 方法调用,创建栈帧的时候将会生成操作数栈(也就是一个数组);数组一旦创建长度就不可更改了,所以栈的深度在编译器就确定了。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。
动态链接
动态链接(Dynamic Linking)是指:每一个栈帧内部都包含一个指向 运行时常量池 中 该栈帧所属方法的引用 。比如:invokedynamic 指令。
在 Java 源文件编译到字节码文件中,所有变量和方法引用都将作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的, 动态链接的作用就是为了将这些符号转换为调用方法的直接引用 。
写个测试代码:
public class DynamicLinkingTest {
int num ;
public static void main(String[] args) {
DynamicLinkingTest dy = new DynamicLinkingTest();
dy.test();
}
public void test(){
dyTest();
}
public void dyTest(){
num++;
}
}
复制代码
编译后使用 javap -v DynamicLinkingTest.class 命令,显示:
Classfile /E:/test-demos/target/classes/jvm/DynamicLinkingTest.class
Last modified 2020年10月17日; size 645 bytes
MD5 checksum a4548dfdcf2a9d748f4e603d3bc7676a
Compiled from "DynamicLinkingTest.java"
public class jvm.DynamicLinkingTest
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // jvm/DynamicLinkingTest
super_class: #7 // java/lang/Object
interfaces: 0, fields: 1, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // jvm/DynamicLinkingTest
#3 = Methodref #2.#26 // jvm/DynamicLinkingTest."<init>":()V
#4 = Methodref #2.#28 // jvm/DynamicLinkingTest.test:()V
#5 = Methodref #2.#29 // jvm/DynamicLinkingTest.dyTest:()V
#6 = Fieldref #2.#30 // jvm/DynamicLinkingTest.num:I
#7 = Class #31 // java/lang/Object
#8 = Utf8 num
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Ljvm/DynamicLinkingTest;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 dy
#22 = Utf8 test
#23 = Utf8 dyTest
#24 = Utf8 SourceFile
#25 = Utf8 DynamicLinkingTest.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = Utf8 jvm/DynamicLinkingTest
#28 = NameAndType #22:#11 // test:()V
#29 = NameAndType #23:#11 // dyTest:()V
#30 = NameAndType #8:#9 // num:I
#31 = Utf8 java/lang/Object
{
int num;
descriptor: I
flags: (0x0000)
public jvm.DynamicLinkingTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/DynamicLinkingTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class jvm/DynamicLinkingTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method test:()V
12: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 dy Ljvm/DynamicLinkingTest;
public void test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method dyTest:()V
4: return
LineNumberTable:
line 16: 0
line 17: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/DynamicLinkingTest;
public void dyTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #6 // Field num:I
5: iconst_1
6: iadd
7: putfield #6 // Field num:I
10: return
LineNumberTable:
line 20: 0
line 21: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ljvm/DynamicLinkingTest;
}
SourceFile: "DynamicLinkingTest.java"
复制代码
- Constant pool 部分就是常量池,在加载时会放在方法区,也叫做运行时常量池,调用的目标就在这里。
- invokespecial 命令后面加了地址,比如:invokevirtual #4 ,后面的 #4 就是常量池中的地址。
- 这样做的目的也是节省资源,重复调用不必在线程独有的栈中创建,而是在线程共享的方法区。
方法返回值
方法返回值实际并不是一个值,而是这个方法返回地址,指向存放调用该方法的 PC寄存器的值。
它的作用就是回到调用方法位置,继续往下执行。
异常退出时,不会给他的上层调用者产生任何的返回值。
虚拟机栈两种异常
Java 虚拟机规范允许虚拟机栈的大小是动态的或者固定不变的。
所以,这分别将导致以下两种常见异常:
- 采用固定大小的虚拟机栈 :每一条线程的虚拟机栈容量在线程创建的时候独立选定。如果线程请求分配的容量超过虚拟机栈允许的最大容量,将会抛出 StackOverflowError 异常。
- 栈大小设置使用 -Xss 进行配置,例如:java -Xss512M ,默认大小是 1M
使用递归方法的时候,如果出现问题将进入死循环,每一次调用将会进行压栈,最后就会出现这个异常。
测试代码如下:
public class StackOverflowTest {
public static void main(String[] args) {
int i = 0;
recursion(i);
}
private static void recursion(int i){
System.out.println(i);
recursion(++i);
}
}
复制代码
抛出异常:
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.ext.DoubleByte$Encoder.encodeLoop(DoubleByte.java:617)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.PrintStream.write(PrintStream.java:526)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at jvm.StackOverflowTest.recursion(StackOverflowTest.java:15)
复制代码
可以根据修改 -Xss 来对比输出值的大小。
- 采用动态扩展的虚拟机栈 :在尝试扩展的时候无法申请到足够的内存,或者在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,那么将会抛出 OutOfMemoryError 异常。
本地方法栈
在将本地方法栈之前,先简单介绍下几个概念:
- Java 本地方法 :由非 Java 语言实现的方法(主要为C/C++),例如 Thread.start0() 。
- 本地方法接口 :也叫做 JNI ,作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序。
简单来说,就是为了和 JVM 所在操作系统交互,或者和硬件交互。
本地方法栈(Native Method Stack)是管理本地方法的调用; 和管理 Java 方法的虚拟机栈相似。
本地方法栈的异常和虚拟机栈相同,工作原理也相同,就略过了。
执行过程:
- 在调用本地方法是,在本地方法栈压入本地方法;
- 由动态链接指向本地方法库;
- 由执行引擎进行调用执行。
堆 Heap
堆是 Java 内存结构中最重要的部分,也是知识点最多的一部分。
这里涉及到垃圾回收将会讲的比较简要(因为还没有学到),以后开专题再详细讲。
JVM 运行时数据区 - 多图预警、万字内存模型解读还是一样,关于 JVM 部分都是先放概念:
- Java 堆区在 JVM 启动的时候就已经创建了,也确定了其空间的大小;
- 堆和方法区存在垃圾回收,堆是垃圾回收重点区域。GC 在大内存和频繁 GC 的情况下,将会影响性能;
- JDK7 之前堆内存在逻辑上分为:新生代(Young)、老年代(Old)、永久代(Perm);
- JDK8 后对堆空间逻辑上分为:新生代、老年代、元空间(Meta);
- 新生代又被分为伊甸园区(Eden)和幸存者0和1区(Survivor);永久代和方法区其实并不在堆内,而是方法区。
堆空间设置
Java 堆在 JVM 启动时就已经创建了,可以通过相关指令设置其大小:
- -Xms 用于表示堆区(年轻代+老年代)的起始内存大小,等价于 -XX:InitialHeapSize 。默认值为电脑物理内存大小 / 64 。
- -Xmx 用于表示堆区(年轻代+老年代)的最大内存,等价于 -XX:MaxHeapSize 。默认值为电脑物理内存大小 / 4 。
- 开发中建议将初始堆内存和最大堆内存设置成一个值;因为堆内存的扩容和释放将加大系统额外的压力。
年轻代和老年代
堆区的进一步划分可以分为如下结构:
JVM 运行时数据区 - 多图预警、万字内存模型解读年轻代和老年代默认占比分配为 1 : 2 ,默认配置为 -XX:NewRatio=2 。若修改为 3,则表示 老年代/年轻代 = 3.
一般情况下是不会修改这个比例的,只有我们明确知道对象的生命周期,才会针对进行更改。
而在年轻代中,Eden 和两个 Survivor 区的默认占比为 8 : 1 : 1 。
Survivor 0 和 1 区的因为需要相互复制,所以它们的空间大小是相同的。
修改年轻代和老年代空间占比的指令为 -XX:SurvivorRatio=8 ,相当于 Eden 区/一个Survivor区
对象在堆中的生命周期
对象在堆中的流程大致分为如下几个步骤:
- new 的对象先放 Eden 区。此区有大小限制。
- 当 Eden 区的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对 Eden 区进行垃圾回收(Minor GC),将 Eden 区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到 Eden 区。
- 然后将 Eden 区中的剩余对象移动到 Survivor 0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到 Survivor 0区的,如果没有回收,就会 放到 Survivor 1区。
- 如果再次经历垃圾回收,此时会重新放回 Survivor 0区,接着再去 Survivor 1区。 每一次在 Survivor 0 和 1区转移,都会为该对象的标志位 age 加一。 到达默认次数15后,下一次就可以晋升到老年代。 最大转移次数可以通过 -XX :MaxTenuringThreshold=<N> 进行设置。
- 在老年代,相对悠闲。当老年代内存不足时,再次触发GC: Major GC, 进行老年代的内存清理。
- 若老年代执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常
可得记住了,这是最正常的流程,后面还有其他特殊情况。。。再给你们放个示意图
不同颜色对应的区域参考上面的区域划分
JVM 运行时数据区 - 多图预警、万字内存模型解读以上步骤是正常情况下,当然不可能所有包含所有情况,也存在一些特殊情况。
- 当 Survivor 区满了,但是 Survivor 区内对象没有达到阈值,新对象也可以直接被晋升到老年代的。
- 遇到超大对象,新生代空间不够,则直接分配到老年代。
最后看下这个流程图,里面涉及的判断应该可以理解了。
JVM 运行时数据区 - 多图预警、万字内存模型解读最后提两句,各个 GC 之间的差别,详细的以后再讲:
- Minor GC :又叫做 YGC / Young GC,对新生代进行 GC。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收,性能耗费较小。 Eden 区满时才触发,Survivor 区满时不会触发。 Minor GC 会触发 STW(Stop the World,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互)。虽然 Minor GC 频率高,但是执行速度快,所以影响不大。
- Major GC :又可以成为 Old GC ,只收集老年代,频率很低。 老年代空间不足时,会先尝试触发 Minor GC,之后空间还是不足,则会触发 Major GC。 Major GC 的速度比 Minor GC 慢 10 倍以上,STW 时间相当长,所以要调优减少发生次数。
- Mixed GC :收集整个新生代和老年代,目前就只有 G1 GC。
- Full GC :又叫做 FGC ,收集整个堆和方法区的 GC。触发情况包括: 调用 System.gc() 时,系统建议执行 Full GC ,但不一定执行。 老年代空间不足、方法区空间不足。 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存。 由 Eden 区、Survivor 区复制时,对象大小大于 Survivor To区可用内存,则把该对象晋升到老年代,且老年代的可用内存小于该对象大小。
所谓的调优,就是让 GC 触发的次数尽量少,避免占用用户线程的资源。
TLAB - 线程本地分配缓存区
首先要弄明白的是,什么是 TLAB( Thread Local Allocation Buffer )?为什么要有 TLAB ?
JVM 运行时数据区 - 多图预警、万字内存模型解读这玩意儿就是 JVM 自带的,它设计了我们学就是了嘛。。。。
基本的原因和情况如下:
- 堆区是线程共享区域,并发环境下从堆区中划分内存空间是线程不安全的。
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
- JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 区。
- 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为 快速分配策略。
然后把 TLAB 的流程图放上:
JVM 运行时数据区 - 多图预警、万字内存模型解读关于 TLAB 的其他知识点如下:
- JVM 会将 TLAB 作为内存空间分配的首选,但并不一定能分配到 TLAB 内。
- -XX:UseTLAB 可以用来设置是否开启 TLAB 空间。
- 默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden空间的 1% 可以通过选项 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。
- 一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
方法区
先看一眼栈、堆、方法区的关系:
JVM 运行时数据区 - 多图预警、万字内存模型解读方法区的概念性知识和堆差不多:
- 方法区和 Java 堆一样,是线程共享的;在 JVM 启动时被创建,空间大小可以设置为固定也可以扩展。
- 方法区在逻辑上是堆的一部分,但一些简单的实现可能不会选择去进行垃圾回收或进行压缩。
- 方法区的大小决定了应用可以保存多少个类。如果应用定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: JDK7之前: java.lang.OutOfMemoryError:PermGen space JDK8之后: java.lang.OutOfMemoryError:Metaspace 加载太多第三方 jar 包、Tomcat 部署太多应用、大量动态的生成反射类;都将导致方法区 OOM 异常
永久代和元空间的本质区别是: 元空间不在虚拟机设置的内存中,而是使用本地内存。
啥是本地内存呢?
就是我们口语上的 8G、16G,这要是还能溢出我也是懵了。
所以对比元空间,永久代将更容易使 Java 应用产生 OOM 异常,即超过 -XX:MaxPermSize 的上限。
方法区参数设置
JDK7 及以前
- 通过 -XX:PermSize 来设置永久代初始分配空间大小,默认值 20.75M 。
- 通过 -XX:MaxPerSize 来设置永久代最大可分配空间,32位机器默认 64M ,64位机器默认 82M 。
- JVM 加载类信息容量超过了最大值,报 java.lang.OutOfMemoryError:PermGen space
JDK8 及以后
- 永久代的两个配置参数改为: -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize ,默认值分别为 21M 和 -1(没有限制)。
- 为了避免频繁地 GC ,建议将 -XX:MetaspaceSize 设置为一个较大的值。
方法区内部结构
方法区内部结构包括:类型信息、域(字段)信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区内还有一个非常重要的结构:运行时常量池 。
在 class 文件中,就有一个常量池表,包含了类名、方法名、参数类型、字面量等类型的符号引用。
字节码中直接调用常量池的信息,避免相同信息重复创建。
还有就是关于 StringTable 的位置:
- JDK7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 Full GC 的时候才会触发。而 Full GC 是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable 回收效率不高。
- 我们开发中会有大量的字符串被创建,回收效率低,会导致永久代内存不足,放到堆里能及时回收内存。
网友评论