深入理解JVM-JVM对象创建与内存分配机制-03-1

本文详细介绍了JVM中对象创建的五个步骤,包括类加载检查、内存分配、初始化零值、设置对象头和执行构造方法。在内存分配环节,讨论了指针碰撞和空闲列表两种分配策略,以及并发情况下如何使用CAS和TLAB确保线程安全。此外,还解释了对象头的组成、对象大小与指针压缩的关系,以及为何进行指针压缩的原因。最后,通过实例展示了对象在内存中的布局。
摘要由CSDN通过智能技术生成

下一章预告:深入理解JVM-JVM对象创建与内存分配机制-03-2

深入理解JVM-JVM对象创建与内存分配机制-03-1

对于JVM对象的创建流程,可能很多人对它的认识只是知道在new一个对象的时候分配空间。对于内存分配很多人也只是认为对象分配在堆空间,变量存放在栈空间。
在这里插入图片描述
在这里插入图片描述

对象的创建

我们先来看一下对象的创建流程主要有五个步骤:
在这里插入图片描述
1.类加载检查:前面文章讲过类加载机制,类加载器会加载类,那么在创建对象的时候就会进行类加载检查。 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

划分内存的方法:
  • 指针碰撞(Bump the Pointer)(默认用指针碰撞):如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
    在这里插入图片描述
    此时如果需要分配空间,分配的空间就是指针后面那一部分,指针再往后移动一格。
  • 空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录.
    在这里插入图片描述
    解决并发问题的方法:
    CAS(compare and swap)
    虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
    本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
    把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。默认为Eden区的1%。

3.初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。通俗点说就是定义int i时,给不给它初始值都会有这么一个初始化零值操作,int类型变成0,string类型变为null,各个不同的类型会初始化为他们对应的“零值”。

4.设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
32位对象头
在这里插入图片描述
64位对象头
在这里插入图片描述
对象头的锁标志位对应着锁状态,在对象没加锁时对象无锁态Mark word对应图中第一行,分代年龄4位最大值为15,刚好应证了上一篇文章所说的最大分代年龄为15,在对象使用锁的时候在并发情况下例如Synchronized它会随着并发情况而进行锁升级,比如竞争不激烈的时候你将会看到00轻量级锁标志位,当竞争比较激烈可能会升级成重量级锁那么对象头的锁标志位就变成了10。

类型指针(Klass Pointer):开启指针压缩占4字节,关闭压缩占8字节,它指向方法区(元空间)的类信息,代码信息等,Klass和Class是不一样的,Klass是指c++的指针c++实现。

数组长度:只有数组对象才有,占4位
对齐填充:保证对象是8字节的整数倍,为什么是8的整数倍,因为根据实践证明8字节整数倍会让对象的寻址效率达到最高。
5.执行< init >方法
执行< init >方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。


对象大小与指针压缩

对象大小可以用jol-core包查看,引入依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;

/**
 * 计算对象大小
 */
public class JOLSample {

    public static void main(String[] args) {
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());

        System.out.println();
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
    }

    // -XX:+UseCompressedOops           默认开启的压缩所有指针
    // -XX:+UseCompressedClassPointers  默认开启的压缩对象头里的类型指针Klass Pointer
    // Oops : Ordinary Object Pointers
    public static class A {
                       //8B mark word
                       //4B Klass Pointer   如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
        int id;        //4B
        String name;   //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
        byte b;        //1B 
        Object o;      //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
    }
}


运行结果:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)    //mark word
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)    //mark word     
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)    //Klass Pointer
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.jvm.JOLSample$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)
     12     4                int A.id                                      0
     16     1               byte A.b                                       0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String A.name                                    null
     24     4   java.lang.Object A.o                                       null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

第一个对象:
在这里插入图片描述
在上图中size的前8字节是对象头的Mark Word占64位,第三个4字节是Klass Pointer,第四个4字节则是对齐填充。
第二个对象
在这里插入图片描述
在这个图中前面三个4字节和第一个对象一样,第四个4字节则是数组的长度,由于该对象刚好为8的倍数,则没有对齐填充

第三个对象
在这里插入图片描述

前面三个4字节还是一样,第四个4字节表示id的类型int类型占四个字节,第五个1字节b变量的类型byte占一字节,第六个3三字节代码变量内部对齐填充(4字节的倍数),所以在这里填充3字节,第七个4字节String占4字节(它是个对象64位机器占8字节64位,但是这里占4字节就是开启了指针压缩,关闭指针压缩则占8字节),同理第八个4字节Object对象也是一样,第九个4字节是对齐填充。

什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed–压缩、oop(ordinary object pointer)–对象指针
3.启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops。你可以在idea该位置填上参数后运行程序。
在这里插入图片描述
关闭指针压缩后的第三个对象对象头内存占用如图
在这里插入图片描述
关闭之后Mark Word和Klass Pointer都占了八个字节也就是前面四个4字节,String类型对象和Object对象都用8个字节来存储了。
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G),换种话来说就是一个对象可能在cpu寄存器中它是需要占35位然后根据对象信息去寻址,但是JVM可以压缩让这个对象在JVM中占32位,当寄存器去取JVM的对象时就解码,存入堆内存中就压缩编码。
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

关于对齐填充:对于大部分处理器,对象以8字节整数倍来对齐填充都是最高效的存取方式。

JVM专栏:点我
下一章(续这章内存分配机制):深入理解JVM-JVM对象创建与内存分配机制-03-2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值