Java对象在内存中的布局是如何实现的?

1、-XX:FieldsAllocationStyle

对象在内存中的布局首要相关配置就是FieldsAllocationStyle,这个配置有3个可选值,即0、1、2。当值为2的时候,会经过一些逻辑判断最终转化为0或者1.

  • -XX:FieldsAllocationStyle=0 表示先分配对象,然后再按照double/long、ints、chars/shorts、bytes/booleans的顺序分配其他字段,也就是类中声明的相同宽度的字段总是会被分配在一起,而相同宽度字段的顺序则是它们在class文件中声明的顺序。-
  • XX:FieldsAllocationStyle=1表示先按照double/long、ints、chars/shorts、bytes/booleans的顺序分配属性,然后再分配对象,分配过程中的其他原则上面为0时是保持一致的,同时这也是JVM默认的分配策略。

当然,上面这2种分配策略只是针对大部分正常情况而言,有以下几种情况是会有所区别的(只是有部分区别,大致是没有问题的)

  • 如果是特定的类、例如基本类型的包装类、String、Class、ClassLoader、软引用等类,会先分配对象,然后再按照double/long、ints、chars/shorts、bytes/booleans的顺序分配,同时-XX:+CompactFields和-XX:FieldsAllocationStyle=1都不会生效。
  • 如果配置-XX:+CompactFields,会将ints、shorts/chars、bytes/booleans、oops的顺序将字段填充到对象头信息与字段起始偏移位置的间隙中去
  • 如果当前类或者类中使用了注解@sun.misc.Contended, 也会打乱上述布局

其他:

由于在计算对象字段的布局(字段基于对象起始位置的偏移量)时,当前类上述各种类型变量的个数是已知的,所以每种类型的起始偏移量就可以通过计算得到,如下:

next_nonstatic_word_offset  = next_nonstatic_double_offset +
                                (nonstatic_double_count * BytesPerLong);
next_nonstatic_short_offset = next_nonstatic_word_offset +
  (nonstatic_word_count * BytesPerInt);
next_nonstatic_byte_offset  = next_nonstatic_short_offset +
  (nonstatic_short_count * BytesPerShort);
next_nonstatic_padded_offset = next_nonstatic_byte_offset +
  nonstatic_byte_count;

而对于oops对象的偏移量处理会比较特殊,如果-XX:FieldsAllocationStyle=0, 那么oops的偏移量起始位置就为对象头之后,如果-XX:FieldsAllocationStyle=1, 则会进行下列处理,使得next_nonstatic_padded_offset与heapOopSize是对齐的。如下:

// let oops jump before padding with this allocation style
if( allocation_style == 1 ) {
  next_nonstatic_oop_offset = next_nonstatic_padded_offset;
  if( nonstatic_oop_count > 0 ) {
    next_nonstatic_oop_offset = align_size_up(next_nonstatic_oop_offset, heapOopSize);
  }
  next_nonstatic_padded_offset = next_nonstatic_oop_offset + (nonstatic_oop_count * heapOopSize);
}

同时由于这个oops补齐操作以及计算完所有字段的偏移量之后,会再进行补齐操作,与heapOopSize进行对齐,heapOopSize在开启和关闭压缩指针的情况下,值分表为4和8。

2、-XX:CompactFields

-XX:CompactFields表示是否将对象中较窄的数据插入到间隙中,-XX:+CompactFields表示插入,-XX:-CompactFields则是不插入。默认JVM是开启插入的。

那么这儿就要讨论一下为什么会插入,以及怎么插入?

首先需要了解Java对象的大致内存布局,最开始的一块区域存放对象标记以及元数据指针,然后才是实例数据,如下图所示:

它们分别对应普通对象与数组对象在内存中的布局。由于对象字段布局是在Class文件解析的时候计算的,而数组类没有对应的Class文件,所以数组对象的布局这儿不做讨论。

继续回到刚刚的话题,将对象中较窄数据的插入间隙,可以细分为2种情况

  • 当前类没有父类或者是父类中没有实例数据,此时会将实例数据前的对象标记和对象元数据指针按照8字节对齐,如上图所示,在开启压缩指针的情况下,对齐前占用12个字节,对齐后到16字节,此时存在4个字节的间隙,那么会将类中存在的字段按照 ints、chars/shorts、bytes/booleans、oops的顺序进行填充,直到将间隙填充完毕,由于对齐之后的间隙要么是0,要么是4,所以填充间隙最多1个ints、2个chars/shorts、4个bytes/booleans、1个oops。
  • 当前类存在父类,并且父类中存在实例数据,此时会将实例数据前的对象标记和对象元数据指针 + 父类的实例数据大小按照8字节对齐,然后再进行填充,由于整个类在计算完所有字段偏移之后,会再与heapOopSize进行对齐,所以父类的实例数据大小肯定是heapOopSize的倍数,也就是与第一种情况类似,不同的是,子类中的字段属性需要在父类字段之后进行分配。

最终可以得到如下图所示:

间隙插入受-XX:CompactFields影响外,还受到配置-XX:-UseCompressedOops的影响,回到上面的对齐,在开启压缩指针的情况下,元数据指针占8个字节,这时候按照上面的细分情况1,也就不存在对齐了,而细分的情况二,由于父类在计算完字段偏移量之后会与heapOopSize对齐,heapOopSize在开启压缩指针的情况下为jintSize, 关闭的情况下为oopSize,分别对应4和8, 也就是关闭压缩指针的情况下,无论如何都不会发生间隙插入。

3、@sun.misc.Contended

@sun.misc.Contended也会影响对象在内存中的布局,这个注解是为了解决伪共享(False Sharing)的问题,关于伪共享的问题这儿就不讲解了。

@sun.misc.Contended 可以用于修饰类、也可以用于修饰字段。

对于在类上的修饰来讲,会在2个地方增加ContendedPaddingWidth,这个变量值为128。

一个地方是对象标记和元数据指针 + 父类实例数据(当前可能没有父类实例数据)之后 + ContendedPaddingWidth,然后再与8位进行对齐,另一个地方是,所有的非Contended实例字段偏移量计算完毕后,再加上ContendedPaddingWidth。

处理完类,接下来是字段,这儿的字段偏移量计算跟上面不一样,并没有按照double/long、ints、chars/shorts、bytes/booleans的顺序来,而是按照@sun.misc.Contended对应的group来进行计算,相同group的字段会放在一起,不同group的字段之间会以ContendedPaddingWidth来隔开,这儿比较特殊的情况是默认分组,默认分组为0,这个分组对应的每个字段在计算完偏移量之后都会加上ContendedPaddingWidth。所以@sun.misc.Contended修饰的字段布局如下图所示:

同时在计算每个字段偏移前,会使当前的偏移量与当前字段类型所对应的字节数对齐,例如int,当前偏移量会以4字节进行对齐,对齐之后的偏移量为当前int字段的偏移量。

4、静态字段的偏移量计算

静态字段的偏移量计算不受-XX:FieldsAllocationStyle和-XX:CompactFields的影响,会直接按照

oops、double/long、ints、chars/shorts、bytes/booleans的顺序进行偏移量的计算。

同时给静态字段 加上@sun.misc.Contended不会起到任何作用。

5、示例

5.1、-XX:FieldsAllocationStyle

测试代码:

final class NoChild {
    private Boolean value = Boolean.TRUE;
    private byte b;
    private int i;
}

@Test
public void test() {
  declaredFields = NoChildContended.class.getDeclaredFields();
  for (Field field : declaredFields) {
    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 					" offset is " + offset);
    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 						offset is " + offset);
    }
  }
}

运行结果:

-XX:FieldsAllocationStyle=0 -XX:-UseCompressedOops
//可以看到对象实例数据顺序为value、int、byte,这儿由于没有开启指针压缩,所以对象引用占了8个字节。
org.yamikaze.NoChild field value offset is 16
org.yamikaze.NoChild field b offset is 28
org.yamikaze.NoChild field i offset is 24
-XX:FieldsAllocationStyle=1 -XX:-UseCompressedOops
//可以看到先分配 int变量 i,其次byte变量 b,最后才是对象 value
//这儿的byte变量b偏移是20,占用大小1字节,而经过对齐之后,会产生3个字节的align
org.yamikaze.NoChild field value offset is 24
org.yamikaze.NoChild field b offset is 20
org.yamikaze.NoChild field i offset is 16

5.2、-XX:CompactFields

测试代码:

class Parent {
    private long value;
    private int j;
    private byte b;
}

class Child2 extends Parent {
    private byte d;
    private long a;
    private int f;
}

@Test
public void test() {

  Child2 c = new Child2();
  Unsafe unsafe = UnSafeUtils.getUnsafe();
  Field[] declaredFields = c.getClass().getSuperclass().getDeclaredFields();
  for (Field field : declaredFields) {

    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 				" offset is " + offset);
    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 						offset is " + offset);
    }
  }

  declaredFields = c.getClass().getDeclaredFields();
  for (Field field : declaredFields) {
    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 				" offset is " + offset);

    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 					offset is " + offset);
    }
  }
}

测试结果:

-XX:+CompactFields -XX:+UseCompressedOops
//可以看到子类Child2的变量f并没有按照double/long、ints、shorts/chars、bytes/booleans的顺序计算偏移量,
//而是插入到了间隙里面
org.yamikaze.Parent field value offset is 16
org.yamikaze.Parent field j offset is 12
org.yamikaze.Parent field b offset is 24
org.yamikaze.Child2 field d offset is 40
org.yamikaze.Child2 field a offset is 32
org.yamikaze.Child2 field f offset is 28
-XX:-CompactFields -XX:+UseCompressedOops
//由于关闭了CompactFields,所以变量f的按照上面的顺序进行偏移量计算
org.yamikaze.Parent field value offset is 16
org.yamikaze.Parent field j offset is 24
org.yamikaze.Parent field b offset is 28
org.yamikaze.Child2 field d offset is 44
org.yamikaze.Child2 field a offset is 32
org.yamikaze.Child2 field f offset is 40

5.3、Contended

测试代码:

@Contended
final class NoChildContended {

    private byte b;

    @Contended("aaa")
    private double value;

    @Contended("bbb")
    private int value1;
}

@Test
public void test() {
  declaredFields = NoChildContended.class.getDeclaredFields();
  for (Field field : declaredFields) {

    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + " offset is " + offset);

    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " offset is " + offset);
    }
  }
}

测试结果:

-XX:-RestrictContended
同分组时:
org.yamikaze.NoChildContended field b offset is 140
org.yamikaze.NoChildContended field value offset is 272
org.yamikaze.NoChildContended field value1 offset is 280
不同分组(默认分组):
org.yamikaze.NoChildContended field b offset is 140
org.yamikaze.NoChildContended field value offset is 272
org.yamikaze.NoChildContended field value1 offset is 408

可以看到,由于Class上有@sun.misc.Contended注解修饰,导致byte变量的偏移量很大(12 + 128) 同样byte变量之后的value,偏移量再次增加了128,达到272(141 + 128 = 269然后与4字节对齐得到272),然后相同分组的value1紧跟着value,而在不同分组的情况下,value1和value之间又隔了128。

6、其他

6.1、通过Unsafe获取实例字段和静态字段的偏移量

//实例字段
unsafe.objectFieldOffset(field);
//静态字段
unsafe.staticFieldOffset(field);

6.2、Unsafe是如何进行实例字段和静态字段偏移量的获取,以及如何通过CAS操作改变值

回到上文的偏移量计算,在经过计算后,每个字段相对于对象头的偏移量都是已知的,这个偏移量会保存到字段信息里面去,那么获取字段偏移量也很简单,直接拿到字段相关信息取得offset即可,而通过CAS操作改变字段的值也很简单,当前对象指针加上字段偏移量就是当前字段在内存中的地址,直接通过指针更字段值即可。

