JVM内存区域划分详解

java虚拟机内存划分:

java虚拟机在执行java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在有些区域则依赖线程的启动和结束而结束和销毁。如下图所示:
在这里插入图片描述
为了增加单位时间内完成的任务量提高计算效率,我们可以在面向对象编程的基础上实现多线程编程。通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域主存(main memory),而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数),数据写入时,先被写入到工作缓存中,JVM选择合适的时机将其同步到主存中,以此提高访问效率。如下图所示:
在这里插入图片描述
其中每个线程的工作内存中都有自己的虚拟机栈、本地方法栈和程序计数器,并且所有线程共享堆内存和方法区。

1、线程隔离的数据区

线程的隔离区域有程序计数器、本地方法栈、虚拟机栈。

1)程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2)Java虚拟机栈

每一个线程都有一个栈,也就是虚拟机栈,栈中的基本元素我们称之为栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个栈帧都包括了一下几部分:局部变量表、操作数栈、动态连接、方法的出口信息 和一些额外的附加信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。其模型示意图大体如下:
在这里插入图片描述栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中。在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。需要注意的是一个栈中能容纳的栈帧是受限,过深的方法调用(无法申请到足够的内存)可能会导致StackOverFlowError,当然,我们可以人为设置栈的大小。

局部变量表:存放编译期间各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用类型(不等同于对象本身)和returnAddredd类型。

操作数栈: 不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问。Java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的而不是基于寄存器的。虽然指令也可以从其他地方取得操作数,比如从字节码流中跟随在操作码(代表指令的字节)之后的字节中或从常量池中,但是主要还是从操作数栈中获得操作数。虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

下面我们用简单的案例来解释一下JVM代码执行的过程,代码实例如下:

public class MainTest {
    public  static int add(){
        int result=0;
        int i=2;
        int j=3;
        int c=5;
        return result =(i+j)*c;
    }

    public static void main(String[] args) {
        MainTest.add();
    }
}

执行过程中代码、操作数栈和局部变量表的变化情况如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3)本地方法栈
本地方法栈与虚拟机栈发挥的作用非常相似,他们之间的区别只不过是虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用到的Native方法服务。


2、线程隔离的数据区域

1、Java堆

堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!
从内存分配的角度来看,java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。不过无论怎么划分,堆存放的都是对象实例。


2、方法区:

方法区又被称为静态区,是程序中永远唯一的元素存储区域。和堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1)运行区常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放。

如类中有下列语句:
String s = new String("abc")

1、首先Class被CLassLoader加载时,"abc"被作为常量读入,在constant  pool里创建了一个共享的"abc"  
2、当调用到new  String("abc")的时候,会在heap里创建这个new  String("abc")

类加载对一个类只会进行一次。"abc"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"abc"字符串被驻留过则不需要重复创建用于驻留的"xyz"实例)。驻留的字符串是放在全局共享的字符串常量池中的。
在这段代码后续被运行的时候,"abc"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1持有。
因此这条语句从开始到执行一共创建了两个对象。


参考目录
深入JVM字节码执行引擎:https://blog.csdn.net/dd864140130/article/details/49515403
Java之执行引擎:https://blog.csdn.net/qq_33938256/article/details/52584658
JVM内存区域与多线程:https://www.jianshu.com/p/ece1bb5fa88b
浅析JAVA中的TLAB:https://www.jianshu.com/p/8be816cbb5ed
Java堆内存和栈内训详解:https://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
深入理解Java虚拟机 --------------------周志明

发布了44 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42784951/article/details/99620975