欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM内存结构解析

程序员文章站 2022-04-18 17:11:14
...

JVM的整体结构

这是Oracle官方对jvm内存的定义:docs.oracle.com/javase/spec…

内存的分布结构

虚拟机栈:每个线程独有的,虚拟机会为每个线程都开辟一块栈空间用来存放当前线程的每个方法在执行过程中的局部变量,当然,这里不仅仅只有局部变量,还有 操作数栈、动态连接、方法出口;
本地方法栈:和虚拟机栈类似,但是他是存放的native方法在执行过程中的局部变量,这部分很少用,不用去关注;
程序计数器:用来记录每个线程在进行上下文切换的时候,记录上次执行的符号引用,也是每个线程独有的;
方法区:在JVM规范当中叫方法区,但是在hotspot的jdk8版本是使用元空间(Metaspace)实现的,所以也可以叫做元空间,类在被加载的时候会把类元信息放到这里、类的静态变量、静态常量也会放到这里;
堆:堆是大家很熟悉的也是特别需要去关注的,存放的是new出来的对象,堆的内部结构主要有两部分构成:新生代和老年代,默认占比是1:2;新生代又分为1个eden区,2个suvivor区,默认占比为8:1:1;
image.png

内存的分配过程

通过如下代码生成class文件,详细分析一下在栈里面对象是怎么划分的:

public class Math {

public void compute() {

    int a = 10;
    int b = 5;
    int c = (a + b) * 100;
    System.out.println(c);
}
public static void main(String[] args) {
    Math math = new Math();
    math.compute();
}

}
复制代码
执行 javap -c Math.class > Math.txt 对class文件进行反编译并输出到Math.txt文件里面详细看一下:

Compiled from “Math.java”
public class com.demo.jvm.Math {
public com.demo.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""????)V
4: return

public void compute();
Code:
0: bipush 10
2: istore_1
3: iconst_5
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: bipush 100
10: imul
11: istore_3
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_3
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: return

public static void main(java.lang.String[]);
Code:
0: new #4 // class com/demo/jvm/Math
3: dup
4: invokespecial #5 // Method “”????)V
7: astore_1
8: aload_1
9: invokevirtual #6 // Method compute:()V
12: return
}
复制代码
反编译之后的结果就是jvm的指令码,每条指令码都有它特定的含义

我们对compute()方法的指令一条一条的分析:

bipush 10 将一个8位带符号整数压入栈:把10放入操作数栈;
istore_1 将int类型值存入局部变量1:把操作数栈里面的值放到局部变量表下标为1的位置(这里为什么是下标1而不是0呢?当栈帧被创建出来的时候,局部变量表中会默认存放一个this的引用,下标是0);
iconst_5 将int类型常量5压入栈:和第1步的意思一样,把5放入操作数栈;
istore_2 将int类型值存入局部变量2:把操作数栈里面的5放到局部变量表的下标为2的位置;
iload_1 从局部变量1中装载int类型值:把局部变量表中的10拿出来放到操作数栈里面;
iload_2 从局部变量2中装载int类型值:把局部变量表中的5拿出来放到操作数栈里面;
iadd 执行int类型的加法:把操作数栈里面的值加起来(10 + 5)= 15;
bipush 100 将一个8位带符号整数压入栈:把100放入操作数栈;
imul 执行int类型的乘法:把操作数栈里面的值乘起来(15 * 100)= 1500;
istore_3 将int类型值存入局部变量3:把操作数栈的1500放到局部变量表下标3的位置;
getstatic #2 从类中获取静态字段:这行指令是获取PrintStream对象的,就是System.out那段代码,后面的#2是符号引用,在常量池里面维护;
iload_3 从局部变量3中装载int类型值:把1500从局部变量表3的位置拿出来放到操作数栈里面;
invokevirtual #3 调度对象的方法:调用#3引用的对象的方法,#3也是符号引用,实际就是调用PrintStream.println参数就是操作数栈里面的1500;
return 方法执行完毕,return结束;
操作数栈:其实就是执行引擎将要进行操作的值,操作完之后就清除掉了,再次操作需要从局部变量表去加载;
局部变量表:就是要存放的局部变量的值(基本数据类型),如果是存放对象,则是对象在堆中的地址引用(这里也涉及到直接指针的概念,后面的内容会说到);
动态链接:代码在执行的过程中对应的符号引用转换为直接地址,且这个引用是会变的;上篇还说到了静态链接,这里要和静态链接区分一下,静态链接是在类加载的时候发生的,动态链接是在对象执行方法的时候发生的;
每个方法都对应一个栈帧,如果无限递归则会导致*Error,栈的默认大小为1M

