EchoDemo's Blogs

JVM内存结构、Java内存模型和Java对象模型

休对故人思故国,且将新火试新茶,诗酒趁年华。—-苏轼《望江南》

JVM内存结构

我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下:

"java内存结构"

以上是JVM规范,不同的虚拟机实现会各有不同,但是一般会遵守规范。根据JVM规范,JVM把内存划分成了这几个区域(其中,方法区和堆是所有线程共享的):

1.方法区(Method Area)

2.堆区(Heap)

3.虚拟机栈(JVM Stack)

4.本地方法栈(Native Method Stack)

5.PC寄存器(The PC Register)

方法区(Method Area)

方法区存放了要加载的类的信息(如类名、修饰符等)、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。方法区是全局共享的,在一定条件下也会被GC(Garbage Collection)。当方法区超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。

在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation),一般来说,方法区上执行GC的情况很少,因此方法区被称为持久代的原因之一,但这并不代表方法区上完全没有GC,其上的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件相当苛刻而且困难。

如图中所示,运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常量和引用。一般来说,常量的分配在编译时就能确定,但也不全是,也可以存储在运行时期产生的常量。比如String类的intern()方法,作用是String类维护了一个常量池,如果调用的字符”hello”已经在常量池中,则直接返回常量池中的地址,否则新建一个常量加入池中,并返回地址。

堆区(Heap)

堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由所有线程共享,在虚拟机启动时创建。堆区主要用于存放对象实例及数组,所有new出来的对象都存储在该区域。

Java虚拟机栈(JVM Stack)

虚拟机栈占用的是操作系统内存,每个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。

局部变量表中存储着和方法相关的局部变量,包括各种基本数据类型及对象的引用地址等,因此他有个特点:内存空间可以在编译期间就确定,运行时不再改变。

虚拟机栈定义了两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足时,抛出OutOfMemoryError。

本地方法栈(Native Method Stack)

本地方法栈用于支持native方法的执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。

PC寄存器(The PC Register)

PC寄存器(The PC Register)是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念模型,各种JVM所采用的方式不一样。字节码解释器工作时,就是通过改变PC寄存器(程序计数器)的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。

每个PC寄存器只能记录一个线程的行号,因此它是线程私有的。如果程序当前正在执行的是一个java方法,则PC寄存器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,则寄存器的值为空,此内存区是唯一不会抛出OutOfMemoryError的区域。


Java内存模型

Java内存模型看上去和Java内存结构(JVM内存结构)差不多,很多人会误以为两者是一回事儿。在前面的关于JVM的内存结构的图中,我们可以看到,其中Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可能可以操作保存在堆或者方法区中的同一个数据。这也就是我们常说的“Java的线程间通过共享内存进行通信”。

Java内存模型(Java Memory Model)简称JMM。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,JMM是和多线程相关的,它描述了一组规则或规范,这个规范定义了一个线程在对共享变量进行写入时对另一个线程是可见的。

简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。在Java中,JMM是一个非常重要的概念,正是由于有了JMM,Java的并发编程才能避免很多问题。下面是JMM抽象示意图:

"java内存模型"


Java对象模型

我们都知道Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。这个关于Java对象自身的存储模型称之为Java对象模型。

在HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。下面是一个简单的Java对象的OOP-Klass模型,即Java对象模型。

"java对象模型"

从上图中可以看到,在方法区的instantKlass中有一个int a=1的数据存储。在堆内存中的两个对象的oop中,分别维护着int b=3,int b=2的实例数据。和oopDesc一样,instantKlass也维护着一些fields,用来保存类中定义的类数据,比如int a=1。


总结

最后,我们再来区分下JVM内存结构、 Java内存模型 以及Java对象模型这三个概念:JVM内存结构,和Java虚拟机的运行时区域有关。 Java内存模型,和Java的并发编程有关。Java对象模型和Java对象在虚拟机中的表现形式有关。

🐶 您的支持将鼓励我继续创作 🐶
-------------本文结束感谢您的阅读-------------