我们提供【社群管理裂变】【自动建群】【多群转播】【活码系统】【小程序开发】【公众号开发】【各类商城 SAAS】一站式服务,各类功能提供免费体验,满意付款,如您还有其他疑问请您添加企鹅/微信 1003312430 方便咨询哦。
非凡社群助手------微信活码系统演示(客户活码二维码累计扫描量一千万+)
活码系统介绍:
二维码图案不变,内容可随时变更, 极大提高营销效果,基于活码技术,二维码图案更简单,扫码更加容易宣传海报、二维码印刷、商品.群.软文。
让二维码图案更简单,扫码更加容易宣传。,
点击箭头处“蓝色字”,关注我们哦!!
前言
上一篇文章对 JVM 的运行时数据区域的内容举行了梳理,本篇文章对 JVM 中的工具和工具的内存结构举行深入剖析。本文参考了《深入明白 Java 虚拟机》、《深入剖析 Java 虚拟机 HotSpot》、《HotSpot 实战》三本书。
下面提到的虚拟机都特指 JDK1.8 版本的 HotSpot VM,其他虚拟机的实现有可能不太一样。
类与工具
在编译时,通过 Javac 编译器为虚拟机规范的 class 文件花样。class 文件花样是与操作系统和机械指令集无关的、平台中立的花样。其他语言编写的代码只需要实现指定语言的编译器编译位 JVM 规范尺度的 class 文件就可以实现该语言运行在 JVM 之上,这就是 JVM 的语言无关性。
通过 java 下令运行 class 文件,首先会通过类加载器将 class 文件加载到内存中,加载 class 文件会为类天生一个 klass 实例。在 klass 包罗了用于形貌 Java 类的元数据,包罗字段个数、巨细、是否为数组、是否有父类、方式信息等。
工具类二分模子
HotSpot 虚拟机是使用 C++实现的, C++也是面向工具语言。可以接纳 java 类逐一映射到 C++类,当确立 Java 工具就确立对应的 C++类的工具。
然则由于若是 C++的工具含有虚函数,则确立的工具会有虚方式表指针,指向虚方式表。若是接纳这种直接一对一映射的方式,会导致含有虚方式的类确立的工具都包罗虚方式指针。因此在 HotSpot 虚拟机中,通过工具类二分模子,将类形貌信息和实例数据举行拆分。使用仅包罗数据不包罗方式的 oop(Ordinary Object Pointer)工具形貌 Java 的工具,使用 klass 形貌 java 的类。oop 的职责在于示意工具实例数据,没需要维护虚函数指针。通过 oop 工具头部的一个指针指向对应的 klass 工具举行关联。
在 HotSpot 虚拟机中,通俗工具的类通过 instanceKlass 示意,工具实例则通过 instanceOopDesc 示意。
在 JVM 中引用类型可以分为工具,基本类型数组和工具类型数组。可以划分映射到 Java 中的对应的工具和类型。
类 工具 工具 instanceKlass instanceOopDesc 基本类型数组 typeArrayKlass typeArrayOopDesc 工具类型数组 objArrayKlass objArrayOopDesc
除了常用的 3 类引用工具外,另有一些其他 JVM 自己要用的java.lang.ClassLoader
用InstanceClassLoaderKlass
形貌,java.lang.Class
用InstanceMirrorKlass
形貌等。
工具
HotSpot VM 使用 oop 形貌工具,oop 字面意思是“通俗工具指针”。它是指向一片内存的指针,只是将这片内存‘视作’(强制类型转换)Java 工具/数组。工具的本质就是用工具头和字段数据填充这片内存。
工具内存结构
JOL 工具
在谈论详细工具结构时,推荐一个 JOL 工具,可以打印工具的内存结构。通过 maven 引入。
通过ClassLayout.parseInstance(new Object()).toPrintable()
即可打印工具的内存结构。
工具头
通俗工具的工具头包罗 2 部门,第一部门被称为Mark Word
,第二部门为类型指针。若是工具为数组,除了通俗工具的两部格外工具头还包罗数组长度。下图是 64 位虚拟机工具头。
32 位虚拟机头部的 Mark Word 长度为 4 个字节。
Mark Word
Mark Word
保留了工具运行时需要的信息,包罗哈希码(HashCode)、GC 分代岁数、偏向状态、锁状态标志、偏向线程 ID、偏向时间戳等信息。通过类型指针,可以找到工具对应的类型信息。32 位虚拟机和 64 位虚拟机的Mark Word
长度划分为 4 字节和 8 字节。
岂论是 32 位照样 64 位虚拟机的工具头部都使用了 4 比特纪录分代岁数,每次 GC 时工具幸存岁数都市加 1,因此工具在 survivor 区最多幸存 15 次,跨越 15 次时,仍然有可达根的工具就会从 survivor 区被转移到暮年月。可以通过-XX:MaxTenuringThreshold=15
参数修改最大幸存岁数。
CMS 垃圾接纳器默以为 6 次。
类型句柄
相比 32 位工具头巨细,64 位工具头更大一些,64 位虚拟机工具头的Mark Word
和类型指针地址
都是 8 字节。而通常情形,我们的程序不需要占用那么大的内存。因此虚拟机通过压缩指针功效,将工具头的类型指针举行压缩。而Mark Word
由于运行时需要保留的头部信息会大于 4 字节,仍然使用 8 字节。若设置开启了-XX:+UseCompressedOops
,虚拟时机将类型指针地址
压缩为 32 位。若设置开启了-XX:+UseCompressedClassPointers
,则会压缩 klass 工具的地址为 32 位。
需要注重的是,当地址经由压缩后,寻址局限不能阻止的会降低。对于 64 位 CPU,由于现在内存一样平常到不了 2^64,因此大多数 64 位 CPU 的地址总线现实会小于 64 位,好比 48 位。
开启-XX:+UseCompressedOops
,默认也会开启-XX:+UseCompressedClassPointers
。关闭-XX:+UseCompressedOops
,默认也会关闭-XX:+UseCompressedClassPointers
。
若是开启-XX:+UseCompressedOops
,然则关闭-XX:+UseCompressedClassPointers
,启动虚拟机的时刻会提醒“Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops”。
通俗工具内存结构(64 位虚拟机指针压缩时)
需要注重,由于内存按小端模式漫衍,因此显示的内容是反着的。上面现实工具头内容为 00000000 00000001 f80001e5
数组工具内存结构(64 位虚拟机指针压缩时)
工具头与锁膨胀
工具头中存储了锁的需要信息,差其余锁的工具头存储内容稍有差异。32 位工具头存储花样如下
JVM 底层对加锁举行了性能优化,默认虚拟机启动后约莫 4 秒会开启偏向锁功效。当虚拟机未启用偏向锁时,锁的演化历程为无锁->轻量锁(自旋锁)->重量锁
。
当虚拟机启用了偏向锁时,锁的演化历程为无锁->偏向锁->轻量锁(自旋锁)->重量锁
。
本文不讨论 JVM 对加锁的详细优化逻辑,内容对照多,感兴趣的可以看同砚可以参考《浅谈偏向锁、轻量级锁、重量级锁》。
无锁
当工具未加锁时,锁状态为01
,32 位虚拟机的工具头部如图所示
需要注重的是其中工具头保留的hashCode
被称为identityHashCode
,当我们挪用工具的hashCode
方式,返回的就是该值。若我们重写了hashCode
的值,工具头的hashCode
值仍然是内部的identityHashCode
,而不是我们重写的hashCode
值。可以通过System.identityHashCode
打印identityHashCode
,或者也可以通过toString
直接打印工具输出 16 进制的identityHashCode
。
偏向锁
偏向锁中的“偏”,就是偏心的“偏”、左袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,若是在接下来的执行历程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再举行同步。
偏向锁的锁状态和未锁状态一样都是01
,当工具处于偏向状态时,偏向符号为1
;当工具处于未偏向时,偏向符号为0
。
32 位虚拟机的偏向锁工具头部如图所示
偏向时间戳,它现实示意偏向的有用期。
无锁状态升级为偏向锁的条件:
-
工具可偏向,工具未加锁时,执行 CAS 更新工具头部线程偏向线程 ID 为当前线程乐成。
-
工具可偏向,工具已加锁,但偏向线程 ID 为空,执行 CAS 更新工具头部线程偏向线程 ID 为当前线程乐成。
-
工具可偏向,工具已加锁,且偏向线程 ID 即是当前线程 ID。
-
工具可偏向,工具已加锁,且偏向线程 ID 不为空且不即是当前线程 ID,执行 CAS 更新工具头部线程偏向线程 ID 为当前线程乐成。
虚拟机启动时,会凭证-XX:BiasedLockingStartupDelay
设置延迟启动偏向,在 JDK1.8 中,默以为 4 秒。有需要时可以通过-XX:BiasedLockingStartupDelay=0
关闭延时偏向。
轻量级锁
轻量级锁的锁状态为00
,32 位虚拟机的轻量级锁头部花样如下
升级为轻量级锁条件:
-
工具不能偏向,跳过偏向锁直接使用轻量级锁。
-
工具可偏向,但偏向加锁失败(存在线程竞争)。
-
工具获取挪用
hashCode
后加锁。 -
工具已升级为重量级锁后,锁降级只能降级为轻量级锁,无法降级为偏向锁。
轻量级锁会在线程的栈帧中开拓一个锁纪录区域,将当前工具的头部保留在锁纪录区域中,将锁纪录区域的地址保留到当前工具头部。
-
工具不能偏向直接升级到轻量锁
-
偏向锁竞争升级为轻量锁
-
偏向后挪用
hashCode
方式升级为轻量级锁
重量级锁
轻量级锁的锁状态为10
,32 位重量级锁头部如图所示
轻量级锁自循环一定次数后一致获取不到锁,则升级为重量级锁条件。自旋次数默以为 10 次,可以通过-XX:PreBlockSpin
设置修改次数。
重量级锁降级
当重量级锁解锁后就会举行锁降级,锁降级只能降级为轻量锁,无法再使用偏向锁。
实例数据
工具实例数据默认根据long、double、int、short、char、byte、boolean、reference
顺序结构,相同字段宽度总是分配在一起。若有父工具,则父工具的实例字段在子工具前面。
另外若是 HotSpot 虚拟机的 +XX:CompactFields
参数值为 true(默认就为 true),那子类之中较窄的变量也允许插入父类变量的清闲之中,以节约出一点点空间。
填充
JVM 中,工具巨细默以为 8 的整数倍,若工具巨细不能被 8 整除,则会填充空字节来填充工具保证。
工具生命周期
在领会完工具头部后,我们看下工具的确立的时刻发生了什么事情。当我们挪用new Object()
确立一个工具时,天生的字节码如下
首先通过new
指令分配工具,并将工具地址入栈,通过dup
指令复制一份栈顶元素。通过invokespecial
指令挪用工具的init
举行初始化会消耗栈顶 2 个槽。由于init
方式需要传入一个参数,该参数即为引用工具自己。在 init 初始化时会将this
指针举行赋值。这样我们在代码中就可以通过this
指向当前工具。
工具确立流程如下图所示。
-
栈上分配
通常工具都是在堆上确立的,若工具仅在当前作用域下使用,那么使用完很快就会被 GC 接纳。JVM 通过逃逸剖析对工具作用域举行剖析,若是工具仅在当前作用域下使用,则将工具的实例数据分配在栈上,从而提升工具确立速率的同时削减 GC 接纳的工具数目。 -
线程局部缓冲区(TLAB)
若是无法在栈上分配,则工具会在堆上分配。对于 JDK1.8 来说,Java 堆通常使用分代模子,(关于 GC,垃圾接纳算法等这里不做详细讨论)。经由统计,90%的工具在使用完成后都市被接纳,因此默认新生代会分配 10%的空间给幸存者区。
工具先在 eden 区举行分配,然则我们知道,堆是所有线程共享的区域,会存在多线程并发问题。因此在堆上分配就需要举行线程同步。为了提高分配效率,JVM 会为每个线程从 eden 区初始化一块堆内存,该内存是线程私有的。这样每次分配工具时就无需举行同步操作,从而提高工具分配效率。线程的这块局部内存区域被称为线程局部缓冲区(TLAB)。通常这块内存会小于 eden 区的 1%。当这块内存用完时,就会重新通过 CAS 的方式为线程重新分配一块 TLAB。
通常工具分配有两种方式,一种是线性分配,当内存是规整时(大部门垃圾接纳器新生代都是用符号整理算法,可以保证内存规整),通过一个指针向后移动工具巨细,直接分配一块内存给工具,指针左边是已使用的内存,指针右边是未使用的内存,这种方式被称为指针碰撞。TLAB 配合指针碰撞手艺能够在线程平安的情形下移动一次指针直接就可以完成工具的内存分配。
当内存不规整时(好比 CMS 垃圾接纳器通常情形并不会每次 GC 后都压缩内存,会存在内存碎片),则需要一块分外的内存纪录哪些内存是空闲的,这个缓存被称为空闲列表。 -
eden 区分配
若是 TLAB 无法分配工具,那么工具只能在 Eden 区直接分配,前面说过,在堆上分配,必须接纳同步战略阻止有发生线程平安问题。若是分配内存时,工具的 klass 没有剖析过,则需要先举行类加载历程,然后才气分配工具。这个历程被称为慢速分配,而若是 klass 已剖析过则直接可以分配工具,这个历程被称为快速分配。 -
暮年月分配
当 eden 区放不下工具时(固然另有其他的判断战略,这里暂时不去体贴),工具直接分配到暮年月。 -
工具实例初始化
当工具完成内存分配时,就会初始化工具,将内存清零。需要注重,工具的静态变量在类初始化的初始化阶段已经完成设置。 -
初始化工具头部
当工具实例初始化完,就会设置工具头部,默认的工具头部存放在 klass,若是启用了偏向,则设置的就是可偏向的工具头。
工具接见方式
现在我们领会了工具的内存结构和工具的确立逻辑,那么工具在运行时,若何通过栈的局部变量找到现实的工具呢?常用的工具接见方式有 2 种,直接指针接见和句柄接见。
直接指针接见
工具确立时,局部变量表只保留工具的地址,地址指向的是堆中的现实工具的 markword 地址,JVM 中接纳的就是这种方式接见工具。
句柄接见
通过句柄接见时势部变量保留的时句柄池的工具句柄,句柄池中,则会存储工具实例指针和工具类型指针。再通过这两个指针划分指向工具实例池中的工具和元数据的 klass。
相比直接指针接见,这种接见方式由于需要 2 次接见,而直接指针只需要一次接见,因此句柄接见工具的速率相对较慢。然则对于垃圾接纳器来说是对照友好的,由于工具移动无需更新栈中的局部变量表的内容,只需要更新句柄池中的工具实例指针的值。
HSDB
前面我们通过 JOL 工具可以很利便的输出工具的结构。JDK 也提供了一些工具可以查看更详细的运行时数据。
HSDB(Hotspot Debugger) 是 JDK1.8 自带的工具,使用该工具可以毗邻到运行时的 java 历程,查看到 JVM 运行时的状态。
以该偏向锁代码为例
为了能看到运行时状态,我们可以使用 idea 工具单笔调试,也可以使用 jdb 工具举行调试。jdb 是 Java 的调试器,位于%JAVA_HOME%/bin
下面。通过jdb -classpath XXX class 名
执行 main 方式。
执行后,我们可以将打断点,然后举行调试。
-
通过
stop in <class id>.<method>[(argument_type,...)]
在方式中打断点,或者可以通过stop at <class id>:<line>
在指定行打断点。 -
通过
stop in com.company.BiasedLock.main
将断点打在 main 方式。 -
通过
run
运行 -
通过 next 举行调试。(可以使用 step 举行单步骤试)
此时我们可以通过 HSDB 毗邻到历程。通过JPS
下令查看历程的 pid
通过java -cp “.;%JAVA_HOME%/lib/sa-jdi.jar” sun.jvm.hotspot.HSDB 3828
启动 HSDB(这种方式会壅闭我们的程序,不要直接在生产环境这样操作)
第一次启动可能会报错误无法找到sawindbg.dll
,这时需要将%JAVA_HOME%/lib
目录下面的sawindbg.dll
文件拷贝到 jre 的/lib
目录下即可。
启动后,在界面选中 main 线程,点击工具栏第二个图片打开线程栈。
HSDB 工具在线程栈中已经标出我们的工具。在菜单找到内存查看器
输入栈局部变量表中的工具的地址,就可以显示出工具的内存,和 JOL 工具打印的工具头部是一样的。
参考文档
-
HSDB - HotSpot debugger
-
JOL:剖析 Java 工具的内存结构
-
[Java JVM] Hotspot GC 研究- 开篇&工具内存结构
-
看了这篇文章,我搞懂了 StringTable
-
盘一盘 synchronized (一)—— 从打印 Java 工具头提及
-
浅谈偏向锁、轻量级锁、重量级锁
-
源码剖析-线程 A 请求偏向于线程 B 的偏向锁
-
C++为什么要弄出虚表这个器械?
-
《深入明白 Java 虚拟机》
-
《Java 虚拟机规范(Java SE 8 版)》
长按二维码
关注我们

