java对象的内存模型详解:内存模型及对象头的奥秘

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 java对象的内存模型详解:内存模型及对象头的奥秘,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

在面试官的因势利导下,很多人对jvm的内存模型已经耳熟能详,但是对我们经常new 出来的对象,比如new Object(),你了解它的内存模型吗?本篇文章将带你走进对象内部,真正去了解这个你最熟悉,也最不熟悉的的对象。

一、对象的内存模型

先上图,简单易懂:
请添加图片描述

再看jvm源码:

class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  // 对象头  
  volatile markOop _mark;
  // 元数据
  union _metadata {
    // 对应的Klass对象  
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
————————————————

在jvm中,所有对象都是由oop对象来描述的,它有两个重要的子类:

  • instanceOopDesc:用来描述普通实例对象
  • arrayOopDesc:用来描述数组对象

二、内存模型解析

Object Header:即我们经常说的对象头,主要包含两部分:

  • markword:存储对象自身的运行时数据,后面详解
  • pointer:即指向当前对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果是数组,对象头中还有一块用于存放数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

instance data:实例数据,对象真正存储的有效信息,即自身或者继承自父类的属性。
padding:对齐填充,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍,因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全,保证对象最终大小是8字节的整数倍。

三、对象头的奥秘

老规矩,先上图,一目了然:
请添加图片描述
Mark Word被设计成了一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。当对象的状态不同,即无锁态、偏向锁、轻量级锁、重量级锁、GC状态时,分别存储不同的内容,例如无锁状态时,存储的是对象的hashcode+对象分代年龄+状态位001。
注意:上面的图是32位的JVM和64位JVM在开启对象头压缩(即jvm参数 -XX:+UseCompressedClassPointers)后的结构图,默认是开启后,关闭可通过设置jvm参数-XX:-UseCompressedClassPointers来关闭。如果关闭压缩,则对象头是占用8个字节,64位的,结构图如下:
在这里插入图片描述
开启压缩后,jvm创建大量对象过程时,可以节省大量的空间,间接的提高内存的利用率。同理-XX:+UseCompressedOops,普通对象指针压缩,也是默认开启的。

四、锁升级

前面讲了对象头在不同的锁状态下,会存储不同的数据,那么这个状态是怎么变化的呢,什么是时候是偏向锁?什么时候是轻量级锁?什么时候是重量级锁?这个就涉及到锁升级的过程(这里针对32位的对象头)。
首先了解一下锁的基本概念:
偏向锁:当线程调用到这个对象的时候,在mark word里面,用23位记录了指向当前线程的指针,说明这个锁当前属于该线程;同时偏向锁位由0变成1;
轻量级锁:当有别的线程来竞争锁的时候,取消偏向锁;30位里面存的是指向栈中锁记录的指针,当有别的线程过来需要竞争锁的时候,(线程的线程栈里会有LR(lock record),去竞争锁,看谁能把指针指向该锁,CAS,线程去修改mark word的指向LR的指针,CAS),所以叫做自旋锁。
**重量级锁:**有些线程自旋超过10次,或者等着的线程超过CPU线程的一半,JDK1.6之后,JVM自适应自选,自己来控制上述参数。锁升级为重量级锁。 Mark word里30位指向的是重量级锁的指针(重量级锁需经过内核批准)。那么线程会进入一个该锁的队列,在队列里等待,是不消耗cpu的。
锁降级:特定情况下才会发生,比如在GC的时候,因此没啥意义。

锁升级的过程:
在这里插入图片描述
过程解析:
1.线程A在进入同步代码块前,先检查MarkWord中的线程ID是否与当前线程ID一致,如果一致(还是线程A获取锁对象),则无需使用CAS来加锁、解锁。
2.如果不一致,再检查是否为偏向锁,如果不是,则自旋等待锁释放。
3.如果是,再检查该线程是否存在(偏向锁不会主动释放锁),如果不在,则设置线程ID为线程A的ID,此时依然是偏向锁。
4.如果还在,则暂停该线程,同时将锁标志位设置为00即轻量级锁(将MarkWord复制到该线程的栈帧中并将MarkWord设置为栈帧中锁记录)。线程A自旋等待锁释放。
5.如果自旋次数到了该线程还没有释放锁,或者该线程还在执行,线程A还在自旋等待,这时又有一个线程B过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
6.如果该线程释放锁,则会唤醒所有阻塞线程,重新竞争锁。

五、证明对象的内存模型

上面说了那么多理论,看起来有点道理,似懂非懂,那么怎么证明对象头的内存模型确实如文章所言,别急,我们来一步步研究

1.先引入maven依赖

		<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

2.在代码中打印所对象

import org.openjdk.jol.info.ClassLayout;
/**
 * @author ljx
 * @Description: 打印对象的内存模型
 * @date 2021/8/26 4:50 下午
 */
public class ObjectMemoryModel {

    private static class Test {
        int i;
        long l;
        boolean b;
        String s = "快给博主点赞";
        void m(){};
    }
    public static void main(String[] args) {
        Test t = new Test();
        final String s = ClassLayout.parseInstance(t).toPrintable();
        System.out.println(s);
    }

}

3.输出结果

请添加图片描述
解析:如图所示,markword占用了8个字节,class pointer占用了四个字节,instance data占用了 int的4个字节+long的8个字节+boolean的1个字节+string对象引用的4个字节 = 共17个字节,此时总共29个字节,不是8个字节的倍数,所以补齐3个字节,即padding 是 3个字节。

参考文章:https://www.zhangshengrong.com/p/v710KvgpXM/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/130206.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!