title: Java对象内存布局
date: 2021-05-05 15:58:39
tags: Java;JVM
Java对象内存布局
-
引子
-
运行时数据区域
-
虚拟机对象
-
锁升级(Synchronized)
1.引子
Java与C++之间隔着一堵由内存分配和垃圾回收筑城围墙,墙外面的人想进去,墙里面的人想出来。
正是因为Java虚拟机的内存分配和垃圾回收机制,减轻了程序员在编码时内存分配的负担,可以把更多精力放在实现上。
任何事物都有利弊,享受Java虚拟机的便利,就要承担相应的风险。
当Java程序内存出现泄漏的时候,如果没有搞懂虚拟机的内存分配及对象内存布局,就像隔靴搔痒,很难排查问题。
2.运行时数据区域

- 程序计数器 pcRegister
当前线程所执行字节码的行号指示器
- Java虚拟机栈 VM Stack
Java方法执行的线程内存模型,方法执行就会创建栈帧Stack Frame
- 本地方法栈 Native Stack
使用Native本地方法是创建
- Java堆 Java Heap
所有的对象实例以及数组都应在堆上分配
- 方法区 Method Area
存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 运行常量池 Runtime Constant Pool
运行常量池是方法区的一部分;常量池表用于存放编译期生成的各种字面量与符号引用
- 直接内存 Direct Memory
NIO 基于Channel和Buffer的I/O方式,使用Native函数直接分配堆外内存。
利用Java堆中的DirectByteBuffer对象作为这块对象的内存引用进行操作,避免在Java堆和Native堆来回复制数据
3.虚拟机对象
Java对象的创建方式
- new (new + invokespecial)
- clone (implements java.lang.Cloneable)
- 反射
- 反序列化(implements Serializable)
对象初始化
- 实例变量初始化
- 实例代码块初始化
- 构造函数初始化
Java对象初始化过程中,主要涉及三种执行对象初始化的结构,
分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化
对象的内部布局
对象在堆内存中的存储布局:
- 对象头
- 实例数据
- 对齐填充
//导入依赖工具jol可以查看对象的内存布局
import org.openjdk.jol.info.ClassLayout;
public class jolTest {
public static class UserTest{
}
public static void main(String[] args) {
UserTest userTest = new UserTest();
System.out.println(ClassLayout.parseInstance(userTest).toPrintable());
}
}
/*
jolTest$UserTest 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 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/
// 16B = 8B(mark word) + 4B(klass pointer) +0B(instance data)+4B (padding)
// kclass pointer 4B是开启指针压缩
<!--JOL 查看对象内存布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
- MarkWord
4.锁升级
jdk6之前,synchronized关键字加锁是无差别的重量级锁
锁升级:偏向锁,轻量级锁,重量级锁
如上图,锁在markword中占3bit:
偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit
-
1.锁对象刚创建,没有任何线程竞争,对象处于无锁状态+不可偏向状态
大小端转换: 00000001
偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit = 0 01
jolTest$UserTest 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 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,
可以通过jvm参数取消这个延迟时间
创建的对象状态为 对象处于无锁状态+可偏向状态
偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit = 1 01
// -XX:BiasedLockingStartupDelay=0
jolTest$UserTest 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 2.在没有线程竞争的条件下,第一个获取锁的线程通过
CAS
将自己的threadId写入到该对象的mark word
中,若后续该线程再次获取锁,需要比较当前线程threadId和对象mark word
中的threadId
是否一致,如果一致那么可以直接获取,并且锁对象始终保持对该线程的偏向,也就是说偏向锁不会主动释放
public static void main(String[] args) {
UserTest userTest = new UserTest();
synchronized (userTest){
System.out.println(ClassLayout.parseInstance(userTest).toPrintable());
}
System.out.println(ClassLayout.parseInstance(userTest).toPrintable());
synchronized (userTest){
System.out.println(ClassLayout.parseInstance(userTest).toPrintable());
}
}
// terminal print
first get synchronized lock
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
------------------------------------------------
unlock
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
------------------------------------------------
second get synchronized lock
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 88 80 72 (00000101 10001000 10000000 01110010) (1921026053)
- 3.当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取
CAS
的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态间转换的消耗
主线程首先对user对象加锁,首次加锁为101偏向锁
子线程等待主线程释放锁后,对user对象加锁,这时将偏向锁升级为00轻量级锁
轻量级锁解锁后,user对象无线程竞争,恢复为001无锁态,并且处于不可偏向状态。如果之后有线程再尝试获取user对象的锁,会直接加轻量级锁,而不是偏向锁
public static void main(String[] args) throws InterruptedException {
final UserTest userTest = new UserTest();
synchronized (userTest){
System.out.println("Main = "+ClassLayout.parseInstance(userTest).toPrintable());
}
Thread thread = new Thread(new Runnable() {
public void run() {
synchronized (userTest){
System.out.println("Thread = "+ClassLayout.parseInstance(userTest).toPrintable());
}
}
});
thread.start();
thread.join();
System.out.println("End = "+ClassLayout.parseInstance(userTest).toPrintable());
}
//terminal print
Main = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 80 e3 (00000101 10010000 10000000 11100011) (-478113787)
Thread = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 a9 d4 03 (01100000 10101001 11010100 00000011) (64268640)
End = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
- 4.两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这时
mark word
中的指针指向的是monitor
对象(也被称为管程或监视器锁)的起始地址
new Thread(new Runnable() {
public void run() {
synchronized (userTest){
System.out.println("Thread1 = "+ClassLayout.parseInstance(userTest).toPrintable());
try{
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
synchronized (userTest){
System.out.println("Thread2 = "+ClassLayout.parseInstance(userTest).toPrintable());
try{
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
try{
TimeUnit.SECONDS.sleep(4);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("main:"+ClassLayout.parseInstance(userTest).toPrintable());
// Teriminal print
Thread1 = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)
Thread2 = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)
main:jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa 49 85 97 (11111010 01001001 10000101 10010111) (-1752872454)
指针压缩
Klass Pointer 类型指针,jdk6之后默认开启指针压缩
#开启指针压缩:
-XX:+UseCompressedOops
#关闭指针压缩:
-XX:-UseCompressedOops
//关闭指针压缩后
Thread1 = jolTest$UserTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5a d1 81 4b (01011010 11010001 10000001 01001011) (1266798938)
4 4 (object header) cb 7f 00 00 (11001011 01111111 00000000 00000000) (32715)
8 4 (object header) b0 37 40 a3 (10110000 00110111 01000000 10100011) (-1556072528)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 实现逻辑
由于使用了8字节对齐后每个对象的地址偏移量后3位必定为0,所以在存储的时候可以将后3位0抹除(
转化为bit是抹除了最后24位),在此基础上再去掉最高位,就完成了指针从8字节到4字节的压缩。
而在实际使用时,在压缩后的指针后加3位0,就能够实现向真实地址的映射
数组长度
public static void main(String[] args) throws InterruptedException {
UserTest[] userTests = new UserTest[2];
System.out.println("main:"+ClassLayout.parseInstance(userTests).toPrintable());
}
main:[LjolTest$UserTest; 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) 81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 8 jolTest$UserTest [LjolTest$UserTest;.<elements> N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
8字节mark word
4字节klass pointer
4字节数组长度,值为2,表示数组中有两个元素
开启指针压缩后每个引用类型占4字节,数组中两个元素共占8字节
参考
2.深入理解Java虚拟机(第三版)- 周志华
3.[深入理解Java对象的创建过程:类的初始化与实例化-书呆子Rico](https://blog.csdn.net/justloveyou_/article/details/72466416?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-3.control)