精通JVM(一)——JVM内存结构
一.JVM内存结构概览
JVM在运行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。JVM内存结构如下图所示,总体而言包括5大区域:方法区、虚拟机栈、本地方法栈、堆、程序计数器。
JVM内存结构
二.各个击破五大区域
1.方法区
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在Java8中移除了永久代的内容,方法区由MetaSpace实现,并直接放到了本地内存中,不受JVM参数的限制。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
以下例子我们通过不断创建并加载新的类来触发OutOfMemoryError异常:
package cn.lsp.demo.jvm;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
public class MetaSpaceOOM extends ClassLoader{
public static void main(String[] args) {
int j = 0;
try {
MetaSpaceOOM test = new MetaSpaceOOM();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
在64位虚拟机上,我们使用如下虚拟机参数运行应用程序:
-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:-UseCompressedClassPointers
从输出中我们可以看到,随着对象的不断生成和加载,最终出现OutOfMemoryError: Metaspace异常
2022-03-19T21:06:49.549-0800: [GC (Metadata GC Threshold) [PSYoungGen: 27528K->1864K(76288K)] 27528K->1872K(251392K), 0.0015480 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2022-03-19T21:06:49.551-0800: [Full GC (Metadata GC Threshold) [PSYoungGen: 1864K->0K(76288K)] [ParOldGen: 8K->1652K(120832K)] 1872K->1652K(197120K), [Metaspace: 9372K->9372K(10240K)], 0.0064921 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2022-03-19T21:06:49.557-0800: [GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] 1652K->1652K(197120K), 0.0008775 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2022-03-19T21:06:49.558-0800: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 1652K->1633K(227328K)] 1652K->1633K(303616K), [Metaspace: 9372K->9372K(10240K)], 0.0098603 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
9344
Heap
PSYoungGen total 76288K, used 1966K [0x00000007aab00000, 0x00000007b2a80000, 0x0000000800000000)
eden space 65536K, 3% used [0x00000007aab00000,0x00000007aaceb9e8,0x00000007aeb00000)
from space 10752K, 0% used [0x00000007af580000,0x00000007af580000,0x00000007b0000000)
to space 10752K, 0% used [0x00000007aeb00000,0x00000007aeb00000,0x00000007af580000)
ParOldGen total 227328K, used 1633K [0x0000000700000000, 0x000000070de00000, 0x00000007aab00000)
object space 227328K, 0% used [0x0000000700000000,0x0000000700198738,0x000000070de00000)
Metaspace used 9404K, capacity 10208K, committed 10240K, reserved 10240K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at cn.lsp.demo.jvm.MetaSpaceOOM.main(MetaSpaceOOM.java:20)
Process finished with exit code 1
2.Java堆
Java堆也是被所有线程共享的一块内存区域,是JVM所管理的内存中最大的一块,它在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。因此它是垃圾收集器管理的主要区域,由于现在的垃圾手集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代还细分为Eden空间、From Survivor空间、To Survivor空间。如果没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常。
以下例子演示堆内存OutOfMemoryError异常:
package cn.lsp.demo.jvm;
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> objects = new ArrayList<>();
while (true) {
objects.add(new OOMObject());
}
}
}
我们使用如下虚拟机参数运行应用程序:
-Xms10M
-Xmx10M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
从输出中我们可以看到,随着在堆上不断创建对象分配内存,且无法回收,最终出现OutOfMemoryError: Java heap space异常
2022-03-19T21:42:43.994-0800: [GC (Allocation Failure) [PSYoungGen: 1926K->498K(2560K)] 1926K->822K(9728K), 0.0013184 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2022-03-19T21:42:43.999-0800: [GC (Allocation Failure) [PSYoungGen: 2546K->496K(2560K)] 2870K->2220K(9728K), 0.0023827 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2022-03-19T21:42:44.003-0800: [GC (Allocation Failure) [PSYoungGen: 2304K->512K(2560K)] 4029K->4038K(9728K), 0.0033042 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2022-03-19T21:42:44.007-0800: [GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 6086K->6080K(9728K), 0.0034903 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2022-03-19T21:42:44.011-0800: [Full GC (Ergonomics) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 5584K->4868K(7168K)] 6080K->4868K(9728K), [Metaspace: 3135K->3135K(1056768K)], 0.0623826 secs] [Times: user=0.36 sys=0.01, real=0.06 secs]
2022-03-19T21:42:44.074-0800: [GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 6916K->6947K(9728K), 0.0028029 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2022-03-19T21:42:44.077-0800: [Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6435K->5964K(7168K)] 6947K->5964K(9728K), [Metaspace: 3152K->3152K(1056768K)], 0.0458218 secs] [Times: user=0.34 sys=0.00, real=0.05 secs]
2022-03-19T21:42:44.123-0800: [Full GC (Ergonomics) [PSYoungGen: 1513K->479K(2560K)] [ParOldGen: 5964K->6927K(7168K)] 7478K->7407K(9728K), [Metaspace: 3174K->3174K(1056768K)], 0.0676921 secs] [Times: user=0.48 sys=0.01, real=0.07 secs]
2022-03-19T21:42:44.191-0800: [Full GC (Allocation Failure) [PSYoungGen: 479K->479K(2560K)] [ParOldGen: 6927K->6909K(7168K)] 7407K->7389K(9728K), [Metaspace: 3174K->3174K(1056768K)], 0.0627649 secs] [Times: user=0.49 sys=0.00, real=0.06 secs]
Heap
PSYoungGen total 2560K, used 545K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 2048K, 26% used [0x00000007bfd00000,0x00000007bfd887f8,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 7168K, used 6909K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 96% used [0x00000007bf600000,0x00000007bfcbf6c0,0x00000007bfd00000)
Metaspace used 3213K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at cn.lsp.demo.jvm.ch01.HeapOOM.main(HeapOOM.java:14)
Process finished with exit code 1
3.虚拟机栈
与方法区、Java堆不同,Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行是同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈和出栈的过程。比如下面的例子,fun1调用fun2,fun2调用fun3,fun3创建Hello对象。
public void fun1() {
fun2();
}
public void fun2() {
fun3();
}
public void fun3() {
Hello hello = new Hello();
}
虚拟机栈
虚拟机栈存在两种异常情况:如果线程请求的栈深度大于虚拟机所运行的深度,将抛出StackOverflowError异常;如果虚拟机栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
下面代码演示虚拟机栈StackOverflowError异常:
package cn.lsp.demo.jvm;
public class StackSOF {
private int stackDeep = 1;
public void stackLeak(){
stackDeep++;
stackLeak();
}
public static void main(String[] args) {
StackSOF sof = new StackSOF();
try {
sof.stackLeak();
} finally {
System.out.println("栈深度:" + sof.stackDeep);
}
}
}
运行结果如下,当栈深度到15944(不同机器可能不同)时就会出现StackOverflowError异常:
栈深度:15944
Exception in thread "main" java.lang.StackOverflowError
at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
... ...
下面代码演示虚拟机栈OutOfMemoryError异常:
package cn.lsp.demo.jvm;
public class StackOOM {
public void stackLeacByThread(){
while (true) {
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
public static void main(String[] args) {
StackOOM oom = new StackOOM();
oom.stackLeacByThread();
}
}
我们使用如下虚拟机参数运行应用程序:
-Xss2M
运行结果如下,通过不断创建线程,最终在栈上出现OutOfMemoryError异常:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at cn.lsp.demo.jvm.StackOOM.stackLeacByThread(StackOOM.java:14)
at cn.lsp.demo.jvm.StackOOM.main(StackOOM.java:21)
4.本地方法栈
本地方法栈与虚拟机栈所发生的作用几乎是一样的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常,此处不再赘述。
5.程序计数器
程序计数器是一块较小的内存区域,它可以看做是当前线程执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java是多线程的,在线程切换回来后,它需要知道原先的执行位置在哪里。就需要用程序计数器来记录这个执行位置的,保证线程间的计数器相互不影响,这个内存区域是线程私有的。
来一个简单的代码,计算(1+2)并返回
package cn.lsp.demo.jvm;
public class HelloWorld {
public int add() {
int a = 1;
int b = 2;
return a + b;
}
}
这段代码经过编译再加载到虚拟机的时候,就变成了以下的字节码,虚拟机执行的时候,就会一行行执行。
william@liushipingdeMacBook-Pro Demo % javac JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld.java
william@liushipingdeMacBook-Pro Demo % javap -c JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld
警告: 二进制文件JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld包含cn.lsp.demo.jvm.HelloWorld
Compiled from "HelloWorld.java"
public class cn.lsp.demo.jvm.HelloWorld {
public cn.lsp.demo.jvm.HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
}
三.总结
JVM整体工作流程如下:
1.通过类加载器将类加载到方法区;
2.创建对象时,会在堆内存中创建;
3.线程调用方法的时,会创建一个栈帧;
4.执行方法前,读取方法区的字节码执行指令;
5.执行指令时,会把执行的位置记录在程序计数器中;
6.方法执行完,这个栈帧就会出栈;