方法区

使用上面的Math代码,执行javap -v Math.class 查看描述信息:

Classfile /E:/workspace/learn/blog-demo/target/classes/com/demo/jvm/Math.class
Last modified 2021-5-19; size 773 bytes
MD5 checksum 09a14d9236f6ceccf06184daa6cef7d9
Compiled from “Math.java”
public class com.demo.jvm.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#29 // java/lang/Object.""????)V
#2 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #32.#33 // java/io/PrintStream.println:(I)V
#4 = Class #34 // com/demo/jvm/Math
#5 = Methodref #4.#29 // com/demo/jvm/Math.""????)V
#6 = Methodref #4.#35 // com/demo/jvm/Math.compute:()V
#7 = Fieldref #4.#36 // com/demo/jvm/Math.math:Lcom/demo/jvm/Math;
#8 = Class #37 // java/lang/Object
#9 = Utf8 math
#10 = Utf8 Lcom/demo/jvm/Math;
#11 = Utf8
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 compute
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 c
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8
#27 = Utf8 SourceFile
#28 = Utf8 Math.java
#29 = NameAndType #11:#12 // “”????)V
#30 = Class #38 // java/lang/System
#31 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#32 = Class #41 // java/io/PrintStream
#33 = NameAndType #42:#43 // println:(I)V
#34 = Utf8 com/demo/jvm/Math
#35 = NameAndType #17:#12 // compute:()V
#36 = NameAndType #9:#10 // math:Lcom/demo/jvm/Math;
#37 = Utf8 java/lang/Object
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (I)V
{
public com.demo.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""????)V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/jvm/Math;

public void compute();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: iconst_5
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: bipush 100
10: imul
11: istore_3
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_3
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 5
line 11: 12
line 12: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/demo/jvm/Math;
3 17 1 a I
5 15 2 b I
12 8 3 c I

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #4 // class com/demo/jvm/Math
3: dup
4: invokespecial #5 // Method “”????)V
7: astore_1
8: aload_1
9: invokevirtual #6 // Method compute:()V
12: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 math Lcom/demo/jvm/Math;

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #4 // class com/demo/jvm/Math
3: dup
4: invokespecial #5 // Method “”????)V
7: putstatic #7 // Field math:Lcom/demo/jvm/Math;
10: return
LineNumberTable:
line 5: 0
}
SourceFile: “Math.java”
复制代码
Constant pool部分就是这个类的常量池,这些信息都是存放在方法区里面的,可以看到,每个指令都有对应的符号引用,这就是类元信息;使用的是直接内存,默认大小为21M,容量满了会触发FullGC,这部分的内存会进行动态调整,如果上次GC回收了大量的内存,则会自动调小,如果没有回收大量内存则会调大,但是最大不会超过设置的最大空间;

一般情况下,对象在被创建出来是存放在eden区,当eden区放满了之后会触发一次MinorGC,使用复制算法把剩余对象放到其中一个Survivor区,继续进行对象的 创建->清理,当年轻代的那些被GC了15次还没有回收的对象放入老年代,老年代一般情况下在无法存放对象的时候会进行一次FullGC(根据具体的垃圾回收器决定,CMS会通过一个并发清理的参数设置什么时候执行FullGC),这次GC的范围包含年轻代,老年代,元空间;如果在老年代没有回收出来可用空间则会直接抛出OOM。
可以跑下面这个demo,使用jvisualvm工具查看对象的分配过程

public class OOMTest {
private byte[] bytes = new byte[1024 * 1024];

public static void main(String[] args) {
    ArrayList<OOMTest> list = new ArrayList<>();
    for(;;) {
        list.add(new OOMTest());
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}
复制代码
本章相关jvm参数:

-XX:MetaspaceSize 元空间的默认大小
-XX:MaxMetaspaceSize 设置元空间的最大内存,如果不设置的话会一直扩容,直到直接内存溢出OOM,经验值设置为256M;
-Xss 每个线程的栈大小,默认为1M
-Xms 初始堆大小,默认物理内存的1/64
-Xmx 最大堆大小,默认物理内存的1/4
-Xmn 新生代大小
-XX:NewSize 设置新生代初始大小
-XX:NewRatio 默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio 默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存

路漫漫其修远兮,吾将上下而求索