关于Java虚拟机的各类问题探讨

1 篇文章 0 订阅

写在前面

鄙人之前读过周志明先生的《深入理解Java虚拟机》一书,虽然感觉受益匪浅,但是感觉读完以后对JVM仍然有很多疑问,在书中也找不到合适的答案,特此开一个博文,把我遇到的问题以及自己思考的结论都写出来,希望各路大神前来批评指正。

对JVM的问题

Java8之后,静态变量存储在哪

问题描述

《深入理解Java虚拟机》的第46页写道“JDK1.7时HopSpot虚拟机已经讲原本放在永久代中的字符串常量池、静态变量移出,到JDK1.8时完全摒弃了永久代的概念”。但是对静态变量从永久代中移出之后,在JDK1.7和JDK1.8中分别存放在哪里,并未阐述清楚。

笔者观点

笔者认为,Java8之后,静态变量存储堆中的各个类型对应的Class对象的实例数据部分。

为什么会有跨代引用问题

问题描述

《深入理解Java虚拟机》的第76页和77页关于跨代引用问题描述:“假如进行了一次只限于新生代区域的收集,但是新生代的对象是完全可能被老年代引用的,为了找出该区域的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代的所有对象来确保可达性分析结果的正群性,反过来也是一样。”
“跨代引用假说:跨代引用相对于同代引用来说仅占极少数。”,存在相互引用关系的两个对象,倾向于同事存在和消亡的。因此“不必再为少量的跨代引用去扫描整个老年代,只需要在新生代建立一个全局的数据结构,把老年代划分成若干小块,标记出老年代的哪一块内存会存在跨代引用。当发生Minor GC 时,只把包含跨代引用的小块内存的对象加入到GC Roots中扫描即可”。
对此,笔者认为,从GC Roots开始沿着引用关系扫描时,无论是老年代还是新生代中的对象,只要其与GC Roots直接或者间接相连,都会被扫描到。被老年代对象A引用的新生代对象B,只要老年代对象A与GC Roots相连,也会被沿着GC Roots—A—B这条路线扫描到,因此不存在扫描不到的问题,也就不需要把A再加入到GC Roots中进行扫描。周志明先生所说的跨代引用问题导致B扫描不到是怎么发生的呢?

笔者观点

在进行可达性分析时,虚拟机会判断一下当前需要扫描的对象在不在本次回收区域,不在的话直接不扫描。比如进行新生代收集时,如果虚拟机发现当前需要扫描的对象A是在老年代,则不再扫描A和A引用的对象。

自旋锁、偏向锁、轻量级锁、重量级锁升级条件和机制是什么

问题描述

《深入理解Java虚拟机》第13章描述了描述了JDK6以后对synchronized的互斥同步锁优化方式。互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给Java虚拟机的并发性带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程不值得。为此,虚拟机采用了自旋锁、偏向锁、轻量级锁和重量级锁等不同等级的锁定状态来优化同步锁。

自旋锁:如果物理机器有两个或以上处理器或者核心数,能让两个或以上的线程同时并行执行,就可以让后面请求锁的线程稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程是否会很快释放锁。为了让线程等待,就需要让线程执行一个忙循环,也就是自旋,这种等待机制就是自旋锁。

偏向锁:偏向锁适用于在无锁竞争的情况下去除一切同步措施,进一步提高程序的性能。当一个对象被第一个线程获取的时候,如果在以后的执行过程中该锁一直没有被其他线程获取,则持有偏向锁的线程将永远不需要进行同步。
如果虚拟机开启偏向锁的,当线程第一次获取对象的时候,对象的对象头中的Mark Word部分中的偏向模式会由“0”变为“1”,表示进入偏向模式。同时使用CAS将持有偏向锁的线程的ID写入Mark Word中。如果获取偏向锁成功,以后持有偏向锁的线程进入锁相关的代码块时,不需要进行任何同步操作。

