Java虚拟机中内存区域的划分

版权声明:转载请注明出处 https://blog.csdn.net/abc123lzf/article/details/83066360

本文内容根据《深入理解Java虚拟机》和《Java虚拟机规范(Java SE 8版)》总结

1、虚拟机运行时数据区

在这里插入图片描述

Java运行时数据区可以分为5个模块:方法区、堆、虚拟机栈、本地方法栈和程序计数器。

(1)程序计数器

程序计数器一个内存较小的内存区域,其值为当前线程所执行的字节码行号。程序计数器是线程隔离的,每个线程所属的内存区域都会持有一个程序计数器。JVM在执行字节码时,会改变这个计数器的值来选取下一个需要执行的字节码指令。

(2)虚拟机栈

Java虚拟机栈同样也是线程隔离的,每个线程都会持有一个虚拟机栈。线程在进入一个方法时都会创建一个栈帧,栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派。栈帧中存储了局部变量表、操作数栈、动态链接、方法出口信息,它随着方法调用而创建,随着方法结束而销毁。

局部变量表存储了编译期可预知的基本数据类型对象引用returnAddress(指向一条字节码指令地址),这三种数据类型就是Java虚拟机所支持的原始数据类型。对于long和double数据类型,会占用2个局部变量空间。

当线程请求的栈深度大于虚拟机允许的长度时,会抛出StackOverflowError异常,如果虚拟机栈允许动态扩展但仍然无法申请到内存空间存放栈帧,那么会抛出OutOfMemoryError异常。

(3)本地方法栈

本地方法栈服务于native方法,虚拟机规范对本地方法栈使用的语言、方式、数据结构没有强制规定,在HotSpot虚拟机中,虚拟机栈和本地方法栈是共用的。本地方法栈同样也会抛出StackOverflowError和OutOfMemoryError。

(4)Java堆

Java堆是线程共享的,虚拟机在启动时会自动创建。对于大多数应用来说,Java堆是JVM所管理的内存中最大的一块,供所有类实例数组对象分配的内存区域,这些对象都会被垃圾收集器管理,无需显式销毁。从内存回收角度来看,现在垃圾收集器基本采用分代收集算法,Java堆中还可以划分为:新生代老年代。再细致些可以划分为EdenFrom SurvivorTo Survivor空间。

如果堆中没有更多的内存空间创建对象,并且堆也无法动态扩展时,将抛出OutOfMemoryError。

(5)方法区

方法区同样也是线程共享的,在虚拟机启动时创建,和很多传统语言的编译代码存储区或者操作系统进程的正文段作用类似,它存储已被JVM加载的类结构信息(包含运行时常量池、字段和方法数据、构造函数、普通方法的字节码内容),还包括一些在类、实例、接口初始化时的特殊方法。

方法区是堆的一个逻辑部分,可以把方法区看成永久代。Java虚拟机规范说明方法区可以不实现垃圾回收,但是如果需要实现类的动态卸载,比如像Tomcat那样可以热部署代码,那么就需要Java虚拟机实现方法区的垃圾回收。

方法区回收无用的类前提是需要满足以下三点:
该类的所有实例已经被GC,即Java堆中不存在该类的实例。
加载该类的类加载器已经被回收。
该类所属的Class对象没有在任何地方被引用,无法在任何地方通过反射来访问该类。

即使上述条件都满足,JVM也不一定必然会去回收它。
以Tomcat为例,在Tomcat启动后,如果我们修改了其中的class文件,那么Tomcat会卸载掉加载这个Web应用的类加载器,并重新构造一个类加载器重新加载Web应用的资源。卸载掉的类加载器和加载的类会在适当的时候被JVM垃圾回收。

(6)运行时常量池

运行时常量池属于方法区的一部分,常量池用于存储编译期生成的各种字面量符号引用

字面量是用于表达源代码中一个固定值的表示法,比如int a = 3;那么3就是int类型的字面量。
符号引用相对于直接引用,符号引用以一组符号(可以理解为字符串)来描述所引用的目标。比如一个Person类需要引用一个Friend类,在编译阶段,Person类是不知道Friend类的实际内存地址的,只能用符号com.test.Friend表示。
而直接引用可以是一个直接指向目标的指针、相对偏移量、一个能间接定位到目标的句柄。

运行时常量池相对于class文件常量池一个特点就是具备动态性,在运行期间可以将新的常量放入池中,比如new一个String后调用它的intern方法。

同样,当常量池无法申请到内存存放常量时会抛出OutOfMemoryError。

(7)直接内存

直接内存不属于虚拟机运行时数据区,也不是虚拟机规范中定义的内存区域。

在Java NIO中引入了基于Channel和Buffer的I/O方式,它使用native方法分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

直接内存的分配不会受到Java堆大小的限制,如果无法分配到直接内存,那么会抛出OutOfMemoryError。


2、HotSpot虚拟机中的对象

(1)对象的创建

当虚拟机遇到new指令时,会检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载过。如果尚未加载,那么就启动类的加载过程。

类加载完成后,JVM将会对对象分配内存,并且将分配到的内存空间都初始化为零值(除了对象头)。
接下来,虚拟机将会设置对象头信息,包含这个对象属于哪个类、对象哈希码、GC分代年龄等信息。这些操作完成后,从JVM角度来看这个新的对象已经创建完成了,但从Java代码角度来看还没有完成,其构造方法还尚未执行。待构造方法执行结束后,这个对象才真正可用。

(2)对象在内存中的结构

对象在内存中存储两种数据:对象头实例变量的值

对象头包含两部分信息:
第一部分存储对象自身运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这些数据长度在32位JVM中占4字节,在64位JVM中占8字节。
第二部分存储类型指针,即对象指向它的类元数据指针,虚拟机可以通过这个指针确定这个对象属于哪个类的实例,比如调用实例的getClass方法。
如果对象属于数组类型,那么对象头中还有一个记录数组长度的数据,当调用数组的length获取长度时,实际上是通过arraylength指令来获取这个对象头的数据并加入到操作数栈返回给调用者。

实例数据才是对象存储的真正信息,无论是父类继承的,还是子类定义的,都需要记录在这个地方。这部分存储顺序受虚拟机分配策略参数和Java源码中定义顺序的影响。其默认分配策略为:long/double、int、short/char、byte/boolean、引用类型。父类的变量会排在子类变量前面。

建立对象后,我们需要通过栈帧的局部变量表来操作Java堆上的变量。对于HotSpot虚拟机来说,Reference类型是通过直接指针来访问Java堆中的对象的。

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/83066360