50 jhat 中 java.lang.String 的实例占用空间为什么是 28 bytes ?

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了50 jhat 中 java.lang.String 的实例占用空间为什么是 28 bytes ?相关的知识,希望对你有一定的参考价值。

前言

 

此问题是 多个 classloader 加载的同类限定名的Class 在 jhat 中显示不全d 同一时期发现的问题  

大致的情况是 看到了 jhat 中统计的各个 oop 的占用空间 似乎是不太能够对的上  

比如 java.lang.String, 在 64bit vm 上面 开启了 UseCompressedOops 之后, 应该是占用 (12 + 4 + 4) + 4[对齐] = 24, 才对 

但是 实际上在 jhat 显示的是 28 bytes 

我们这里来看一下这个问题, 顺便也可以 扩展一些扩展一些其他问题 

可以先看一下 普通对象的内存布局 对象的默认布局

以下部分内容, 截图 若无特殊说明, 基于 jdk8 

测试用例 

测试用例如下, 然后 生成 dump 文件, 使用 jhat 进行分析 

/**
 * Test27MultiClassInClassloader
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2021-12-12 14:27
 */
public class Test27MultiClassInClassloader 

    // Test27MultiClassInClassloader
    public static void main(String[] args) throws Exception 

        List<Object> refs = new ArrayList<>();
        ClassLoader appClassloader = Test27MultiClassInClassloader.class.getClassLoader();
        URL[] classpath = new URL[]
                new File("/Users/jerry/IdeaProjects/HelloWorld/target/classes").toURI().toURL()
        ;
        String[] alwaysParentPattern = new String[];
        for (int i = 0; i < 10; i++) 
            ChildFirstClassLoader classLoader = new ChildFirstClassLoader(classpath, appClassloader, alwaysParentPattern);
            Class userClazz = classLoader.loadClass("com.hx.test12.Test27MultiClassInClassloaderUser");
            System.out.println(" the " + i + "th classloader " + Integer.toHexString(userClazz.hashCode()));
            Object userInstance = userClazz.newInstance();
            refs.add(userInstance);
        

        System.in.read();

    


查看 java.lang.String 的 instances 

呵呵 对了一下 显然不对 

查看 Test27MultiClassInClassloaderUser

呵呵 对了一下 显然不对 

jhat 的 instanceSize 是怎么计算的 ?

大致的计算方式是 oop 的 nonStatic属性 占用的字节数 + 对象头[16字节] 

首先这个计算 是不够精确的, 因为 会存在将 [Word, Short, Byte, Oop] 放在一些场景下产生的 gap 里面[比如 64bit 的 vm, 开启了 UseCompressedOops 之后, 对象头 对齐 16 byte 的 gap]

oop 的 nonStatic属性 占用的字节数 是读取自 dump 文件, 当前 oop 对应的记录, 根据 offset 来直接获取当前 oop 的 nonStatic属性 占用的字节数 

那么 这个数据就是来自于 heapdump 生成的地方了? 

HotspotVM 生成 dump 文件中的 instanceRecord 

 以下截图基于 jdk9 

如下部分是计算 oop 的 instanceSize 的地方 

可以看到的是 oop 相关均是算作 8byte, 但是实际我们 64bit vm 上面 开启了 UseCompressedOops 之后仅占用 4byte 

另外就是 上面提到的 可能有一部分 [Word, Short, Byte, Oop] 是被分配到 gap 里面的 

综上 dump 文件中的 oop 占用空间的计算 是不够精确的, 不可信的 

java.lang.String 的内存布局

对象的默认内存布局 中提到了一些普通的类型的内存布局, allocate_style 为 1 

    // Fields order: longs/doubles, ints, shorts/chars, bytes, oops, padded fields

java.lang.String 等一部分特殊的 InstanceKlass 的 allocate_style 是不相同的 

  // The next classes have predefined hard-coded fields offsets
  // (see in JavaClasses::compute_hard_coded_offsets()).
  // Use default fields allocation order for them.
  if( (allocation_style != 0 || compact_fields ) && _loader_data->class_loader() == NULL &&
      (_class_name == vmSymbols::java_lang_AssertionStatusDirectives() ||
       _class_name == vmSymbols::java_lang_Class() ||
       _class_name == vmSymbols::java_lang_ClassLoader() ||
       _class_name == vmSymbols::java_lang_ref_Reference() ||
       _class_name == vmSymbols::java_lang_ref_SoftReference() ||
       _class_name == vmSymbols::java_lang_StackTraceElement() ||
       _class_name == vmSymbols::java_lang_String() ||
       _class_name == vmSymbols::java_lang_Throwable() ||
       _class_name == vmSymbols::java_lang_Boolean() ||
       _class_name == vmSymbols::java_lang_Character() ||
       _class_name == vmSymbols::java_lang_Float() ||
       _class_name == vmSymbols::java_lang_Double() ||
       _class_name == vmSymbols::java_lang_Byte() ||
       _class_name == vmSymbols::java_lang_Short() ||
       _class_name == vmSymbols::java_lang_Integer() ||
       _class_name == vmSymbols::java_lang_Long())) 
    allocation_style = 0;     // Allocate oops first
    compact_fields   = false; // Don't compact fields
  

allocate_style 为 0 的场景下面, oops 是放置于 longs/doubles 之前 

对于我们 java.lang.String 来说一个内存编排应该是 

1. private final char value[]; 

2. private int hash;

  int next_nonstatic_oop_offset = 0;
  int next_nonstatic_double_offset = 0;

  // Rearrange fields for a given allocation style
  if( allocation_style == 0 ) 
    // Fields order: oops, longs/doubles, ints, shorts/chars, bytes, padded fields
    next_nonstatic_oop_offset    = next_nonstatic_field_offset;
    next_nonstatic_double_offset = next_nonstatic_oop_offset +
                                    (nonstatic_oop_count * heapOopSize);
  

然后我们使用常用的工具 jol 来看一下, 一个 "xx" 的 java.lang.String, 可以看到 确实如此 

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0     4          (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4          (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4          (object header)                           da 02 00 f8 (11011010 00000010 00000000 11111000) (-134216998)
     12     4   char[] String.value                              [x, x]
     16     4      int String.hash                               0
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

完 

参考 

对象的默认布局

以上是关于50 jhat 中 java.lang.String 的实例占用空间为什么是 28 bytes ?的主要内容,如果未能解决你的问题,请参考以下文章

org.json.JSONException: Value okhttp3.internal.http.RealResponseBody@da68fa1 of type java.lang.Strin

H2数据库函数及数据类型概述

jhat:虚拟机堆转储快照分析工具

JVM监控及诊断工具-命令之jhat

Java命令学习系列——jhat

深入理解Java虚拟机——虚拟机堆转储快照分析工具(jhat)