JVM内存区域详解
# JVM内存区域详解
# 1 运行时数据区
Java内存区域就是运行时数据区。
按照线程私有和共享进行区分的话。私有部分包含:程序计数器、虚拟机栈、本地方法栈;共享部分包含:堆、方法区
- 程序计数器:线程私有,用于记录当前线程下条指令的位置,不会因为线程的切换而忘记当前线程所执行到的位置;分支、循环、跳转、异常处理、线程恢复都需要程序计数器来完成。程序计数器占的空间非常小,并且不会出现
OutOfMemeryError
,他会随着线程创建而创建,线程的结束而结束。 - 虚拟机栈:线程私有,由一个个栈帧组成,每一个栈帧对应一个方法,每一个方法的调用到执行完成对应着栈帧的入栈和出栈,每个栈帧都包含局部变量表、操作数栈、动态链接、方法返回地址。会出现
StackOverFlowError
和 OutOfMemeryError` 两种错误。 - 本地方法栈:线程私有,虚拟机栈执行的是Java方法,本地方法栈则使用虚拟机的Native方法。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息。也会出现
StackOverFlowError
和OutOfMemoryError
两种错误。 - 堆:线程共享,存放对象实例,几乎所有对象实例以及数组都在这里分配内存。堆主要分为两部分:新生代(Eden、Survivor)和老生代(Old),分为两部分有利于垃圾回收,因为垃圾回收的主要区域就是堆。
- 方法区:用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
# 1.1 程序计数器
线程私有,用于记录当前线程下条指令的位置,不会因为线程的切换而忘记当前线程所执行到的位置;分支、循环、跳转、异常处理、线程恢复都需要程序计数器来完成。程序计数器占的空间非常小,并且不会出现 OutOfMemeryError
,他会随着线程创建而创建,线程的结束而结束。
# 1.2 虚拟机栈
线程私有,由一个个栈帧组成,每一个栈帧对应一个方法,每一个方法的调用到执行完成对应着栈帧的入栈和出栈,每个栈帧都包含局部变量表、操作数栈、动态链接、方法返回地址。会出现 StackOverFlowError
和 OutOfMemeryError
两种错误。
StackOverFlowError
:线程请求的栈深度大于虚拟机所运行的最大深度。
OutOfMemoryError
:扩展栈容量无法申请到足够的内存时。HotSpot虚拟机不支持动态扩展栈,所以不会出现这个异常。
JVM 虚拟机栈的参数设置
-Xss
等价于 -XX:ThreadStackSize
设置方法:-Xss128k
,设置单个线程栈的大小,一般默认值为 512k ~ 1024k
# 局部变量表
主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
# 动态链接
主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
# 操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
# 方法返回地址
返回方法被调用的位置。
# 1.3 本地方法栈
线程私有,虚拟机栈执行的是 Java 方法,本地方法栈则使用虚拟机的 Native 方法。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息。也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
StackOverFlowError
:线程请求的栈深度大于虚拟机所运行的最大深度。
OutOfMemoryError
:扩展栈容量无法申请到足够的内存时。HotSpot虚拟机不支持动态扩展栈,所以不会出现这个异常。
# 1.4 堆
线程共享,存放对象实例,几乎所有对象实例以及数组都在这里分配内存。堆主要分为两部分:新生代(Eden、Survivor)和老生代(Old),分为两部分有利于垃圾回收,因为垃圾回收的主要区域就是堆。
堆最容易出现的就是 OutOfMemoryError
错误,并且出现这种错误之后的表现形式还会有几种,比如:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
: 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。
JVM 堆的参数设置
🔑 -Xms
等价于 -XX:InitialHeapSize
表示初始化堆大小,一旦对象容量超过堆的初始容量,JAVA 堆会自动扩容到 -Xmx 大小。默认值为物理内存的 1/64。
🔑 -Xmx
等价于 -XX:MaxHeapSize
表示堆可以扩展到的最大值,在很多情况下,通常将 -Xms 和 -Xmx 设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。默认值为物理内存的 1/4。
将堆内存设置为 20MB:-Xms20m -Xmx20m
# 1.5 方法区
用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- JDK1.8:在元空间内,直接使用内存,内存受系统的限制。
- JDK1.7:方法区与堆地址相连,实现方式是永久代
# 2 常见问题
# Java堆溢出
🔶 原理:Java堆用于存储对象实例,通过不断地创建对象,并且保证创建的对象不会被垃圾回收机制回收,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常
🔶 设置JVM参数:堆的最大值和最小值均设置为 1MB,通过参数 -XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在出现内存溢出异常的时候 Dump 出当前的内存堆转存快照以便事后分析
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
🔶 Java代码
public class OOMTest {
}
2
3
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
}
🔶 控制台输出
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid25616.hprof ...
Heap dump file created [2466309 bytes in 0.006 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2
3
4
异常为 java.lang.OutOfMemoryError: Java heap space
,表示堆溢出。
# 虚拟机栈和本地方法栈溢出
🔶 原理:通过递归不断调用,相当于往栈种不断加入栈帧,直到达到栈的最大值。
🔶 设置JVM参数:设置栈空间空间大小。
-Xss128k
🔶 Java代码
public class OOMTest {
public void dfs() {
dfs();
}
public static void main(String[] args) {
OOMTest oomTest = new OOMTest();
oomTest.dfs();
}
}
2
3
4
5
6
7
8
9
10
🔶 控制台输出
垃圾回收详情
[GC (Allocation Failure) [PSYoungGen: 509K->464K(1024K)] 509K->464K(1536K), 0.0012068 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 966K->432K(1024K)] 966K->432K(1536K), 0.0038625 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 938K->512K(1024K)] 938K->560K(1536K), 0.0023008 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1023K->498K(1024K)] 1071K->794K(1536K), 0.0010295 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 498K->468K(1024K)] [ParOldGen: 296K->248K(512K)] 794K->717K(1536K), [Metaspace: 3138K->3138K(1056768K)], 0.0057444 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 820K->820K(1024K)] 1069K->1325K(1536K), 0.0014325 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 820K->0K(1024K)] [ParOldGen: 504K->471K(512K)] 1325K->471K(1536K), [Metaspace: 3155K->3155K(1056768K)], 0.0113762 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 445K->432K(1024K)] [ParOldGen: 471K->471K(512K)] 916K->903K(1536K), [Metaspace: 3162K->3162K(1056768K)], 0.0092557 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 432K->432K(1024K)] [ParOldGen: 471K->461K(512K)] 903K->894K(1536K), [Metaspace: 3162K->3162K(1056768K)], 0.0101378 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
2
3
4
5
6
7
8
9
GC (Allocation Failure)
:因为内存分配失败导致垃圾回收,只有年轻代发生了垃圾回收,所以是 Minor GC
Full GC (Ergonomics)
:年轻代和老年代同时进行垃圾回收
错误信息
Exception in thread "main" java.lang.StackOverflowError
at test.OOMTest.dfs(OOMTest.java:18)
...后续异常堆栈信息省略
2
3
异常为 java.lang.StackOverflowError
,表示栈溢出。
# 方法区溢出
对于JDK1.6来说,字符串常量池存储在方法区中,所以我们可以通过不断的往字符串常量池中添加字符串使方法区溢出。
# 元空间溢出
🔶 原理:方法区的主要职责是用来存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。我们可以在运行时通过反射创建大量的类区填满方法区直到溢出为止。
🔶 设置JVM参数:方法区是在元空间内,所以需要设置元空间的最大值,单位为字节。
-XX:MaxMetaspaceSize=10240000
🔶 Java代码:需要引入Cglib依赖,通过Cglib反射创建对象
public class OOMTest {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
Object o = enhancer.create();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🔶 控制台输出
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
异常为 java.lang.OutOfMemoryError: Metaspace
,表示元空间溢出。
# 本地方法栈有什么用
本地方法栈与虚拟机栈所发挥的作用非常相似,区别只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则是为了虚拟机使用本地方法服务。
# 没有程序计数器会怎么样
Java 程序中的流程控制无法得到正确的控制,多线程也无法正确的轮换。
# 类存放在哪里
方法区,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
# 为什么要废弃永久代,引入元空间?
在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。
移除永久代是为融合 HotSpot VM 与 JRockit VM 而做出的努力,因为JRockit没有永久代,不需要配置永久代。永久代会为GC带来不必要的复杂度,并且回收效率偏低。
最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。