JOL(java object layout --java 对象内存布局)
⚠⚠⚠本文以java普通对象为切入点,分析java的对象内存布局,数组见文末
maven地址👇👇👇,用0.9版本即可,新版本打印的信息简化
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
相关方法:
1.使用jol计算对象的大小(单位为字节):
ClassLayout.parseInstance(obj).instanceSize()
2.使用jol查看对象内部的内存布局:
ClassLayout.parseInstance(obj).toPrintable()
3.查看对象外部信息:包括引用的对象:
GraphLayout.parseInstance(obj).toPrintable()
4.查看对象占用空间总大小:
GraphLayout.parseInstance(obj).totalSize()
基础概念:
问题1. Java对象如何存储?
对象的实例(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
为对象头;
- object header:对象头
- 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)
markOop
与narrowKlass
的类型定义在/hotspot/src/share/vm/oops/oopsHierarchy.hpp
头文件中:
[oopsHierarchy.hpp头文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
typedef juint narrowKlass;
typedef class markOopDesc* markOop;
因此,narrowKlass
是一个juint
,junit
是在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()
这个方法返回的就是64bit
的MarkWord
了。
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 <stdio.h>
#include <iostream>
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" <<endl;
}
使用oop(指针)
创建一个oopDesc*
,并调用show
方法。
#include <iostream>
#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
中添加一个字段,并提供一个方法访问,程序运行就会报错,因此,这样创建的对象只能调用方法,不能访问字段。
总结:
对象布局大体由三部分构成:
- 对象头【对象头的前
64
位(8byte
)是MarkWord
,后32
位(4byte
)是类的元数据指针(开启指针压缩)。】 - 实例数据
- 字节对齐(可有可无,若对象头加上实例数据是8的倍数时,则不存在字节对齐)
Hotspot 64位实现
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位操作系统中对象头信息
hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳markOop中不同的锁标识位,代表着不同的锁状态:
数组在内存中的存储布局
参考:
- *并发编程----4、对象头详解
- JVM学习笔记 番外4 - synchronized 锁状态
- *JAVA 对象头解析
- https://www.bilibili.com/video/BV1xK4y1C7aT?p=3