轻量级锁:轻量级锁适用于“绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。在代码即将进入同步代码块的时候,如果同步对象没有被锁定,虚拟机首先会在当前线程的栈帧中建立一个名为锁记录的空间,用来存储对象的Mark Word拷贝。然后,虚拟机会把对象头中的Mark Word改为指向栈帧中锁记录的指针。如果修改成功,说明线程获取轻量级锁成功。如果修改失败,虚拟机会检查对象的Mark Word是不是指向当前线程的栈帧,是的话说明当前线程已经持有了对象的锁,可以直接进入同步代码块。否的话说明锁对象被其他线程抢占,则轻量级锁升级为重量级锁。

重量级锁:直接使用线程阻塞和切换来实现锁竞争。阻塞和唤醒线程是重量级操作,状态转换需要耗费处理器时间,可能用于线程状态转行的时间要比用户代码本事执行的时间还长。

笔者观点

笔者查询了各类网络资料,认为锁升级的机制和触发条件是这样的:

线程进入同步代码块的时候,通过Mark Word中的标志位检查对象当前的锁状态:
1、如果标志位是01,说明对象此时处于未锁定或可偏向状态。
此时再检查Mark Word中的偏向模式
1.1 、如果是偏向模式是0,说明对象此时没有被线程获取偏向锁,则通过CAS将线程ID写入Mark Word。如果写入成功则将偏向模式改为1,说明当前线程获取偏向锁成功,如果失败则说明出现锁竞争,则偏向锁升级为轻量级锁。
1.2、如果是偏向模式是1,则坚持Mark Word中的线程ID是不是当前线程的ID,是的话则直接进入同步代码块。不是的话,通过CAS将线程ID替换进Mark Word,如果替换成功则说明线程成功获取偏向锁,如果替换失败则偏向锁升级轻量级锁。
总之,偏向锁只存在于没有锁竞争的条件下,一旦出现锁竞争,则升级轻量级锁。
偏向锁升级轻量级锁的过程为:先在原持有锁的线程的栈帧中分配锁记录,并且拷贝对象头的Mark Word到锁记录中,再对象头中的Mark Word改为指向栈帧中锁记录的指针,原持有锁的线程的偏向锁就升级为轻量级锁了。然后,虚拟机会在当前竞争锁的线程中同样进行栈帧中分配锁记录,拷贝对象头的Mark Word到锁记录中两个操作。于是,当前线程也升级为轻量级锁模式开始竞争。

2、如果标志位是00,说明对象此时处于轻量级锁状态。
如果对象处于轻量级锁状态,虚拟机会在当前竞争锁的线程中同样进行栈帧中分配锁记录,拷贝对象头的Mark Word到锁记录中,然后开始进行轻量级锁的竞争。轻量级锁的竞争方式为:当前线程通过CAS将对象头Mark Word中的锁记录指针替换为指向当前线程中栈帧的锁记录,如果替换成功说明当前线程获取了轻量级锁,替换失败的话当前线程进行一次自旋然后再次尝试,如果再失败则再自旋后尝试。当自旋达到一定次数仍然竞争锁失败时,轻量级锁升级为重量级锁。
轻量级锁升级重量级锁时,只需要将Mark Word中的标志位由00改为10即可,表示进入重量级锁模式。当前线程升级重量级锁后,直接挂起线程。

3、如果标志位是10,说明对象此时处于重量级锁状态。
此时直接挂起线程,待原持有重量级锁的线程释放锁之后,再唤醒线程。

总结:
偏向锁适用于同一时刻只有一个线程访问同步代码块的情况,线程只需要坚持线程的偏向模式是不是已偏向或偏向线程是不是自己即可,如果偏向线程不是自己,则通过CAS更新对象的偏向线程ID,更新失败则升级轻量级锁。即一旦出现锁竞争,则升级轻量级锁。
轻量级锁适用于锁竞争比较少而且竞争时间很短的情况,线程通过CAS替换Mark Word中的锁记录指针,失败后进行一次忙循环(自旋)然后再次尝试,几次自旋并失败后升级重量级锁,自旋锁是轻量级锁和重量级锁之间的缓冲。
重量级锁之间已线程挂起和唤醒的方式来实现线程同步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值