深挖JVM相关

首先梳理JVM结构,放图

 JVM(hotspot版本)由堆、虚拟机栈、本地方法栈、方法区、程序计数器组成。其中堆和方法区是线程共享的,剩下的几个部分都是线程私有。

线程共享堆用来存放java中创建的对象示例,因此设计的占用内存最大。同时也是垃圾收集器(GC)的重点关照对象,主要分为两大部分,新生代和老年代(分代回收算法)。新生代包括Eden、From Servivor、To Survivor三部分。堆几乎包含了所有对象示例(部分实例可以在栈上分配内存)举例:  堆在物理内存上可以是不连续的。

方法区线程共享,主要用来存储类信息、常量(池)、静态变量、即时编译器编译后的代码。方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,类型的卸载,条件相当苛刻(什么条件?),但是回收是有必要的。

程序计数器线程私有,占用内存小,不会发生OOM异常(为什么不会?)程序计数器用来记录程序执行到哪一步的位置状态,这个地方和CPU切换线程后的逻辑有关系,因为线程需要记录上一次执行的位置在哪里。CPU在每个时刻只能执行一条指令也就是多线程到CPU的级别其实也是串行的(单核,多核心可以做到并行),所以每一个线程都有一个自己的程序计数器记录位置互不影响。如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Native方法,这个计数器值为空。

虚拟机栈线程私有,用来存储局部变量包括八种基本类型和对象引用类型和方法返回地址(字节码指令地址,用来返回方法调用后的位置,无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。),如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈:线程私有,本地方法栈(Native Method Stacks)与虚拟机栈作用相似,也会抛出StackOverflowError和OutOfMemoryError异常。区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。

HotSpot内存布局:

对象头

对象头记录了对象在运行过程中所需要使用的一些数据:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳。

对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。

实例数据

实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。

对齐填充

用于确保对象的总长度为8字节的整数倍。HotSpot VM的自动内存管理系统要求对象的大小必须是 8字节的整数倍。而对象头部分正好是8字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用。

对象创建过程:在HotSpot虚拟机中对象的内存布局分为以下3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

直接指针访问方式

引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址.

 

 

HotSpot内存布局在HotSpot虚拟机中对象的内存布局分为以下3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)


对象头
对象头记录了对象在运行过程中所需要使用的一些数据:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳。
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。
实例数据
实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
对齐填充
用于确保对象的总长度为8字节的整数倍。HotSpot VM的自动内存管理系统要求对象的大小必须是 8字节的整数倍。而对象头部分正好是8字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用。
HotSpot对象的操作对象的创建过程类加载检查
虚拟机在解析.class文件时,若遇到一条new指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。
为新生对象分配内存
对象所需内存的大小在类加载完成后便可完全确定,接下来从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式指针碰撞和空闲列表
指针碰撞如果Java堆中内存绝对规整(说明采用的是“复制算法”或“标记整理法”),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为“指针碰撞”。
空闲列表如果 Java堆中内存并不规整,已使用的内存和空闲内存交错(说明采用的是标记-清除法,有碎片),此时没法简单进行指针碰撞, VM 必须维护一个列表,记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例。这种方式称为“空闲列表”。
初始化
分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。至此,整个对象的创建过程就完成了。
对象的访问方式所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的。也就是说在建立一个对象时两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。 那么根据引用存放的地址类型的不同,对象有不同的访问方式。
句柄访问方式
堆中需要有一块叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
引用类型的变量存放的是该对象的句柄地址(reference)。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。


直接指针访问方式
引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。


需要说明的是,HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。
posted @ 2023-05-08 17:16  DreamCatt  阅读(15)  评论(0)    收藏  举报