JOL(java object layout --java 对象内存布局)

JOL(java object layout --java 对象内存布局)

⚠⚠⚠本文以java普通对象为切入点,分析java的对象内存布局,数组见文末

maven地址,用0.9版本即可,新版本打印的信息简化



    org.openjdk.jol
    jol-core
    0.9
    provided

相关方法:
1.使用jol计算对象的大小(单位为字节):
	ClassLayout.parseInstance(obj).instanceSize()
2.使用jol查看对象内部的内存布局:
	ClassLayout.parseInstance(obj).toPrintable()
3.查看对象外部信息:包括引用的对象:
	GraphLayout.parseInstance(obj).toPrintable()
4.查看对象占用空间总大小:
	GraphLayout.parseInstance(obj).totalSize()

基础概念:

问题1. Java对象如何存储?

JOL(java object layout --java 对象内存布局)_第1张图片

对象的实例(instantOopDesc)保存在上,对象的元数据(instantKlass,即class文件)保存在方法区(元空间?),对象的引用保存在栈上。

问题2:指针压缩

开启指针压缩可以减少对象的内存使用。

在关闭指针压缩时,String、Integer等字段由于是引用类型,因此分别占8个字节;

而开启指针压缩之后,这两个字段只分别占用4个字节。

因此,开启指针压缩,理论上来讲,大约能节省接近百分之五十的内存。(如果对象属性都是引用类型的话)

jdk8及以后版本已经默认开启指针压缩,无需配置。

下文中,有对指针压缩进行测试

  • 开启(-XX:+UseCompressedOops) 可以压缩指针。
  • 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针。

1.空属性-对象布局

定义一个简单的Java 对象, 打印其内存布局

import org.openjdk.jol.info.ClassLayout;

public class Entity {
    public static void main(String[] args) {
        Entity entity = new Entity();
        // 打印java 对象内存布局
        System.out.println(ClassLayout.parseInstance(entity).toPrintable());
    }
}

