JVM内存模型简介

cb9d379c5b7c406197fbae7515034b1c.jpgJVM(Java Virtual Machine)又被分为三大子系统,类加载子系统,运行时数据区,执行引擎。在这里我们主要讲解一下JVM的运行时数据区,也就是我们常说的JVM存储数据的内存模型。在这里提一点,平常我们常说内存模型,其实在Java中存在两大内存模型,一个是JVM的内存模型,也就是堆,栈之类的。还有一个是Java线程工作的内存模型,java工作的内存模型指的是主内存,工作内存,两个是不同的概念

JVM的结构图

​ 这里重点讲一下JVM的运行时数据区,也就是我们所说的JVM内存模型。其实像JVM结构中的类加载子系统,也能够讲出很多东西出来。

​ 首先,JVM的内存模型分为线程私有,线程共享的区域

线程私有部分

​ 图中所画的部分程序计数器,虚拟机栈,本地方法栈是每个线程私有的部分,大概讲述一下每个部分所起到的作用

​ 程序计数器:线程在运行期间,由于会出发CPU时间片资源抢夺的情况,假如线程A执行到if判断,循环,异常处理等这些字节码指令,时间片突然被抢占,出现阻塞,程序计数器会帮助记录每个线程所执行到下一条字节码指令,等线程A再次拿到时间片继续执行。

​ 虚拟机栈:每个线程在执行每个方法时,都会创建一个栈帧,栈帧里面又会包括一些局部变量表,操作数栈,方法出口等信息,每个方法从执行到执行完成,也就是完成我们的入栈和出栈过程

​ 首先看一段代码,来讲解栈帧中每个部分的作用

public class Test {

    public static void test() {

        int a = 1;

        int b = 2;

        int c = (a + b) * 10;

    }

    public static void main(String[] args) {

        test();

    }

}

​ 以上代码对应的虚拟栈图

​ 局部变量表:在我们程序中,方法中定义的一些局部变量都会存放在我们的局部变量表,也就是我们代码中的a,b,c这种局部变量

​ 将上述代码用javap -c Test命令可以查看到程序的字节码指令

public class com.ezhiyang.crm.config.Test {

  public com.ezhiyang.crm.config.Test();

    Code:

       0: aload_0

       1: invokespecial #1 // Method java/lang/Object."<init>":()V

       4: return

  public static void test();

    Code:

       0: iconst_1 // 将一个常量1加载到操作数栈

       1: istore_0 // 将一个数值(也就是常量1)从操作数栈存储到局部变量 表,也就是我们代码中的int a = 1

       2: iconst_2

       3: istore_1

       4: iload_0

       5: iload_1

       6: iadd // 执行加法操作

       7: bipush // 将10入操作数栈 10

       9: imul // 执行乘法操作

      10: istore_2 // 再将乘法出来的结果从操作数栈存储到局部变量 表,也就是我们的int c = (a + b) * 10

      11: return

  public static void main(java.lang.String[]);

    Code:

       0: invokestatic #2 // Method test:()V

       3: return

}

​ 操作数栈:像我们的程序后面都会被编译成这种字节码指令,而操作数栈将变量之间的运算入栈,然后存储计算结果,再出栈赋值给局部变量表

​ 方法出口:代码中main()方法调用了test()方法,这时test()执行完成后,需要继续执行main()方法,方法出口记录了test()方法执行完成后的一个出口,也就是回到main()

​ 本地方法栈:在Java代码中我们有时可以看到用native修饰的方法,而这些方法并不由Java语言去实现,而是由Java去调用底层的C++语言实现的,跟我们的虚拟机栈有点类似,只不过是执行native方法的线程栈帧

线程共享部分

​ 线程共享的部分就是我们图中所画的堆,方法区(又叫做永久代,JDK1.8后被改为元空间)

​ 看一张方法区内存分布图

​ 方法区:方法区主要存储的是虚拟机加载的类信息(版本,字段,方法,接口等信息),成员常量,静态变量等数据,其中又包括一部分是运行时常量池,运行时常量池存放的是编译期间生成的符号引用,后面经过解析出来的直接引用也会储存在常量池中。

​ 当虚拟机new指令时,首先会根据这个指令的参数在常量池中定位到一个类的符号引用,如果定位不到,则需要重新对这个类进行加载,解析和初始化了,这里就涉及了Class文件加载的7大过程。如果能找到符号引用,证明这个类已经加载过,并会给该对象分配内存空间,调用构造方法进行初始化。也就是我们new一个对象发生了那些事

​ JDK1.8后,方法区被叫做元空间,并且内存大小限制不限JVM的内存大小限制,而是直接使用计算机的直接内存,受计算机的内存大小限制

​ 堆:堆是JVM中最大的一块内存区域了,主要存放我们程序中new出来的对象,提一点并非所有的对象都是在堆上进行分配,随着线程逃逸,标量替换等技术的发展,对象也有可能在栈上进行分配。下面通过一段代码进行讲解,对象不一定在堆上进行分配

public class Test {

    public static void test() {

        A a = new A();

    }

    public static void main(String[] args) {

        test();

    }

}

class A {

    int a = 0;

}

像这段代码,执行test()方法时,new了一个A对象,但是A对象又只有一个基本类型变量a,A对象也没有出现线程逃逸现象,因为JVM存在一些指令优化功能,并会把A对象直接进行标量替换成int a = 0,这时就会出现对象在栈上进行分配。

堆也是GC进行回收最主要的区域,容易出现OutOfMemoryError异常

​ 堆中又分为young(新生代),old(老年代),分配的内存比例1:2。young新生代中的对象具有"朝生夕死"的特点,也就是对象刚创建不久,并会被回收掉。是Minor回收的主要区域。而老年代中的对象具有的特点则是不容易被回收掉,一些大对象比如数组并会存放在老年代中。是Full GC回收的主要区域。后面写一篇JVM GC垃圾回收的文章做介绍,这里不做过多的讲解了

​ young新生代中又分为eden区,两个survivor区,分配的内存比例8:1:1。我们程序中所new出的对象首先会在eden区进行分配,然后经过Minor GC,eden区没有被回收的对象又会被放到survivor Form区,survivor Form区经过GC后的对象没有被回收掉,又会转移到survivor To区,这两个区的对象是可以双向转移的。新生代采用GC的算法是复制算法,两个survivor区正是复制算法所要预留出来的区域

猜你喜欢

转载自blog.csdn.net/weixin_57763462/article/details/131495615