OopMap看不懂,怎么调优哇原创
哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO…
最近正在写三色标记算法,准备实现OopMap了,把JVM的OopMap机制认真梳理了下。为什么要梳理,而不是基于对理论的理解去写?为了尽量跟Hotspot源码保持一致。东西有点多,怕后面忘记,写几篇文章记录下。
国籍惯例,百度了下OopMap相关的文章,嗯,基本都是从周志明的书《深入理解Java虚拟机》摘录出来的,见怪不怪了。因为本人熟读Hotspot源码,所以本人的文章除了会把OopMap理论与GC、运行时串起来,还会加入我的独特理解,及Hotspot源码证明。
大概会讲这些:
1、垃圾收集器各个阶段与STW、安全点、OopMap之间的关系
2、识别数据类型的三种算法
3、GC如何找到JNI线程创建的对象
4、哪些场景会生成OopMap记录
5、生成OopMap记录的几个重要方法
6、OopMap记录如何理解
01
OopMap
一看到Oop,大家是不是条件反射:面向对象?面向对象map?好像说不通啊!
这里的Oop是普通对象指针的意思:ordinary object pointer。这里的map也不是指对应的数据结构是map,而是地图的意思。所以翻译过来就是普通对象指针地图,与现实很贴切。
何谓普通对象指针?就是指一个Java对象,在JVM中,或者Hotspot源码层面,对应一个C++实例。在Java层面,我们叫Java对象,在JVM层面,我们叫oop。
与之对应的就是klass,就是一个Java类在JVM中对应的C++实例。还有handle,这个是一整套机制,后面讲到GC处理JNI线程创建的对象时细讲。
oop-klass-handle机制,是JVM的基石,对于理解Hotspot源码,极其重要。
关于OopMap,很多人对它的基础理解都错了,自然看一些文章,怎么看都看不懂。
02
聊聊GC
语言学习最大的门槛是语言本身,比如C语言的指针。相对来说,高级语言如Java、Python、PHP等,语言本身难度较低,所以学起来比较容易。你觉得生活轻松,是因为有人替你承担了大多数,计算机的世界也一样。
高级语言学起来之所以轻松,是因为高级语言的运行环境,即虚拟机,为你承担了大多数。今天就谈谈其中之一的GC。
GC的目的很简单:自动回收内存。再精确点说,高效回收不被使用的对象占用的内存。为了这一目标,GC经历了串行回收到并行回收到并发回收阶段。
并发阶段好像已经是终点,于是开始在内存设计上做文章,经历了从一个大堆到1:1堆到1:1:8堆到region堆。好像基于region块的堆又是终点了。下一代GC将会是什么样呢?我也不知道,但我保持期待。
GC一般是找到还在使用的对象,打上标记,清理那些没有打标记的对象。那GC怎么找到还在使用的对象呢?分两个阶段:先找到活动对象,再基于活动对象引用链一层一层往下找。
活动对象在哪里:
1、Java线程栈中
2、JNI线程栈中
3、寄存器中
4、可能我没想到的情况
栈与寄存器都是无状态的,言外之意就是说GC时是不知道寄存器中或栈中那一串数字到底是oop还是就是数字。
那怎么办呢?JVM采用的是准确式GC,言外之意就是说能准确的知道哪些区域存储的是oop。怎么做到的呢?就是通过引入OopMap。其实就是一种空间换时间的方式,在GC来临之前,根据栈图及寄存器生成OopMap记录。
生成OopMap记录,必然带来额外的开销,所以不适合持续进行。就像一个商店盘点第二天需要进的货,不是卖出一件盘点一下,而是打烊前完整得盘点。程序世界也一样。那这个何时的时机是什么时候呢?就是GC之前,安全点激活,线程进入安全点的时候。
关于安全点,STW理解得不太好的小伙伴,可以看我另外一篇文章 传送门
下一个问题:GC是怎么基于根对象一层一层往下找的?言外之意是说GC是怎么知道对象哪些属性是引用?这就要说到另一个数据结构:OopMapBlock。这个本篇文章就不展开讲了,后面有机会会写文章单独讲,这里大家知道有这样一个东西的存在,做了这个事情,就可以了。学习,有的时候,需要囫囵吞枣。
总结一下,程序平时运行不会生成OopMap,只有在GC前才会生成OopMap。GC前会激活安全点,所有线程都会进入安全点,即STW。JVM根据平时运行形成的栈图创建OopMap记录,供GC时遍历根对象使用。这句话就是GC、STW、安全点、OopMap的完整关系。解释执行是这样运作的,编译执行还不太一样,后面讲。
这里有个新名词:栈图。栈图底层实现后面有空讲,大家只需要知道它是干这个事的就行:它用来标识局部变量表或操作数栈中每个slot存储的数据类型。
是不是有些小伙伴想到了记忆集及卡表?这里不展开讲,感兴趣的小伙伴可以看我之前写的一篇文章 传送门
03
准确式GC
前面一段好干有木有?来点不干的舒缓下情绪,因为后面还有更干的 ^_^
根据识别数据类型的准确度划分,有这三种算法:
1、保守式GC:即不记录数据的类型,GC时全栈扫描,但是就算是全栈扫描,怎么区分是oop还是数字呢?进行边界比对,比如堆的起始位置是top,结束位置是bottom,在这中间的就是oop。很明显,这个算法实现起来很简单,但是太low了,效率也很低。
2、半保守式GC:根对象不记录类型,根对象派生出来的对象记录数据类型。比如根对象A的地址存储在栈中,没有标记类型,找出所有根对象需要全栈扫描,但是A对象的属性B对象,在类A的元信息中有记录,可以顺藤摸瓜找到,减少程序憨憨遍历时间。
3、准确式GC:不管是根对象,还是派生对象,都标记类型。JVM中引入外部数据结构OopMap+OopMapBlock实现了该算法。
采用准确式GC思路,本质就是说,程序平时运行多花费一点点时间记录数据,帮助GC时减少延时,是值得的。
到这里就把OopMap相关的理论知识讲完了。当然,Hotspot源码很庞大,关于OopMap机制的理解我也有可能犯错。对于文中的有些观点,如果你有不同的理解,欢迎找我交流,一起求真。
04
OopMap格式
经常看到的OopMap格式,大概有这些。我列出来,大家看看有木有自己见过的,下篇文章我展开讲。
你好,我是子牙。十余年技术生涯,一路披荆斩棘从小白到技术总监到大厂中间件到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核及特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。不考虑交个朋友吗?关注硬核子牙: