性能文章>04 对象的默认内存布局>

04 对象的默认内存布局原创

1年前
280144

前言

最近看到了这样的一篇文章, 一个对象的引用占多少个字节呢?4个?8个?算出来都不对 , 呵呵 这是一个之前想要弄明白, 但是这块的代码似乎是看着有点复杂, 所以 一直没有花时间来整理一下, 呵呵最近看到了一篇文章, 看了一下 R大的分析 

然后 自己结合自己的 实际情况, 整理了一下 些东西 

以下代码, 截图 基于 jdk9 

首先是测试用例

package com.hx.test04;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * TypeSizeOf
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2020-03-07 16:04
 */
public class Test05TypeSizeOf {

  // fields
  private long id = 1;
  private Test05TypeSizeOf test2 = null;
  private List<Test05TypeSizeOf> list = new ArrayList<>();
  private Date date = new Date();
  private byte status = 2;
  private byte count = 3;

  // refer : https://hllvm-group.iteye.com/group/topic/38670
  // -ea -javaagent:/Users/jerry/Tmp/agent/HelloWorld-1.0-SNAPSHOT_agent.jar
  // vm 调试相关参数为 -da -dsa -Xint -Xmx100M -XX:+UseSerialGC -javaagent:/Users/jerry/Tmp/agent/HelloWorld-1.0-SNAPSHOT_agent.jar com.hx.test04.Test05TypeSizeOf
  public static void main(String[] args) {

    long sizeOfTest05TypeOfSizeOf = Test01PremainAgentClazz.inst.getObjectSize(new Test05TypeSizeOf());
    System.out.println(sizeOfTest05TypeOfSizeOf);

  }

}

测试结果如下 

第一个输出是 javaagent 里面的输出, 第二个输出是 这里 main 方法里面的输出 

那么, 这里输出的结果是 40, 好了 我们现在知道了 Test05TypeSizeOf 会占用 40个 字节, 那么 他又是如何计算的呢, 在内存中是如何排列的呢 ?

布局的计算方式?

classFileParser.layout_fields 相关代码片段如下 

  int nonstatic_oop_space_count   = 0;
  int nonstatic_word_space_count  = 0;
  int nonstatic_short_space_count = 0;
  int nonstatic_byte_space_count  = 0;
  int nonstatic_oop_space_offset = 0;
  int nonstatic_word_space_offset = 0;
  int nonstatic_short_space_offset = 0;
  int nonstatic_byte_space_offset = 0;

  // Try to squeeze some of the fields into the gaps due to
  // long/double alignment.
  if (nonstatic_double_count > 0) {
    int offset = next_nonstatic_double_offset;
    next_nonstatic_double_offset = align_size_up(offset, BytesPerLong);
    if (compact_fields && offset != next_nonstatic_double_offset) {
      // Allocate available fields into the gap before double field.
      int length = next_nonstatic_double_offset - offset;
      assert(length == BytesPerInt, "");
      nonstatic_word_space_offset = offset;
      if (nonstatic_word_count > 0) {
        nonstatic_word_count      -= 1;
        nonstatic_word_space_count = 1; // Only one will fit
        length -= BytesPerInt;
        offset += BytesPerInt;
      }
      nonstatic_short_space_offset = offset;
      while (length >= BytesPerShort && nonstatic_short_count > 0) {
        nonstatic_short_count       -= 1;
        nonstatic_short_space_count += 1;
        length -= BytesPerShort;
        offset += BytesPerShort;
      }
      nonstatic_byte_space_offset = offset;
      while (length > 0 && nonstatic_byte_count > 0) {
        nonstatic_byte_count       -= 1;
        nonstatic_byte_space_count += 1;
        length -= 1;
      }
      // Allocate oop field in the gap if there are no other fields for that.
      nonstatic_oop_space_offset = offset;
      if (length >= heapOopSize && nonstatic_oop_count > 0 &&
              allocation_style != 0) { // when oop fields not first
        nonstatic_oop_count      -= 1;
        nonstatic_oop_space_count = 1; // Only one will fit
        length -= heapOopSize;
        offset += heapOopSize;
      }
    }
  }

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

  // 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);
  }

 classFileParser.layout_fields 这里是布局的处理, 默认情况下 allocation_style = 1

并且 我们这里存在 一个 long/double 字段, "id" 

将数据分为了两批, 一个批次是 *_space_count[Word, Short, Byte, Oop], 是存放在 align_size_up(markOop + klass, BytesPerLong) 的空隙
另外的一部分 : longs/doubles, ints, shorts/chars, bytes, oops, padded fields, 排列
对于我们这里的场景, 如下

对应于我们这里的 Test05TypeSizeOf 的实际情况, 布局大致如下 

markOop[8] + klass[4]
byte status[1]
byte count[1]
padding[2]
long [8]
test2 [4]
list [4]
date [4]
padding [4]
合计 40 bytes, 5 word

运行时的数据?

那么我们理论上得到了数据的排列如下, 那么我们看一下 实际的运行时的一些情况呢 ? 

HSDB attach 到目标进程, inspect Test05TypeSizeOf 的实例 

 inspect 0x0000000795969ab8
  Oop for com/hx/test04/Test05TypeSizeOf @ 0x0000000795969ab8
  _mark: 1
  _metadata._compressed_klass: InstanceKlass for com/hx/test04/Test05TypeSizeOf
  id: 1
  test2: null
  list: Oop for java/util/ArrayList @ 0x0000000795969ae0
  date: Oop for java/util/Date @ 0x00000007959831f8
  status: 2
  count: 3

 

查看一下 的内存数据呢 ? 

 mem 0x0000000795969ab8 5
  0x0000000795969ab8: 0x0000000000000001
  0x0000000795969ac0: 0x00000302f800c354
  0x0000000795969ac8: 0x0000000000000001
  0x0000000795969ad0: 0xf2b2d35c00000000
  0x0000000795969ad8: 0x00000000f2b3063f

# 数据拆解如下
 0x0000000795969ab8 : 0x0000000000000001 为 markOop
 0x0000000795969ac0 : 0xf800c354 为 compressedKlass
 0x0000000795969ac4 : 0x02 为 status
 0x0000000795969ac5 : 0x03 为 count
 0x0000000795969ac6 : 0x0000 为 padding
 0x0000000795969ac8 : 0x0000000000000001 为 id
 0x0000000795969ad0 : 0x0000000 为 test2
 0x0000000795969ad4 : 0xf2b2d35c 为 list
 0x0000000795969ad8 : 0xf2b3063f 为 date
 0x0000000795969ae0 : 0x0000000 为 padding

但是 发现一个问题, 为什么记录的 数据的地址 和 给定的对象的 oop 的地址不一样呢 ? 

compressedOops 相关

从上面可以看到, 实际存储的 oop 的地址数据 和 给定的 oop 的真实地址是不一样的, 那么这是怎么回事呢?, 两个数据又有什么关联呢 ?

实际存储的 list 地址为 0xf2b2d35c, list 的真实地址为 0x0000000795969ae0 

这是因为一个 UseCompressedOops 特性, 那么我们来看下 压缩之后的地址 和 原来的地址的关系吧 

oop.decode_heap_oop_not_null 相关实现如下 

我们这里, base 为 0, shift 为 3 (<< 3 等价于 * 8[字长])

base, shift 初始化的地方在这里 

至于 base 为什么是 0, shift 是 3, 我们可以暂时不深究 

好了, 两者之间的关系大概就是这样, 那么 apply 到这里的实际情况呢 ?

实际存储的 list 地址为 0xf2b2d35c, list 的真实地址为 0x0000000795969ae0 

0xf2b2d35c * 8 = 0x795969ae0 

那么同理 另外一个 date 的地址记录的是 0xf2b3063f, date 的真实地址为 0x00000007959831f8 

0xf2b3063f * 8 = 0x7959831f8

好了, 这里的相关东西 大概就这些了 

======================= add at 2020.05.31 =======================

一下内容引用自R大的文章 Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针

HotSpot VM现在只使用3种模式的压缩指针:
1、当整个GC堆所预留的虚拟地址范围的最高的地址在4GB以下的时候,使用"zero based Compressed Oops, 32-bits Oops"模式,也就是基地址为0、shift也为0;
2、当GC堆的最高地址超过了4GB,但在32GB以下的时候,使用"zero based Compressed Oops"模式,也就是基地址为0、shift为 LogMinObjAlignmentInBytes (默认为3)的模式;
3、当GC堆的最高地址超过了32GB,但整个GC堆的大小仍然在32GB以下的时候,使用非零基地址、shift为 LogMinObjAlignmentInBytes (默认为3)的模式。
如果上面三种情况都无法满足,那压缩指针就无法使用了。

上述三种模式的名字在Universe类里有声明:
hotspot/src/share/vm/memory/universe.hpp

C++代码 

 收藏代码

  1. // Narrow Oop encoding mode:  
  2. // 0 - Use 32-bits oops without encoding when  
  3. //     NarrowOopHeapBaseMin + heap_size < 4Gb  
  4. // 1 - Use zero based compressed oops with encoding when  
  5. //     NarrowOopHeapBaseMin + heap_size < 32Gb  
  6. // 2 - Use compressed oops with heap base + encoding.  
  7. enum NARROW_OOP_MODE {  
  8.   UnscaledNarrowOop  = 0,  
  9.   ZeroBasedNarrowOop = 1,  
  10.   HeapBasedNarrowOop = 2  
  11. };  

 

哎, 可惜我这里 场景1 和 场景3 构造不出来 

参考

一个对象的引用占多少个字节呢?4个?8个?算出来都不对 
https://hllvm-group.iteye.com/group/topic/38670

 

点赞收藏
分类:标签:
黄金键盘
请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

4
4