彻底搞懂java内存模型图文详解

先画个图,但凡有点java基础的,这个图估计都看吐了,没办法

关于上面的几个概念,这里不详解了,自己google下,接下来将从代码层面来分析下java程序运行时,内存分配具体流程,看一段代码

public class Math {

    public static final Integer CONSTANT_1 = 111;
    public static Object obj = new Object();
    
    public int math(){
        int a = 1;
        int b = 2;
        int c = (a+b)*10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        System.out.println(math.math());
    }
}

我们都知道jvm要运行java代码,首先要把java代码转换成jvm可识别的字节码,也就是xxx.class的文件;jvm加载字节码,然后再根据字节码进行对象的创建,方法的调用;

所以掌握字节码是我们绕不过去的一个坎,说到字节码,大家第一印象可能是

cafe babe 0000 0031 0020 0a00 0500 1309
0014 0015 0a00 1600 1707 0018 0700 1901
0006 3c69 6e69 743e 0100 0328 2956 0100
0443 6f64 6501 000f 4c69 6e65 4e75 6d62
6572 5461 626c 6501 0012 4c6f 6361 6c56
6172 6961 626c 6554 6162 6c65 0100 0474
6869 7301 0006 4c54 6573 743b 0100 046d
6169 6e01 0016 285b 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0100 0461
7267 7301 0013 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b01 000a 536f 7572
6365 4669 6c65 0100 0954 6573 742e 6a61
7661 0c00 0600 0707 001a 0c00 1b00 1c07
001d 0c00 1e00 1f01 0004 5465 7374 0100
106a 6176 612f 6c61 6e67 2f4f 626a 6563

额,这种16进制的数字,看了脑壳儿疼;其实我们可以将这些字节码转换成可识别好理解的命令行,具体操作如下

javap -c Math.class >>Math.txt

查看下生成文件

Compiled from "Math.java"
public class Math {
  public static final java.lang.Integer CONSTANT_1;

  public static java.lang.Object obj;

  public Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int math();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: aload_1
      12: invokevirtual #5                  // Method math:()I
      15: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      18: return

  static {};
    Code:
       0: bipush        111
       2: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: putstatic     #8                  // Field CONSTANT_1:Ljava/lang/Integer;
       8: new           #9                  // class java/lang/Object
      11: dup
      12: invokespecial #1                  // Method java/lang/Object."<init>":()V
      15: putstatic     #10                 // Field obj:Ljava/lang/Object;
      18: return
}

感觉像是代码结构的样子,其实这就是一行行的字节码的指令,jvm根据字节码指令执行代码逻辑,但是还是看不懂;别急,我们看下math方法字节码指令是什么意思

public int math();
  Code:
     0: iconst_1  //将int类型常量1压入栈
     1: istore_1  //将int类型值存入局部变量1
     2: iconst_2  //将int类型常量2压入栈
     3: istore_2  //将int类型值存入局部变量2
     4: iload_1   //从局部变量1中装载int类型值
     5: iload_2   //从局部变量2中装载int类型值
     6: iadd      //执行int类型的加法
    7: bipush        10   //将一个8位带符号整数压入栈
    9: imul      //执行int类型的乘法
    10: istore_3  //将int类型值存入局部变量3
    11: iload_3   //从局部变量3中装载int类型值
    12: ireturn   //从方法中返回int类型的数据

这些字节码指令代表的含义可以通过https://blog.csdn.net/weixin_39372979/article/details/80846834这篇博客查询;看到这里,

感觉还是一头雾水,很正常,接下来我们就根据这些字节码指令从头分析下jvm在执行这些字节码指令时,变量如何赋值,方法如何调用;

java程序在执行前,首先需要将字节码加载到方法区,静态变量和常量也在此时初始化

例如这段代码

public static final Integer CONSTANT_1 = 111; 
public static Object obj = new Object();