// 输出结果
com.brown.Entity object internals: // Entity对象内存布局
 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)         05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
   12    4      (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • OFFSET:偏移地址,单位字节
  • SIZE:占用的内存大小,单位为字节
  • TYPE DESCRIPTION: 类型描述,其中object header为对象头;
  1. object header:对象头
  2. loss due to the next object alignment:由于下一个对象对齐而导致的丢失(有4Byte是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0Byte)。
  • VALUE : 对应内存中当前存储的值;
  • Instance size:实例字节数值大小(**此处一个空的java对象(不包含任意字段属性)实例,其实例大小为``16Byte**

分析:

由上文可知:在开启指针压缩的情况下,Entity对象(不包含属性字段)的对象头占用12个字节。

Java程序运行的过程中,每创建一个新的对象,JVM就会相应地创建一个对应类型的oop对象,存储在堆中。如new Entity(),则会创建一个instanceOopDesc,基类为oopDesc

[instanceOop.hpp文件:hotspot/src/share/vm/oops/instanceOop.hpp]
class instanceOopDesc : public oopDesc {
}

instanceOopDesc只提供了几个静态方法,如获取对象头大小。因此重点看其父类oopDesc

[oop.hpp文件:hotspot/src/share/vm/oops/oop.hpp]
class oopDesc {
   friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  ........
}

我们只关心对象头,普通对象(如Entity对象,本篇不讲数组类型)的对象头由一个markOop和一个联合体组成,markOop就是MarkWord。这个联合体是指向类的元数据指针,未开启指针压缩时使用_klass,开启指针压缩时使用_compressed_klass(压缩Class)

markOopnarrowKlass的类型定义在/hotspot/src/share/vm/oops/oopsHierarchy.hpp头文件中:

[oopsHierarchy.hpp头文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
typedef juint narrowKlass;
typedef class markOopDesc* markOop;

因此,narrowKlass是一个juintjunit是在globalDefinitions_visCPP.hpp头文件中定义的,这是一个无符号整数,即4个字节。所以开启指针压缩之后,指向Klass对象的指针大小为4字节。

[/hotspot/src/share/vm/utilties/globalDefinitions_visCPP.hpp]
typedef unsigned int juint;

markOop则是markOopDesc类型指针,markOopDesc就是MarkWord。不知道你们有没有感觉到奇怪,在64位jvm中,markOopDesc指针是8字节,即64bit,确实刚好是MarkWord的大小,但是指针指向的不是一个对象吗?我们先看markOopDesc类。

[markOop.hpp文件:hotspot/src/share/vm/oops/markOop.hpp]
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
class markOopDesc: public oopDesc {
 ......
}

markOop.hpp头文件中给出了64bit的MarkWord存储的信息说明。markOopDesc类也继承oopDesc。如果单纯的看markOopDesc类的源码,根本找不出来,markOopDesc是用那个字段存储MarkWord的。而且,根据从各种来源的资料中,我们所知道的是,对象头的前8个字节存储的就是是否偏向锁、轻量级锁等等信息(全文都是以64位为例),所以不应该是个指针啊。

为了解答这个疑惑,我是先从markOopDesc类的源码中,找一个方法,比如,获取gc对象年龄的方法,看下jvm是从哪里获取的数据。

class markOopDesc: public oopDesc {
public:
 // 获取对象年龄
  uint  age() const {
    return mask_bits(value() >> age_shift, age_mask);
  }
  // 更新对象年龄
  markOop set_age(uint v) const {
    return markOop((value() & ~age_mask_in_place) | (((uintptr_t)v & age_mask) << age_shift));
  }
  // 自增对象年龄
  markOop incr_age() const {
    return age() == max_age ? markOop(this) : set_age(age() + 1);
  }
}

那么,value()这个方法返回的就是64bitMarkWord了。

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }
}

value方法返回的是一个指针,就是this。从set_age和incr_age方法中也可以看出,只要修改MarkWord,就会返回一个新的markOop(markOopDesc*)。难怪会将markOopDesc定义为markOop,就是将markOopDesc当成一个8字节的整数来使用。想要理解这个,我们需要先补充点c++知识,因此我写了个demo。

demo 分析

自定义一个类叫oopDesc,并且除构造函数和析构函数之外,只提供一个Show方法。

[.hpp文件]
#ifndef oopDesc_hpp
#define oopDesc_hpp
 
#include 
 
#include 
using namespace std;
 
// 将oopDesc* 定义为 oop
typedef class oopDesc* oop;
 
class oopDesc{
public:
    void Show();
};
#endif /* oopDesc_hpp */
 
[.cpp文件]
#include "oopDesc.hpp"
 
void oopDesc::Show(){
    cout << "oopDesc by wujiuye" <

使用oop(指针)创建一个oopDesc*,并调用show方法。

#include 
#include "oopDesc.hpp"
using namespace std;
int main(int argc, const char * argv[]) {
    oopDesc* o = oop(0x200);
    cout << o << endl;
    o->Show();
    return 0;
}

测试输出

0x200
oopDesc by wujiuye
Program ended with exit code: 0

因此,通过类名(value)可以创建一个野指针对象,将指针赋值为value,这样就可以使用this作为MarkWord了。如果在oopDesc中添加一个字段,并提供一个方法访问,程序运行就会报错,因此,这样创建的对象只能调用方法,不能访问字段。

总结:

对象布局大体由三部分构成

  1. 对象头【对象头的前64位(8byte)是MarkWord,后32位(4byte)是类的元数据指针(开启指针压缩)。】
  2. 实例数据
  3. 字节对齐(可有可无,若对象头加上实例数据是8的倍数时,则不存在字节对齐)

Hotspot 64位实现

JOL(java object layout --java 对象内存布局)_第2张图片

mark word中锁状态描述(根据后三位判断)

偏向锁位 1bit(是否偏向锁) 锁标志位 2bit 锁状态
0 01 无锁态(new)
1 01 偏向锁
- 00 轻量级锁(自旋锁、无锁、自适应自旋锁)
- 10 重量级锁
- 11 GC 标记

mark word

hotspot中对于对象头的描述

//  32 bits:  32位操作系统
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:  64位操作系统
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)  // 无锁
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)  // 偏向锁
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // 轻量级锁、重量级锁
//  size:64 ----------------------------------------------------->| (CMS free block) 总长度