到此这篇关于Java对象在内存中的布局是如何实现的?的文章就介绍到这了,更多相关Java对象在内存中的布局内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入理解JVM之Java对象的创建、内存布局、访问定位详解

    本文实例讲述了深入理解JVM之Java对象的创建.内存布局.访问定位.分享给大家供大家参考,具体如下: 对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执

  • 详解Java对象的内存布局

    前言 今天来讲些抽象的东西 -- 对象头,因为我在学习的过程中发现很多地方都关联到了对象头的知识点,例如JDK中的 synchronized锁优化 和 JVM 中对象年龄升级等等.要深入理解这些知识的原理,了解对象头的概念很有必要,而且可以为后面分享 synchronized 原理和 JVM 知识的时候做准备. 对象内存构成 Java 中通过 new 关键字创建一个类的实例对象,对象存于内存的堆中并给其分配一个内存地址,那么是否想过如下这些问题: 这个实例对象是以怎样的形态存在内存中的? 一个O

  • 详解Java对象创建的过程及内存布局

    一.对象的内存布局 对象头 对象头主要保存对象自身的运行时数据和用于指定该对象属于哪个类的类型指针. 实例数据 保存对象的有效数据,例如对象的字段信息,其中包括从父类继承下来的. 对齐填充 对齐填充不是必须存在的,没有特别的含义,只起到一个占位符的作用. 二.对象的创建过程 实例化一个类的对象的过程是一个典型的递归过程. 在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类. 此时,首先实例化Object类

  • Java对象在内存中的布局是如何实现的?

    1.-XX:FieldsAllocationStyle 对象在内存中的布局首要相关配置就是FieldsAllocationStyle,这个配置有3个可选值,即0.1.2.当值为2的时候,会经过一些逻辑判断最终转化为0或者1. -XX:FieldsAllocationStyle=0 表示先分配对象,然后再按照double/long.ints.chars/shorts.bytes/booleans的顺序分配其他字段,也就是类中声明的相同宽度的字段总是会被分配在一起,而相同宽度字段的顺序则是它们在cl

  • Java 对象在 JVM 中的内存布局超详细解说

    目录 一.new 对象的几种说法 二.Java 对象在内存中的存在形式 1. 栈帧(Frame) 2. 对象在内存中的存在形式 ① 3. 对象中的方法存储在那儿? 4. Java 对象在内存中的存在形式 ② 三.类中属性详细说明 四.细小知识点 1. 如何创建对象 2. 如何访问属性 五.Exercise 六.总结 一.new 对象的几种说法 初学 Java 面向对象的时候,实例化对象的说法有很多种,我老是被这些说法给弄晕. public class Test { public static v

  • Java对象的内存布局全流程

    目录 对象内存布局 对象占用内存空间 证明对象内存布局 开始先抛出一个问题:一个对象o,Object o = new Object();创建完成后会占用多少字节的内存? 要能回答这个问题,就需要了解java对象的内存布局. 对象内存布局 一个Java对象在内存中包括对象头.实例数据和对齐填充三个部分.如下图所示: 对象头 Mark Word:包含一系列的标记位比如hashcode.GC分代年龄.偏向锁位,锁标志位等.这个Mark Word在对象被加了不同量级的锁时所包含的内容和布局都有所不同,这

  • java保证对象在内存中唯一性的实现方法

    前言 刚看到这个题目的朋友第一反应肯定是好奇,之后再细细思考下就会发现这个题目眼熟了. 就算是同一个答案,如果提问的方式不同, 往往会对回答造成干扰或者影响, 上学的时候老师也说过差不多的话, 学过设计模式的人看到这个题目之后肯定不会陌生:请描述java中单例设计模式的实现步骤,那估计就能答出来了. 而java中的单例模式,相信只要是你看过一些java视频教程,肯定就能写出来的. 所以我们在平时学习的时候要注意举一反三,掌握其精髓,这样在面试或者工作的时候才能应对自如,久而久之,就成为了大神级别

  • Java对象在JVM中的生命周期详解

    概念 在Java中,对象的生命周期包括以下几个阶段: 创建阶段(Created) 应用阶段(In Use) 不可见阶段(Invisible) 不可达阶段(Unreachable) 收集阶段(Collected) 终结阶段(Finalized) 对象空间重分配阶段(De-allocated) Java对象在JVM中的生命周期 当你通过new语句创建一个java对象时,JVM就会为这个对象分配一块内存空间,只要这个对象被引用变量引用了,那么这个对象就会一直驻留在内存中,否则,它就会结束生命周期,JV

  • Java对象创建内存案例解析

    Java对象创建内存图解析 1. 栈 Java栈的区域很小 , 特点是存取的速度特别快,栈存储的特点是, 先进后出,存储速度快的原因: 栈内存, 通过 栈指针'来创建空间与释放空间,指针向下移动, 会创建新的内存, 向上移动, 会释放这些内存.这种方式速度特别快 , 仅次于PC寄存器,但是这种移动的方式, 必须要明确移动的大小与范围 ,明确大小与范围是为了方便指针的移动 , 这是一个对于数据存储的限制, 存储的数据大小是固定的 , 影响了程序的灵活性. 所以我们把更大部分的数据 存储到了堆内存中

  • java 对象实例化过程中的多态特性解析

    目录 java 对象实例化过程中的多态特性 通过案例说明 通过上述代码 java对象的三个特性(封装.继承.多态) 1.封装 2.继承 3.多态 java 对象实例化过程中的多态特性 执行对象实例化过程中遵循多态特性 ==> 调用的方法都是实例化的子类中的重写方法,只有明确调用了super.xxx关键词或者是子类中没有该方法时,才会去调用父类相同的同名方法. 通过案例说明 package com.njau.test1; class Test { public static void main(S

  • php对象在内存中的存在形式分析

    本文实例分析了php对象在内存中的存在形式.分享给大家供大家参考.具体分析如下: <?php class Person{ public $name; public $age; } $p1 = new Person(); $p1->name = "小明"; $p1->age=80; $p2=$p1; $p2->age=85; echo $p2->name; echo $p1->age; ?> (1)$p1对应内存地址,假设是0x123,($p1和

  • 详解Java字符串在内存中的存储位置

    在JDK6的时候在Java虚拟机(这里指的是HotSpot)中内存区域分为本地方法栈.虚拟机栈.堆.程序计数器.方法区等,方法区又被称作永久代. 这里只说一下字符串的存储位置,在Java虚拟机内存中有个区域叫做运行时常量池,是方法区的一部分.在JDK6中其中存放的有类的版本.字段.方法.接口等描述信息以及常量池,常量池用来存放编译期间生成的各种字面量和符号引用,字符串就存储在这个位置.下面通过代码来看下现象. 这是JDK6的实验现象,Java虚拟机的配置如下: -XX:PermSize=5M -

随机推荐