JVM对象揭秘以及内存布局

下文我将从对象的创建和对象在内存中的布局两个方面介绍

1.对象的创建

java是一门面向对象的语言,java的世界里,无时无刻都有新的对象产生。在java语法层面上来看,新建一个对象就是java里面的new关键字。那么映射到JVM里面,对象是如何创建的呢。

首先虚拟机会查看新创建的对象是否可以在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经经过类的加载,解析,初始化的过程。如果没有,那么就必须进行类的加载等过程(这个以后会详细讲,主要就是把一个编译后的class文件经过一系列的检查和解析,然后把class文件解析后的数据加载到虚拟机的过程,这步会把常量放到常量池里面,并且在方法去存储类的一些符号引用,符号引用就是引用的字母,这部分我还在研究中,以后我会单独来写类的加载过程)。经过类的加载之后,就要真正的创建对象,首先需要做的就是给对象分配内存,而对象所需要的内存大小在类的加载过程中就会确定。

前面讲过,对象都是存储在堆里面的,而虚拟机是允许系统给虚拟机分配的堆是不连续的内存空间的。那么分配的内存的时候就会遇见两种不同的情况。

①如果内存是连续的。就是用过的内存在一边,没有用过的在另一边,中间有一条逻辑上的分界线。那么分配内存时只需要把分界线向空闲的方向移动对象的大小的距离就可以了。这种方式称为指针碰撞。

②如果内存是不连续的。就是用过的内存和没有用过的内存是交叉在一期的。这个时候虚拟机就需要知道哪些内存空间是空闲的,就是虚拟机要维持一个空闲列表记录空闲的内存区域。然后找到合适的内存分配给对象后,更新空闲列表,这种分配内存的方式成为空闲列表。

具体虚拟机上使用的是哪种分配方式取决与堆得空间是否连续,而这部分由取决与垃圾回收器是否会在GC之后对内存进行整理。如果回收器对内存进行了整理,那么就是用内存碰撞,否则就需要使用空闲列表。

java里面创建对象是很频繁的操作,那么如何在这段期间保持原子性呢?第一种解决方式就是CAS加上失败重试机制,第二种解决方案是TLAB,本地线程分配缓冲的方法(这个不是特别理解)。

内存分配之后,虚拟机会把分配的内存空间的初始值都设置为零(各个类型对应的零值),除了对象头。这一部分保证了对象的全局变量在不进行的初始化的时候也可以访问。最后就是根据代码上的逻辑给不同的全局变量赋上对应的初始值了,这样一个对象(程序员的女朋友)就被创建出来了。

2.对象的内存布局

在默认的HotSpot虚拟机上面,对象的内存通常分为三部分

内存划分
对象头 ObjectHeader
实例数据 instancedata
对齐填充 padding

①对象头

对象头包含两部分信息,一部分是存储对象自身运行时的数据,包括GC年龄,是否持有锁,哈希码等信息。

还有一部分就是存储的对象的一些静态信息,比如类型指针,就是对象指向他的类元数据的指针。虚拟机通过这个指针找到这个对象所属类的元数据信息。但是这个并不是固定的。这就涉及到对象的访问定位的方式问题。通产对象头的大小在32bit的虚拟机上大小就是32bit,在64bit虚拟机上就是64bit。注意如果这个对象是数组,那么一般还需要额外的32bit来记录数组的长度,因为根据数组的元数据无法去确认数组的大小。

  • 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。

  • 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。

  • 64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。

  • 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。

  • 静态属性不算在对象大小内。

②实例数据

实例数据存储的就是类本身的数据,一般情况来说就是类的成员变量。无论是从父类继承下来的还是本身的字段,都需要记录。这部分除了跟字段在代码中定义的顺序有关之外,还跟虚拟机的分配策略参数有关系。一般情况下,都是父类的参数在子类的前面,相同宽度的字段总是分配到一起(比如boolean和byte,double和long会优先分配到一起),还有就是会优先分配宽度较小的字段,这个主要是为了节约内存,跟补齐区域有关系。注意子类较小宽度的变量也有可能插在父类的变量之间。

③对齐填充

这部分区域并不是必然的。主要是以为HotSpot内存管理系统要求对象的内存之和需要时8字节的整数倍,所以在前两部分没有达到要求的时候便会出现对齐填充区域。

3.对象的访问方式

由于栈上的引用类型数据只是存了堆上数据的引用,但时java虚拟机规范并没有规定如何根据引用定位到具体的对象和找到对象的一些元数据。所以这个完全是根据虚拟机本身来实现的。现在市主要有两种访问方式

①.根据句柄访问:

可以看到堆里面需要维护一个句柄池,一个指向实例数据,一个指向对象所属类的元数据。栈上面的引用只是应用到句柄池就可以。

②根据指针访问

可以看到只有一个指针指向对象的实际内存区域,然后在根据对象的实际内存区域指向所属类的元数据。

上面两种方式各有优点和缺点。句柄访问好处是引用类型的句柄比较稳定,即使对象发生变化,也只是改变句柄池里面句柄的指向就可以,不需要改变引用类型的句柄,但是指针访问就需要改变引用类型的指针。但是它的确定也很明显,就是定位的时候对多一次句柄定位的开销。效率没有指针访问快。但是java是一个对象创建十分频繁的语言,所以对定位速度要求比较高,所以大部分虚拟机都是采用的指针访问。

猜你喜欢

转载自blog.csdn.net/qq_30055391/article/details/84800156