在JVM中,对象在内存中的布局分为三个部分:对象头、实例数据和对齐填充
HotSpot虚拟机的对象头(Object Header)包括两部分信息:
第一部分"Mark Word":用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.
第二部分"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )
注意:
markword : 32位 占4字节 ,64位 占 8字节
klasspoint : 开启压缩占4字节,未开启压缩 占 8字节。
这一部分用于存储对象的类型指针,该指针指向它的类元数据,jvm通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64的JVM将会比32位的JVM多耗费50的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩。其中 oop即ordinary object pointer 普通对象指针。
-XX:+UseCompressedOops 开启指针压缩
-XX:-UseCompressedOops 不开启指针压缩
对象头:Mark Word+Klass Pointer类型指针 未开启压缩的情况下
32位 Mark Word =4bytes ,类型指针 4bytes ,对象头=8bytes =64bits
64位 Mark Word =8bytes ,类型指针 8bytes ,对象头=16bytes=128bits;
注意:默认情况下,开启了指针压缩 可能只有12字节,必须是8字节的整数倍
所以会额外补充4个字节。
就是定义类中的成员属性
对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
maven依赖
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10version>
dependency>
public class Test002 extends Thread {
private DemoLock demoLock = new DemoLock();
@Override
public void run() {
}
public void create() {
synchronized (demoLock) {
}
}
public static void main(String[] args) {
DemoLock demoLock = new DemoLock();
System.out.println(Integer.toHexString(demoLock.hashCode()));
System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());
}
}
64位虚拟机 对象头占用16个字节— 没有压缩指针
32位虚拟机 对象头占用8个字节
1、bit --位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
2、byte --字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。
1Byte = 8bit (1B=8bit)
1KB = 1024Byte(字节)=8*1024bit
1MB = 1024KB
1GB = 1024MB
数据类型 | 位 |
---|---|
int | 32bit |
short | 16bit |
long | 64bit |
byte | 8bit |
char | 16bit |
float | 32bit |
double | 64bit |
boolean | 1bit |
启用指针压缩-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
1.默认开启指针压缩
Object objectLock = new Object();
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
public class Test03 {
public static void main(String[] args) {
DemoLockObject demoLockObject = new DemoLockObject();
System.out.println(ClassLayout.parseInstance(demoLockObject).toPrintable());
}
static class DemoLockObject {
int j = 4;
long i = 1;
boolean m = false;
}
}
在开启了指针压缩的情况下:
DemoLockObject 对象头 12个字节
实例数据 int j=4 4个字节 long i=1 8个字节 boolean m=false 1个字节
对齐补充 7个字节,总共32个字节。
hotspot\src\share\vm\oops\markOop.hpp
HotSpot----阿里巴巴
注意:该描述是为64位虚拟机
① 哈希值:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的
②GC分代年龄(占4位):记录幸存者区对象被GC之后的年龄age,一般age为15(阈值为15的原因是因为age只有4位最大就可以将阈值设置15)之后下一次GC就会直接进入老年代,要是还没有等到年龄为15,幸存者区就满了怎么办,那就下一次GC就将大对象或者年龄大者直接进入老年代。
③ 锁状态标志:记录一些加锁的信息(我们都是使用加锁的话,在底层是锁的对象,而不是锁的代码,锁对象的话,那会改变什么信息来表示这个对象被改变了呢?也就是怎么才算加锁了呢?
4位字符编码的最大值
如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
00010110 00000000 00000000 00000000
01110101 11010100 00011100
偏向锁标识位 | 锁标识位 | 锁状态 | 存储内容 |
---|---|---|---|
0 | 01 | 未锁定 | hash code(31),年龄(4) |
1 | 01 | 偏向锁 | 线程ID(54),时间戳(2),年龄(4) |
无 | 00 | 轻量级锁 | 栈中锁记录的指针(64) |
无 | 10 | 重量级锁 | monitor的指针(64) |
无 | 11 | GC标记 | 空,不需要记录信息 |
偏向锁 启动jvm参数 设置:-XX:BiasedLockingStartupDelay=0
101----1偏向锁 锁标志位01
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0
public class Test04 {
private static Object objectLock = new Object();
public static void main(String[] args) throws InterruptedException {
//-XX:BiasedLockingStartupDelay=0 强制开启
// System.out.println(">>----------------无锁状态-------------------<<");
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
synchronized (objectLock) {
try {
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
Thread.sleep(5000);
System.out.println("..子线程..");
} catch (Exception e) {
}
}
}
}, "子线程1").start();
// Thread.sleep(1000);
// sync();
}
public static void sync() throws InterruptedException {
System.out.println(" 主线程获取锁 重量级别锁");
//11010000 01000000
synchronized (objectLock) {
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
}
}
}
public class Test04 {
private static Object objectLock = new Object();
public static void main(String[] args) throws InterruptedException {
//-XX:BiasedLockingStartupDelay=0 强制开启
// System.out.println(">>----------------无锁状态-------------------<<");
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
synchronized (objectLock) {
try {
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
Thread.sleep(5000);
System.out.println("..子线程..");
} catch (Exception e) {
}
}
}
}, "子线程1").start();
Thread.sleep(1000);
sync();
}
public static void sync() throws InterruptedException {
System.out.println(" 主线程获取锁 重量级别锁");
//11010000 01000000
synchronized (objectLock) {
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
}
}
}
当我们在使用synchronized锁,通过javap 反汇编指令可以得出:
synchronized锁底层monitorenter和monitorexit指令实现。
Monitorenter:获取锁-----lock.lock();
Monitorexit:释放锁 ---- lock.unlock();
也就是底层实际上基于JVM级别C++对象
当多个线程在获取锁的时,会创建一个monitor(监视器)对象,该对象
成员变量有 owner 拥有锁的线程、recursions 重入次数等。
同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权
有执行monitorenter 和monitorexit,其中monitorexit指令有两个,分别代表正常退出和异常退出。下面我们看看这两个指令的官方文档的介绍
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
Hostpot 标准
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
简单翻译:
每一个对象都会和一个监视器C++ monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
1.若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
简单翻译:
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
monitorexit释放锁:monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。为什么会有两个monitorexit,因为 Synchronized锁的同步代码块如果抛出异常的情况下,则自动释放锁。
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理
public static synchronized void count2() {
System.out.println();
}