关于32位操作系统的对象头信息,参考文末!!!

下面是64位操作系统的对象头的描述翻译如下:

|--------------------------------------------------------------------------------------------------------------------------------------|

​ Object Header (128 bits)

|--------------------------------------------------------------------------------------------------------------------------------------|

| Mark Word (64 bits) | Klass Word (64 bits) |默认开启指针压缩(32bits) |--------------------------------------------------------------------------------------------------------------------------------------|
|unused:25|identity_hashcode:31(56) | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|--------------------------------------------------------------------------------------------------------------------------------------|
|thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|---------------------------------------------------------------------|----------------------------------------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|--------------------------------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|------------------------------------------------------------------------------------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------------------------------|

mark word用于存储对象的运行时记录信息,如哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等:

2.有属性-对象布局

/**
 * 带有属性的 实体类
 */
public class Student {
    private String name;
    private Integer age;
}
import org.openjdk.jol.info.ClassLayout;

public class Entity {
    public static void main(String[] args) {
        Student o = new Student();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}
// 输出结果(默认开启指针压缩):
com.brown.Student 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)            43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
  12    4     java.lang.Integer Student.age               0
  16    4     java.lang.String Student.name              null
  20    4     (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 输出结果(关闭指针压缩)【(-XX:-UseCompressedOops)】:
 com.brown.Student 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)           30 35 64 1c (00110000 00110101 01100100 00011100) (476329264)
  12    4              (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  16    8    java.lang.String Student.name                      null
  24    8   java.lang.Integer Student.age                        null
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

指针压缩对于内存的优化

​ 从输出结果可以看出,开通指针压缩时,该对象所占的内存是:24Byte,关闭指针压缩时,该对象所占的内存是:32Byte,节省25%的内存

​ 对象头大小的变化:

​ 关闭指针压缩时,对象头中元数据指针为Klass类型,占用8个字节,而开启指针压缩时,对象头中元数据指针为narrowKlass 类型,占用8个字节(上文已分析)

3.关于锁-对象布局

此处以synchronized为例,分析MarkWord中对象锁信息的存储情况

import org.openjdk.jol.info.ClassLayout;

public class Entity {
    public static void main(String[] args) {
        Entity entity = new Entity();
        
        // 打印java 对象内存布局
        synchronized (entity){
            System.out.println(ClassLayout.parseInstance(entity).toPrintable());
        }
    }
}
// 输出结果:
com.brown.Entity object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                            VALUE
  0     4     (object header)              f8 f5 4c 02 (11111000 11110101 01001100 00000010) (38598136)
  4     4     (object header)              00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  8     4     (object header)              28 30 9c 1b (00101000 00110000 10011100 00011011) (463220776)
  12    4     (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看出,现在打印的第一行MarkWord 结果,已经和1.空属性-对象布局中的输出结果不一样了。

MarkWordk为0x0000 0000 024c f5f8,二进制为0xb00000000 00000000 00000000 00000000 00000010 01001100 11110101 11111000

倒数第三位为"0",说明不是偏向锁状态,倒数两位为"00",因此,是轻量级锁状态,那么前面62位就是指向栈中锁记录的指针。

另外,可以看出,执行Synchronized代码块的时候,锁定对象

???:

计算用的和输出到的正好相反【把输出结果中的十六进制数倒过来拼一起就是这一串了】。这里涉及到一个知识点“大端存储与小端存储”(汇编语言)。

  • Little-Endian:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。
  • Big-Endian:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。

锁升级

锁的状态流转:

32位操作系统中对象头信息

JOL(java object layout --java 对象内存布局)_第3张图片

hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳

markOop中不同的锁标识位,代表着不同的锁状态:

数组在内存中的存储布局

JOL(java object layout --java 对象内存布局)_第4张图片

参考:

  1. *并发编程----4、对象头详解
  2. JVM学习笔记 番外4 - synchronized 锁状态
  3. *JAVA 对象头解析
  4. https://www.bilibili.com/video/BV1xK4y1C7aT?p=3

你可能感兴趣的:(JVM,java,后端)