转载地址:https://www.jianshu.com/p/ec28e3a59e80;
https://www.cnblogs.com/sten/p/5651484.html;
https://www.cnblogs.com/f-zhao/p/6189104.html?utm_source=itdadao&utm_medium=referral;
一、Java对象结构
概述
对象实例由对象头、实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度;
| 类型 | 32位JVM | 64位JVM|
| ------ ---- | ------------| --------- |
| markword | 32bit | 64bit |
| 类型指针 | 32bit |64bit ,开启指针压缩时为32bit |
| 数组长度 | 32bit |32bit |
header.png
compressed_header.png
可以看到
- 开启指针压缩时,markword占用8bytes,类型指针占用8bytes,共占用16bytes;
- 未开启指针压缩时,markword占用8bytes,类型指针占用4bytes,但由于java内存地址按照8bytes对齐,长度必须是8的倍数,因此会从12bytes补全到16bytes;
- 数组长度为4bytes,同样会进行对齐,补足到8bytes;
另外从上面的截图可以看到,开启指针压缩之后,对象类型指针为0xf800c005,但实际的类型指针为0x7c0060028;那么指针是如何压缩的呢?实际上由于java地址一定是8的倍数,因此将0xf800c005*8即可得到实际的指针0x7c0060028,关于指针压缩的更多知识可参考官方文档。
markword结构
markword的结构,定义在markOop.hpp文件:
32 bits:
--------
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:
--------
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)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
[ptr | 00] locked ptr points to real header on stack
[header | 0 | 01] unlocked regular object header
[ptr | 10] monitor inflated lock (header is wapped out)
[ptr | 11] marked used by markSweep to mark an object
由于目前基本都在使用64位JVM,此处不再对32位的结构进行详细说明:
偏向锁标识位 | 锁标识位 | 锁状态 | 存储内容 |
---|---|---|---|
0 | 01 | 未锁定 | hash code(31),年龄(4) |
1 | 01 | 偏向锁 | 线程ID(54),时间戳(2),年龄(4) |
无 | 00 | 轻量级锁 | 栈中锁记录的指针(64) |
无 | 10 | 重量级锁 | monitor的指针(64) |
无 | 11 | GC标记 | 空,不需要记录信息 |
此处,有几点要注意:
- 如果对象没有重写hashcode方法,那么默认是调用os::random产生hashcode,可以通过System.identityHashCode获取;os::random产生hashcode的规则为:next_rand = (16807seed) mod (2*31-1),因此可以使用31位存储;另外一旦生成了hashcode,JVM会将其记录在markword中;
- GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15;
- 当处于轻量级锁、重量级锁时,记录的对象指针,根据JVM的说明,此时认为指针仍然是64位,最低两位假定为0;当处于偏向锁时,记录的为获得偏向锁的线程指针,该指针也是64位;
We assume that stack/thread pointers have the lowest two bits cleared.
ObjectMonitor* monitor() const {
assert(has_monitor(), "check");
// Use xor instead of &~ to provide one extra tag-bit check.
return (ObjectMonitor*) (value() ^ monitor_value);//monitor_value=2,value最右两位为10,因此异或之后最右两位为0
}
JavaThread* biased_locker() const {
assert(has_bias_pattern(), "should not call this otherwise");
return (JavaThread*) ((intptr_t) (mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place))));
//~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place)为11111111111111111111110010000000,计算后的结果中,低10位全部为0;
}
由于java中内存地址都是8的倍数,因此可以理解为最低3bit为0,因此假设轻量级和重量级锁的最低2位为0是成立的;但为什么偏向锁的最低10位都是0?查看markOop.hpp文件,发现有这么一句话:
// Alignment of JavaThread pointers encoded in object header required by biased locking
enum { biased_lock_alignment = 2 << (epoch_shift + epoch_bits)
//epoch_shift+epoch_bits=10
};
thread.hpp中重载了operator new:
void* operator new(size_t size) { return allocate(size, true); }
// ======= Thread ========
// Support for forcing alignment of thread objects for biased locking
void* Thread::allocate(size_t size, bool throw_excpt, MEMFLAGS flags) {
if (UseBiasedLocking) {
const int alignment = markOopDesc::biased_lock_alignment;//10
size_t aligned_size = size + (alignment - sizeof(intptr_t));
void* real_malloc_addr = throw_excpt? AllocateHeap(aligned_size, flags, CURRENT_PC)
: os::malloc(aligned_size, flags, CURRENT_PC);
void* aligned_addr = (void*) align_size_up((intptr_t) real_malloc_addr, alignment);
assert(((uintptr_t) aligned_addr + (uintptr_t) size) <=
((uintptr_t) real_malloc_addr + (uintptr_t) aligned_size),
"JavaThread alignment code overflowed allocated storage");
if (TraceBiasedLocking) {
if (aligned_addr != real_malloc_addr)
tty->print_cr("Aligned thread " INTPTR_FORMAT " to " INTPTR_FORMAT,
real_malloc_addr, aligned_addr);
}
((Thread*) aligned_addr)->_real_malloc_address = real_malloc_addr;
return aligned_addr;
} else {
return throw_excpt? AllocateHeap(size, flags, CURRENT_PC)
: os::malloc(size, flags, CURRENT_PC);
}
}
如果开启了偏移锁,在创建线程时,线程地址会进行对齐处理,保证低10位为0
实例数据
实例数据中主要包括对象的各种成员变量,包括基本类型和引用类型;static类型的变量会放到java/lang/Class中,而不会放到实例数据中;
对于引用类型的成员(包括string),存储的指针;对于基本类型,直接存储内容;通常会将基本类型存储在一起,引用类型存储在一起;
例如类Test的成员定义如下:
private static Test t1=new Test();
private Test t2;
private int a=5;
private Integer b=7;
private String c="112";
private BigDecimal d=new BigDecimal("5");
private long e=9l;
body.png
可以看到long e、int a为基本类型,存储在一起;其它的引用类型存储在一起;int占用4bytes,不足8bytes,自动补足到8bytes;
二、JVM锁说明
以前Synchronised关键字加锁效率问题,经常受到吐槽。后来java的开发团队进行了优化,引入了偏向锁、自旋锁、轻量锁,性能有了很大的提升。下面我们来分析下这里面的过程和原理。
最初阶段,我们的代码段或者方法加syncronized关键字,如下:
syncronized(object) { // do something }
执行这段代码时,其实相当于,在这段代码前面加了monitorenter指令;后面加了monitorexit指令。如下:
monitorenter // do something monitorexit
这两指令都得和操作系统连接,相对来说是比较重的,比较耗时的。
但是,在代码里加synchronised 关键字时,只是全面考虑,很多情况只有一个线程在执行,如果在有这种情况下,就有些浪费了;另外,有时,即使多线程访问,他的等待时间是很短的,比monitorenter相关指令还轻,这也是可想办法优化的。
在叙述相关优化锁之前,我们先说下java 对象头部分的 Mark Word。在32位虚拟机上,他的长度是32位;在64位虚拟机上,他的长度是64位。
普通情况下,Mark Word的格式如下:
hash:25 ------------>| age:4 | biased_lock:1 lock:2
最后部分2个比特,是固定的,表示对象锁的状况
01 表示无锁,或者偏向锁; 00表示轻量级锁;10表示重量级锁
第三部分是标识是否偏向锁。当最后部分为01时,第三部分为0表示普通无锁对象,第三部分为1表示偏向锁;最后部分为其他值,第三部分不存在。
age表示分代年龄;hash表示对象hash值。
普通无锁:对象hash:25 ------------>| age:4 | 0 | 01
偏向锁:偏向锁的线程和时间戳:25 ------------>| age:4 | 1 | 01
轻量锁:指向栈中锁记录的指针:30 | 00
重量锁:指向互斥量(重量级锁)的指针:30 | 10
java1.6之后,对象有4中状态:无锁态,偏向锁态,轻量锁态,重量锁态。
java程序执行,没有遇到同步块时,都是无锁态。Mark Word如上普通无锁的情况。
遇到同步块之后,Mark Word从普通无锁变成偏向锁的情况,通过CAS加锁。如果是相同线程在次进入,不受影响。执行完成,CAS解锁,Mark Word又变成普通无锁的情况。
如果在偏向锁中,遇到锁竞争的情况。先线程停止,通过CAS锁升级到轻量锁,成功的一方继续执行,另一方开始自旋操作。待他完了继续执行。这里的自旋有个智能学习的策略。假设每个同步块的初始自旋次数是20次,A同步块的线程自旋时,每次很小的自旋次数就获得了执行时间,那后面他的自旋信用会很高,会超过20次往上升;B同步块的线程自旋时,每次都需要很大的自旋次数才能获得执行时间,后面就会把他的自旋次数从20往下降或者不给自旋机会。
当然,自旋失败后,锁得再次升级,通过CAS操作从轻量锁升级到重量锁,这就回到一开始的moniterenter指令和moniterexit指令了。
jvm还有其他一些锁的优化措施。
锁清除:在某些情况下,同步块中的代码不可能被其他线程访问修改的时候,这时候的锁是可以清除的。
锁粗化:在一些代码中,会频繁的加锁,加锁,加锁;当然,有时并不是程序员这么编写的,因为我们的jdk api中也会加些锁操作。我们得频繁的做互斥操作,可以把几个同步块合并成一个同步块来优化执行。
一、锁在Java虚拟机中的实现与优化
1.1 偏向锁
偏向锁是JDK 1.6 提出的一种锁优化方式。其核心思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需进行相关的同步操作,从而节省了操作时间。如果在此之前有其他线程进行了锁请求,则锁退出偏向模式。在JVM中使用-XX:+UseBiasedLocking可以设置启用偏向锁。
当锁对象处于偏向模式时,对象头会记录获取锁的线程
[JavaThread* | epoch | age | 1 | 01]
这样,当该线程再次尝试获得锁时,通过Mark Word的线程信息就可以判断当前线程是否持有偏向锁。
偏向锁在锁竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持在偏向模式,此时,使用锁偏向不仅得不到性能的优化,反而有可能降低系统性能。
1.2 轻量级锁
如果偏向锁失败,Java虚拟机会让线程申请轻量级锁。轻量级锁在虚拟机内部,使用一个称谓BasicObjectLock的对象实现,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈的栈帧中。在BasicLock对象内部还维护者displaced_header字段,他用于备份对象头部的Mark Word。
当一个线程持有一个对象的锁时,对象头部Mark Word如下:
[ptr | 00] locked
末尾两位比特为00,整个Mark Word为指向BasicLock对象的指针。由于BasicObjectLock对象在线程栈中,因此该指针必然指向持有该锁的线程栈空间。当需要判断某一线程是否持有该对象锁时,也只需简单的判断对象头的指针是否在当前线程的栈地址范围即可。同时,BasicLock对象的displaced_header字段,备份了元对象的Mark Word内存。BasicObjectLock对象的obj字段则指向该对象。
1.3 锁膨胀
当轻量级锁失败,虚拟机就会使用重量级锁。在使用重量级锁时,对象的Mark Word如下:
[ptr | 10] monitor
末尾的2比特标记位被置为10。整个Mark Word表示指向monitor对象的指针。
1.4 自旋锁
锁膨胀后,进入ObjectMonitor的enter(),线程很可能会在操作系统层面被挂起,这样线程上下文切换的性能损失就比较大。因此,在锁膨胀后,虚拟机会做最后的争取,希望线程可以尽快进入临界区而避免被操作系统挂起。一种较为有效的手段就是使用自旋锁。
自旋锁可以使线程在没有取得锁时,不被挂起,而转而去执行一个空循环(即所谓的自旋),在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了执行被挂起的操作,反而浪费了系统资源。
在JDK 1.6 中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。
在JDK 1.7中,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁。自旋锁总是会执行,自旋次数也由虚拟机自行调整。
1.5 锁消除
锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
二、锁在应用层的优化思路
2.1 减少锁持有时间
public Matcher matcher(CharSequence input) {
if (!compiled) {
synchronized(this) {
if (!compiled)
compile();
}
}
Matcher m = new Matcher(this, input);
return m;
}
2.2 减小锁粒度
典型的场景就是ConcurrentHashMap类的实现。ConcurrentHashMap将整个HshMap分成若干段(Segment),每个段都是一个子HashMap。
如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
2.3 锁分离
锁分离是减小锁粒度的一个特例。他依据应用程序的功能特点,将一个独占锁分成多个锁。一个典型的案例就是java.util.concurrent.LinkedBlockingQueue的实现。
在LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中zeng增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但由于LinkedBlockingQueue是基于链表的,因此,两个操作分别作用于队列的前端和尾端,从理论上来说,两者并不冲突。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
2.4 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
为此,虚拟机在遇到一连串连续的对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。这个操作叫做锁的粗化。
三、无锁
可以使用yi'z一种称为非阻塞同步的方法,这种方法不需要使用“锁”(因此称之为无锁),但是依然能确保数据和程序在高并发环境下,保持多线程间的一致性。
3.1 理解CAS
CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值=E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
3.2 原子操作
为了能让CAS操作被Java应用程序充分使用,在JDK的java.util.concurrent.atomic包下,有一组使用无锁算法实现的原子操作类,主要有AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicLongArray和AtomicReference等。他们分别封装了对整数、整数数组、长整型、长整型数组和普通对象的多线程安全操作。
3.3 LongAdder
在JDK 1.8中引入了LongAdder。结合减小锁粒度与ConcurrentHashMap的实现,我们可以想到一种对传统AtomicInteger等原子类的改进思路。虽然在CAS操作中没有锁,但是像减小锁粒度这种分离热点的思路依然可以使用。一种可行的方案就是仿造ConcurrentHashMap,将热点数据分离。比如,可以将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计数,而最终的技术结果,则为这个数组的求和累加。其中,热点数据value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度。LongAdder正是使用了这种思想。
四、理解Java内存模型
- 原子性
- 有序性
- 可见性
- Happens-Before原则