来都来了,点个在看再走吧~~~

手机摄影如何拍出高大上的感觉? - 木西 AlexanDENG 的回答 - 知乎 : 2016.6.6. 更新:今天被推到发现啦,感谢知乎的编辑团队。也欢迎大家来值乎向我提问 https://www.zhihu.com/zhi/people/723514935031652352 (二维码自动识别)距离这个问题首次回答已经 4 个多月了。这四个月答主我又积累了一些手机摄影的片子,在这里分享给大家。请不要怀疑,真的全部使用手机…
,手机摄影如何拍出高大上的感觉? - 木西 AlexanDENG 的回答 - 知乎 : 2016.6.6. 更新:今天被推到发现啦,感谢知乎的编辑团队。也欢迎大家来值乎向我提问 https://www.zhihu.com/zhi/people/723514935031652352 (二维码自动识别)距离这个问题首次回答已经 4 个多月了。这四个月答主我又积累了一些手机摄影的片子,在这里分享给大家。请不要怀疑,真的全部使用手机…,手机摄影如何拍出高大上的感觉? - 木西 AlexanDENG 的回答 - 知乎 : 2016.6.6
还没有人赞赏,快来当第一个赞赏的人吧!
- 2¥
- 5¥
- 10¥
- 20¥
- 50¥
声明:本文来自非凡建群宝投稿,不代表微信机器人立场,版权归原作者所有,欢迎分享本文,转载请保留出处!