Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:
对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头:比如hash码,对象所属的年代,对象锁,锁状态标识,偏向锁线程ID,偏向时间,数组长度(数据对象才有)等。
实例数据:存放类的属性数据信息,包括父类的属性信息。
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
HotSpot虚拟机的对象头包括:
Mark Word:
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别是32bit和64bit,官方称它为 MarkWord
Klass Pointer
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。32位4字节,64位开启指针压缩或最大堆内存<32G时4字节,否则8字节。
JDK1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
数组长度(只有数据对象有):
如果对象是一个数组,那在对象头中还必须有一块数据用于记录数组长度。4字节
使用JOL工具查看内存布局
给大家推荐一个可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized 加锁加在对象上,对象是如何记录锁状态的?
锁状态被记录在每个对象的对象头的Mark Word 中。
Mark Word 是如何记录锁状态的:
HotSpot通过MarkOop类型实现 Mark Word , 具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在级小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。
简单点理解就是:MarkWord 结构搞的这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。
hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。
在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
64位JVM下的对象结构描述
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针
Mark Word中锁标记枚举:
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
更直观的理解方式:
偏向锁:
偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,
因此为了消除数据在无竞争情况下锁重入的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果
/***StringBuffer内部同步***/
public synchronized int length() {
return count;
}
//System.out.println 无意识的使用锁
public void println(String x) {
synchronized (this) {
print(x); newLine();
}
当JVM启用了偏向锁模式 (JDK6默认开启),新创建的对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态。
偏向锁延迟偏向:
偏向锁模式存在偏向锁延迟机制:HotSpot虚拟机在启动后有4s的延迟才会对每个新建的对象开启偏向锁模式,JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。这个过程中会使用大量的synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。
为了减少初始化时间,JVM默认延迟加载偏向锁。
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
System.out.printf(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
System.out.printf(ClassLayout.parseInstance(new Object()).toPrintable());
偏向锁状态跟踪:
package com.juc.demo1;
import org.openjdk.jol.info.ClassLayout;
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.printf(ClassLayout.parseInstance(new Object()).toPrintable());
// HotSpot 虚拟机在启动后有个4s的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+ "开始执行、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName()+ "获取锁执行中、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+ "释放锁、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T1").start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
思考: 如果对象调用了hashCode,还会开启偏向锁模式吗?
public static void main(String[] args) throws InterruptedException {
System.out.printf(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj = new Object();
obj.hashCode();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
偏向锁撤销之调用对象HashCode:
调用锁对象的obj.hashCode();或者System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。
因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashCode的。
轻量锁会在锁记录中记录hashCode
重量锁会在Monitor中记录hashCode
当对象处于可偏向(也是就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向。
当对象可偏向时,MarkWord将变成未锁定状态,并只能升级轻量锁。
当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级为重量锁
偏向锁撤销之调用wait/notify
偏向锁状态执行obj.notify() 会升级为轻量锁,调用obj.wait(timeout) 会升级为重量锁
轻量锁:
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word的结构也变为轻量级锁的结构。轻量级锁所适应场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
轻量级锁跟踪:
package com.juc.demo1;
import org.openjdk.jol.info.ClassLayout;
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.printf(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj = new Object();
obj.hashCode();
// System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+ "开始执行、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName()+ "获取锁执行中、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+ "释放锁、。。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T1").start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
思考:轻量级锁是否可以降级为偏向锁?
测试:锁升级场景 偏向锁升级轻量级锁
模拟两个线程轻微竞争场景:
package com.juc.demo1;
import org.openjdk.jol.info.ClassLayout;
public class LightWeightLockDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj = new Object();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "开始执行。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "获取锁执行中。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName() + "释放锁。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T1").start();
Thread.sleep(10);
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "开始执行。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "获取锁执行中。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName() + "释放锁。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T2").start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
轻量级锁膨胀为重量级锁:
package com.juc.demo1;
import org.openjdk.jol.info.ClassLayout;
public class WeightLockDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj = new Object();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "开始执行。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "获取锁执行中。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName() + "释放锁。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T1").start();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "开始执行。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "获取锁执行中。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName() + "释放锁。。。。。"+ClassLayout.parseInstance(obj).toPrintable());
},"T2").start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
总结:锁对象状态转换
synchronized 锁优化:
偏向锁批量重偏向&批量撤销
从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步代码块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获取锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或者升级为轻量锁,会消耗一定的性能,所以在
多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。 于是,就有了批量偏向与批量撤销的机制。
原理:
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑
应用场景:
批量重偏向机制是为了解决: 一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,
这样会导致大量的偏向锁撤销操作。
批量撤销机制是为了解决: 在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
JVM的默认参数值:
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值
测试:批量重偏向
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,
重偏向会重置对象 的 Thread ID
package com.juc.demo1;
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead1").start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
System.out.println("打印thead1,list中第20个对象的对象头:");
System.out.println((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if (i >= 15 && i <= 21 || i >= 38) {
System.out.println("thread2-第" + (i + 1) + "次加锁执行中\t" + ClassLayout.parseInstance(obj).toPrintable());
}
}
if (i == 17 || i == 19) {
System.out.println("thread2-第" + (i + 1) + "次释放锁\t" + ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead2").start();
LockSupport.park();
}
}
测试结果:
thread1: 创建50个偏向线程thread1的偏向锁 1-50 偏向锁
1-18 偏向锁撤销,升级为轻量级锁 (thread1释放锁之后为偏向锁状态)
19-40 偏向锁撤销达到阈值(20),执行了批量重偏向 (测试结果在第19就开始批量重偏向了)
测试:批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
public static void main(String[] args) throws InterruptedException {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead1").start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
log.debug("打印thead1,list中第20个对象的对象头:");
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead2").start();
Thread.sleep(3000);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock =list.get(i);
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug("thread3-第" + (i + 1) + "次准备加锁\t"+
ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock){
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+
ClassLayout.parseInstance(lock).toPrintable());
}
}
}
},"thread3").start();
Thread.sleep(3000);
log.debug("查看新创建的对象");
log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
LockSupport.park();
测试结果:
thread3:
1-18 从无锁状态直接获取轻量级锁 (thread2释放锁之后变为无锁状态)
19-40 偏向锁撤销,升级为轻量级锁 (thread2释放锁之后为偏向锁状态)
41-50 达到偏向锁撤销的阈值40,批量撤销偏向锁,升级为轻量级锁 (thread1释放锁之后为偏向锁状态)
新创建的对象: 无锁状态
总结:
1.批量重偏向和批量撤销是针对类的优化,和对象无关
2.偏向锁重偏向一次之后不可再次重偏向
3.当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利。
自旋优化:
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
Java 7 之后不能控制是否开启自旋功能
注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化:
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
锁消除:
锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
/**
* 锁消除
* -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
* -XX:-EliminateLocks 关闭锁消除
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
public static void main(String[] args) throws InterruptedException {
LockEliminationTest demo = new LockEliminationTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
demo.append("aaa", "bbb");
}
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start) + " ms");
}
StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。
测试结果: 关闭锁消除执行时间4688 ms 开启锁消除执行时间:2601 ms
逃逸分析(Escape Analysis)
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域。
方法逃逸(对象逃出当前方法):
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程):
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations //开启标量替换(默认打开)
测试:
/**
*
* 进行两种测试
* 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
* VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 开启逃逸分析 jdk8默认开启
* VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 执行main方法后
* jps 查看进程
* jmap -histo 进程ID
*
*/
@Slf4j
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
log.info("执行时间:" + (end - start) + " ms");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
/**
* JIT编译时会对代码进行逃逸分析
* 并不是所有对象存放在堆区,有的一部分存在线程栈空间
* Ponit没有逃逸
*/
private static String alloc() {
Point point = new Point();
return point.toString();
}
/**
*同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
/**
* 标量替换
*
*/
private static void test2() {
Point point = new Point(1,2);
System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
// int x=1;
// int y=2;
// System.out.println("point.x="+x+"; point.y="+y);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Point{
private int x;
private int y;
测试结果:开启逃逸分析,部分对象会在栈上分配