对应的字节码指令

  static {};
    Code:
       0: bipush        111                 //将一个8位带符号整数压入栈
       2: invokestatic  #7                  //调用类(静态)方法 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: putstatic     #8                  //设置类中静态字段的值 Field CONSTANT_1:Ljava/lang/Integer;
       8: new           #9                  //创建一个新对象 class java/lang/Object
      11: dup                               //复制栈顶部一个字长内容
      12: invokespecial #1                  //调用类(静态)方法 Method java/lang/Object."<init>":()V
      15: putstatic     #10                 //设置类中静态字段的值 Field obj:Ljava/lang/Object;
     18: return

逐行解析下这些指令的意思

先看下第0行字节码指令:bipush 111,将一个8位带符号的的整数要入操作数栈,需要指明的是这个操作数栈是类加载器线程所拥有;

2: invokestatic #7 ,调用Integer.valueOf()静态方法,因为CONSTANT_1为Integer对象,所以就会存在装箱,拆箱操作,这里的意思就是CONSTANT_1 = Integer.valueOf(1)

5: putstatic #8,用public static修饰符来修饰变量CONSTANT_1

如上这几行字节码指令对应的代码片段就是

public static final Integer CONSTANT_1 = 111;

我们可以看到0~5,中间其实省略了一些步骤,例如之前111入栈,其实在publicstatic之前,需要出栈获得111,并将变量存入静态变量表中

8: new #9 ,在堆区创建一个Object class 对象

11: dup ,赋值栈顶部的一个字节长度内容,也就是将这个字节长度内容当作对象引用

12: invokespecial #1,调用Object的静态方法init构造对象,也就是默认的构造函数,此过程是使用了反射,中间省略了obj引用指向新构造的对象

15: putstatic #10 ,将obj设置为public static

在执行完如上操作后,jvm运行是内存变化如下

上面只是完成了类的加载,以及静态变量的初始化等操作,接下来才是真正程序调用,我们知道java程序执行的入口函数为main;

jvm进程会分配一个线程来执行main方法,这个线程也就是我们常说的主线程,也就是程序的主干,看下main方法

public static void main(String[] args) {
    Math math = new Math();
    System.out.println(math.math());
}

jvm进程分配线程去执行main函数时,会将main方法封装成一个栈帧压入Java栈,这里要注意的是每个线程都会有自己独有的Java 栈,这个栈是线程私有的,其他线程无法访问,还有就是每当发生方法调用的时候,只要是在同一个线程内,方法栈帧就会压入同一个java栈;接下来我们看下main方法第一行,创建了一个Math对象,创建对象时,jvm首先会在方法区找到Math的类信息模版,然后根据类模版在堆区创建对象,栈帧内保存对象的引用并且执行堆中对象,第一行代码执行完,jvm内存结构如下

main函数的第二行 调用math对象的math() 方法,之前讲过,每当方法调用就会创建栈帧压入Java栈,压栈后如下图

接下来我们重点介绍下math方法内部执行流程,先来回顾下java栈组成部分

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口

先看下math方法

public int math(){
    int a = 1;
    int b = 2;
    int c = (a+b)*10;
    return c;
}

其对应的字节码

public int math();
  Code:
     0: iconst_1  //将int类型常量1压入栈
     1: istore_1  //将int类型值存入局部变量1
     2: iconst_2  //将int类型常量2压入栈
     3: istore_2  //将int类型值存入局部变量2
     4: iload_1   //从局部变量1中装载int类型值
     5: iload_2   //从局部变量2中装载int类型值
     6: iadd      //执行int类型的加法
    7: bipush        10   //将一个8位带符号整数压入栈
    9: imul      //执行int类型的乘法
    10: istore_3  //将int类型值存入局部变量3
    11: iload_3   //从局部变量3中装载int类型值
    12: ireturn   //从方法中返回int类型的数据

0: iconst_1,将int类型常量1压入操作数栈,这里的栈需要说明下,是栈帧自己内部的栈结构,区别于线程的Java 栈

1: istore_1,将int类型值存入局部变量1,

