深入理解 Java 虚拟机
Java 运行时数据区包含哪几个部分? 哪些是共享的,哪些是私有的?
jvm的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java 8 虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
- 程序计数器(Program Counter Register)
- Java虚拟机栈(Java Virtual Machine Stacks)
- 本地方法栈(Native Method Stack)
- Java堆(Java Heap)
- 方法区(Methed Area)
私有
程序计数器,是一块较小的内存空间,记录的是正的是正在执行的虚拟机字节码指令的地址。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,完成程序的流程控制。
JVM多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在一个确定的时刻,一个处理器(或者说多核处理器的一个内核)只会执行一条线程中的命令。因此,为了正常的切换线程,每个线程都会有一个独立的程序计数器,各线程的程序计数器不会互相影响。这个私有的程序计数器所占的这块内存即是线程的“私有内存”。
Java 虚拟机栈由一个个栈帧组成,每个方法被执行时,Java 虚拟机都会同步创建一个栈帧。一个方法从开始被调用到执行完毕,对应的就是 一个栈帧在虚拟机中 入栈到出栈的过程。
栈帧 Stack Frame,用于存储 局部变量表,操作数栈,动态连接,方法出口等信息。
? 局部变量表:
? 存放编译期间可知的 各种 Java 虚拟机基本数据类型(int, boolean, char, short, byte, long, float, double),对象引用和 returnAddress 类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中以局部变量槽来表示,在编译期间,局部变量所需的空间就会完成分配,动态运行期间不会改变所需的空间。
为虚拟机使用到的本地方法 native 方法服务,其余的同 Java 虚拟机栈。
公有
Java堆是被所有线程共享的一块区域,Java堆在虚拟机启动时创建,是垃圾收集器管理的内存区域。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
根据Java虚拟机规范,堆所在的物理内存区间可以是不连续的,只要逻辑连续就可以。实现时既可以是固定大小,也可以是可扩展的。如果堆无法扩展时,就会抛出OutOfMemoryError。
方法区用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
方法区存在于永久代,可以说方法区是接口,是规范,永久代是 HotSpot 虚拟机给出的实现,
? 运行时常量池
? 运行时常量池是方法区的一部分,Class文件中有类的版本,字段,方法,接口等描述信息和用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池具有动态性,运行期间也可以将新的常量放入。
对象的创建过程
类加载检查:
当 Java 虚拟机遇到一条字节码 new 指令时,首先将去检查这个指令后面的参数也就是类名,是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载【也就是这个类是否被加载】,解析和初始化过,如果没有,那么就要先执行相应的类加载过程。
分配内存:
在类加载完成后,对象所需要的内存大小完全确定,可以对新生对象分配内存。
- 两种方式:如果内存规整,就是指针碰撞,将指针向空闲空间方向移动与对象大小相等的距离,如果内存不规整,那就是空闲列表,维护一个列表,记录哪些内存块可用,在分配时,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
注意:这种情况需要考虑对象在虚拟机的创建中是否是频繁的,因为在多线程的情况下是不安全的,比如对象A正在使用一个指针位置划分内存区域,指针还没来得及修改,对象B也开始划分内存,那就是使用原来的指针位置,就会发生问题
? 二、把分配内存的动作按照不同的线程分配在不同的空间中运行,即为每个线程分配一小块内存,叫做本地线程分配缓存(Thread Local Allocation Buffer).。那么每个线程的分配内存空间就在各自的TLAB中进行。(就是把堆中未分配的内存再分配给不同的线程)初始化内存空间,将分配到的内存空间(对象头除外)全部分配为 0 值,这步保证了对象的实例字段在 Java 代码中可以不赋值就直接使用,使得程序可以访问到这些字段的数据类型所对应的零值。
初始化内存空间
将分配到的内存空间(对象头除外)全部分配为 0 值,这步保证了对象的实例字段在 Java 代码中可以不赋值就直接使用,使得程序可以访问到这些字段的数据类型所对应的零值。
进行对象头的设置
在对象的对象头中保存一些必要的信息:这个对象是哪个类的实例,如何才能找到类的元数据信息, 对象的哈希码,对象的GC 分代年龄等信息。
init
上面的过程全部执行完,在JVM一个对象已经产生。在java中< init>方法还没有执行执行, < init>也就是构造函数,按照程序员的意愿初始化对象,执行完一个对象就真正产生了
对象内存布局
一个对象分为以下几个部分: 对象头,实例数据,对齐填充
对象头 Header:
对象头包括两类数据:对象自身的运行时数据和类型指针。
-
**运行时数据:**如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。 -
第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例; -
如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。
实例数据
实例数据是对象真正存储的有效信息,也就是我们在程序代码中所定义的各种类型的字段内容,包括从父类继承的。 这部分的存储顺序会受到 HotSpot 虚拟机默认的分配策略参数和字段在 Java 源码中定义顺序的影响。
对齐填充
对齐填充其实不是必然的,作用就是在占位符,保证对象的起始地址是 8 字节的整数倍 。也就是说,任何对象的大小都必须是 8 字节的整数倍,这个是 HotSpot 虚拟机的自动内存管理系统的要求。
对象的访问的两种方式
Java 通过栈上的 reference 数据来操作堆上的具体对象。
对象访问的两种方式为句柄和直接指针,
- 句柄方式,Java 堆中将可能会划分除一块内存来作为句柄池,然后 reference 储存句柄池的地址,句柄中包含了指向对象实例数据的指针,和对象类型数据的指针。
优缺点:
句柄方式比较稳定:Java栈本地变量中的reference中存储的时稳定的句柄地址,对象被移动时,只会改变句柄中的实例数据指针,不需要修改 reference 本身。
mg-zcfe9vSe-1636549257833)]
优缺点:
句柄方式比较稳定:Java栈本地变量中的reference中存储的时稳定的句柄地址,对象被移动时,只会改变句柄中的实例数据指针,不需要修改 reference 本身。
直接指针的话,少了一次指针定位的开销,所以好处就是访问速度更快。
|