Java JVM内存结构,区域划分

时间:2022-11-25 15:10:25

先贴一个网上还有一些书上用的很多的一张图:

Java JVM内存结构,区域划分

我们平时多多少少都知道java内存结构主要由 两部分组成。可见这是我们平时最关心的两个部分。

先引用一下《深入理解Java虚拟机》一书中的解读:我们平时所说的堆就是上图中画的堆(Heap)。而我们平时说说的栈指的是上图的虚拟机栈(VM Stack)那我们先看一下Java内存最主要的这两部分,虚拟机栈(VM Stack)和堆(heap)

一、虚拟机栈(VM Stack)

每一个线程都有一个自己的虚拟机栈,不同线程之间无法访问到其他线程的虚拟机栈
虚拟机栈中存储的单位是每一个方法在执行时都会创建一个栈帧用于存储当前方法的局部变量表,操作数栈,动态链接,方法出口等。

Java JVM内存结构,区域划分

1.局部变量表 
1) 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用类型(reference类型,注意不是对象本身,可能是一个指向对象起始地址的指针,对象本身存储在堆)和returnAddress类型(指向了一条字节码指令的地址)
2) 成员由方法参数方法内部定义的局部变量组成,其容量用Slot作为最小单位,大小为32位。所以64位的long和double类型的数据会占用两slot,其余的数据类型占用1slot 
3) 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。 
4 )局部变量表所需要的内存空间是在编译器就完成分配的,当进入一个方式时这个方法需要在帧中分配多大的局部变量空间是完全确定的,在运行期间不会改变局部变量表大小 
5 )如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error,这也是为什么要注意一些递归操作。 

2.操作数栈 
1 )后入先出(LIFO)栈。当一个方法开始执行时,它的操作数栈是空的。在方法执行过程中,会有各种字节码指令往操作数栈写入和提取内容。 
2 )在概念模型中,两个栈帧是完全独立的。但大多虚拟机实现都会做优化,让两个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈与上面的栈帧的部*部变量表重叠在一起,无须进行额外的参数复制。 

3.动态链接 
每个栈帧都包含一个指向运行时常量池(后面会介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

4.方法返回地址 
当方法开始执行后,有两种方式退出。一是遇到方法返回的字节码指令;二是遇到异常并且这个异常没有在方法体内得到处理。无论哪种退出方式,方法退出之后都要返回到方法被调用的位置。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是通过异常处理器表来确定的,栈帧中一般不会保存此信息。 
方法退出的过程就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,修改PC计数器的值以指向后一条指令等。

二、java堆(Heap)

1.Java堆和栈不一样,堆是被所有线程共享的一块内存区域。
2.堆存放的是所有对象的实例以及数组
3.从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代,再细分一点就有
Eden空间、From Survivor空间、To survivor空间等。
那么堆和栈的主要区别通俗点说就是:对于每一个线程,都会创建自己的线程栈,其他线程是访问不到的,而对于堆,是对于所有线程都可见的。栈主要存放指向对象的引用和基本数据类型,而堆存放对象的具体实体

三、方法区(Method Area)和运行时常量池(Runtime constant pool)

方法区也是线程共享的区域,用于存储已经被虚拟机加载的类信息,常量,静态变量等,注意这里和堆中存放的内容做好区分,堆存放的是类的实例数据,而方法区存的是类的类型数据,比如类名称,类中方法的信息等

运行时常量池是方法区的一部分,存放的是Class文件常量池的符号引用信息。要了解运行时常量池要先了解一下Class文件常量池的概念

我们知道jvm加载的是Class文件,Class文件中保存了一个常量池,可以理解为Class文件之中的资源仓库,class文件常量池主要存放两大类:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为finla的常量值等。
而符号引用则属于编译原理方面的概念,包括下面三个常量:
1.类和接口的权限类名
2.字段的名称和描述符
3.方法的名称和描述符

关于这个符号引用(Symbolic References),再做一下解释:Java代码在进行编译的时候,并不像C和C++那样有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态链接,也就是说,Class常量池中的符号引用并不保存各个方法字段类等信息的最终内存位置,当虚拟机运行时,从常量池中获得这些符号引用,再在类创建或运行时解析、翻译找到具体的内存地址

运行时常量池就是把Class文件常量池加载进内存中来,进行上面的描述的动态链接,将符号引用和具体内存地址关联起来,刚开始时运行时常量池里中的链接也都还是符号引用(Symbolic References),跟在Class文件里一样,边运行边就会把用到的符号引用转换成直接链接。

四、程序计数器:

程序计数器是一块很小的区域,他可以看作是当前线程所执行的字节码的行号指示器

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以在任何时候一个处理器(对于多核处理器指一核)都只会执行一条线程中的指令。因此为了能切换后能恢复到正确的位置,每个线程都要有一个独立的程序计数器。——《深入理解java虚拟机》

五、直接内存:

JDK1.4加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,Buffer有个方法是allocateDirect(int capacity) ,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。——《深入理解java虚拟机》



参考:

《深入理解java虚拟机》

JVM Internals