这两步行字节码指令其实就是int a = 1;那在java栈中是怎么实现的,看下图

iconst_2和istore_2指令

iload_1 //从局部变量1中装载int类型值

iload_2 //从局部变量2中装载int类型值

从变量表中获得a,b的值并压入操作数栈

iadd //执行int类型的加法

执行这个指令的时候会将操作数栈的2和1弹出,并且执行加法操作,并且将结果压入操作数栈

bipush 10 //将一个8位带符号整数压入栈

imul //执行int类型的乘法

istore_3 //将int类型值存入局部变量3

iload_3 //从局部变量3中装载int类型值

到这里math方法就执行完了,接下来就是从方法出口回到方法被调用的地方,执行完这部后,math方法的栈帧就会从主线成的Java栈中弹出

栈帧中有一个部分一直没有讲,就是动态链接,这里引用一篇博客给出的定义,说的很透彻

一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?

需要解析成相应的直接引用,利用直接引用来准确地找到。

举个例子,就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个给这个地址起了个别名叫A, 以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。

这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

到这里Java栈部分就讲完了,在jvm的内存区域中,还有一部分和java栈相似的内存区域:本地方法栈,之前Java栈将的都是一些java方法调用的实现,在java程序中有很多调用本地的方法,这部分方法是用c语言实现的,举个例子:

例如Thread.start()方法就是调用的本地方法start0

private native void start0();

执行引擎在执行到本地方法的时候,会把本地方法封装成栈帧放到本地方法栈中,这点和java栈类似,接着执行引擎会会通过本地方法接口,调用c语言类库去执行,这些底层类库类似于xxx.dll这种文件;

程序计数器:程序计数器保存着程序执行的位置,也就是字节码的行号

7: bipush

这里的7就是字节码的行号,执行完一行字节码指令,程序计数器就会更新,并指向下一条字节码指令的行号;

注意:

java栈,本地方法栈,程序计数器都是线程私有的,而且这部分内存区域不存在GC,只要线程一结束,栈空间就会被释放,也就是说这部分内存的生命周期和线程是一致的

最后讲下堆

先看下jdk1.7之前的结构图

首先堆内存分为年轻代,老年代,在jdk1.7之前在逻辑上永久代划分到堆区,但是使用的是非堆内存空间(永久代大小设置例子:-XX:PermSize:8M);年轻代Young占整个堆内存的1/3,老年代Tenured占堆内存的2/3;年轻代继续细分的话,又分为Eden和两个Survivor;其中Eden大小占年轻代的8/10,survivor占1/10;年轻代为什么这么划分,我们可以从GC回收流程来分析下:

1.每当创建新对象,首先会在Eden空间中创建,当Eden 空间不足以分配创建对象,就会在Eden触发Minor GC

2.Minor GC会把Eden还存活的对象复制到Survivor,此时该Survivor角色为from,剩余的那个Survivor为to角色

3.Eden存活的对象复制到Survivor时,有两种情况,一种就是Survovor空间足以存下所有Eden存活的对象,另一种就是空间不够;

对于第一种情况,GC就退出了,第二种情况Survivor也会触发一次MinorGc,并把存活的对象复制到另外一个Survivor,此时两个survivor角色互换;当然此时也有一种情况,Minor Gc后Survivor还是空间不够,此时在两个Survivor之间来回GC 复制对象,当GC次数达到一定阈值时,还存活的对象会被移动到老年代;

4.当越来越多的对象被移动到老年代,最终老年代空间满了,这是就会触发Full GC;Full GC 会出现线程暂停,停滞的情况(STW,stop the world)

5. 重复1-4这几个步骤

jdk.1.7之后,永久代逻辑上从对空间移除,取而代之的是MetaData;之前永久代使用的是JVM的内存,而元空间使用的是直接的物理内存,还有一点要注意的是,元空间和永久代是方法区的具体实现,方法区是逻辑上的一个抽象概念

发布了118 篇原创文章 · 获赞 37 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/woloqun/article/details/87904168