杂谈JVM(万字手打)
序
学习jvm已有半月,为了防止自己学完就忘记,写此博客.
jvm之运行时内存
jvm的运行时内存,是学习jvm一个不错的切入点,在此一一列出:
1.虚拟机栈: 一千个人眼中有一千个哈姆雷特,一千个线程有一千个虚拟机栈,在操作系统层面看的话,用户级线程便是分着不同的栈去执行的,既然操作系统老大哥都这样,jvm的线程肯定也是一个线程一个栈了.一个虚拟机栈中又有什么呢,看看老大哥的栈中,是一个一个的栈帧,jvm自然也是栈帧了(栈帧即方法).除了栈帧,jvm还有一个小的可以忽略的程序计数器(程序计数器记录每个线程运行的位置,方便线程的切换),操作系统拥有着tcb(ThreadControllerTable),可以记录自己运行到哪儿了,所以不需要程序计数器.因此就没有这个概念了吧.那么栈帧里面又是什么呢,这里面jvm就分的很细致了,操作数栈,局部变量表,动态链接,返回地址.
操作数栈是个啥呢?操作系统中,根据指令,将需要操作的数据放入寄存器组中,之后运算出来,运算中途的数据存入寄存器里面暂存,最后要是出了结果需要保存,那就把它保存到栈帧中(内存).那jvm的操作数栈是什么呢,笔者觉得这只是个抽象的概念,靠jvm具体的底层发挥和编写,或许它是一级缓存又或者它就是寄存器,只要你读取速度够快,可以满足jvm要求,应该就差不多了. 咳咳,后来看了下<<深入理解java虚拟机>>,找到了答案,原来是两种不同的指令集结构,java用的是基于栈的指令集结构,而操作系统使用的是基于寄存器的指令集结构,他们的区别在于,基于栈的指令集结构因为不需要关注寄存器,使其可移植性好,但是速度较慢,因为需要频繁的出入栈操作.而基于寄存器的指令集结构是主流cpu都支持的.书中也提到了jvm将常用的操作映射到了寄存器中,同时也使用了栈顶缓存去加快指令运算速度,看来我之前的想法也并非全是错误的.(<<深入理解java虚拟机>>牛批!!!)
局部变量表是个啥呢?这个没啥好说的了,从入门java开始,局部变量表一直是被我当作栈的存在,什么值传递,地址传递搞得我晕晕乎乎,貌似c语言可以自定义值传递还是地址传递啊(可能不是),但是java就写死了,基本数据类型是值传递,其他的对象就是地址传递,这些值啊,地址啊是方法私有的,那么当然是存在每个栈帧的局部变量表里面的了.对象就把地址放到局部变量表中
动态链接的话,就比较远了,可以扯很久,c也是有动态链接的,都是为了解决某些需要调用的时候才能确定的地址,你总不能直接写地址吧,那以后改了点代码,所有的地址是不是都要改一下呢,再者你也不知道加载到内存以后你的地址在哪儿了.java中的动态链接是将符号引用转化为直接引用的过程,为什么叫动态的,是因为它在运行时这个符号引用的直接引用才确定下来的,那什么时候会发生动态链接呢?在java的重写时,具有多态性,发生重写的过程便是去检查实际类型(A继承B,A a = new B() 这时B为a的实际类型)的该方法,如果有,自然直接去调用,如果没有就要去找他的父类有没有,没有就是父类的父类去找,这时便是会发生动态链接的,因为发生在运行时,找到了符合的方法才把符号引用替换掉为直接引用(重写感觉和自带的类加载器双亲委派机制反着来的)
返回地址字面意思,方法执行完以后,栈帧出栈,那么你要回归上一个栈帧去执行了,这个就是记录你上个栈帧执行到哪儿了
2.堆:java运行时内存里面的大佬,堪称一霸.为什么一霸,因为它一般情况下都是最大的那一块.堆中又有方法区(1.8里面的hotspot为元空间),老年代,新生代这三为巨头
方法区一个很稳定的巨头,他的子民有些从混沌初开(jar包刚运行的时候)之时就基本诞生并且安居在此,有些也会在运行时诞生一些出来(类加载进来的类),已然摆脱六道轮回(YonugGc),但是却难逃那天地重塑(FullGc),就算是fullgc也不是那么容易回收他们,真正的天难灭,地难葬.里面的居民都是些什么角色呢? 运行时常量池(Runtime Constant Pool) 字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法.另外方法区只是一个规范,具体的实现根据jdk的版本和厂商不同是各不相同的.
老年代是一些古董们的聚集地,他们通过后天的努力基本上也是摆脱了六道轮回(YonugGc)之苦,却也是逃不过天地重塑(FullGc)的.另:有些实力天生雄厚的家伙(大对象)可以很容易混入老年代,而他们可能就是天地重塑的祸根之一
新生代烽烟四起之地,所有人饱受这六道轮回(YonugGc)之苦,想要活下去必须上头有人(根可达),新生代分为Eden,From和To三个区域.只有历经轮回(默认经历16次的YoungGc)才能熬到老年代,不必日日担忧
3.本地方法栈:本地方法栈的话,从操作系统上对比的话是这样子的.C语言无法直接使用硬件,所以操作系统提供系统调用的api,让C语言使用,Jvm作为模仿操作系统的小弟,他也没法系统调用啊,所以直接封装了本地方法栈(C语言写的),提供给java去调用,来实现对硬件的控制.
4.直接内存:直接内存,已经脱离了jvm的管控了,一般是unsafe类的或者是nio里面的各种Buffer会使用的,在笔者看来,倒像是直接开始搞c了,自己回收内存,自己管理,大佬们的利器,小白(笔者)慎用!
jvm之对象的一生
当某个new命令被执行时,一个对象就要被创建了,让我们看看他的一生吧.
1.对象结构:java的对象结构分为:对象头,实例数据和填充部分
对象头又可以分为markWord,对象指针和数组长度,对象指针就是指向该对象是哪个类的,数组长度是该对象如果是数组,那么便记录该数组的长度,不是数组对象则无该部分,markWord较为复杂了,其中有锁信息,对象的hash值,对象的分代年龄等
实例数据顾名思义,你的实例的信息
填充部分 java对象的大小是有规则的,是8字节的倍数,为什么有这样的规则呢,是为了更轻松的管理内存(C也有这样的对齐规矩)
2.对象的诞生:一个java对象是怎么出现的呢,new自然是一个很简单的方法,其中反射的newInstance(Class的和Constructor的)也会创建对象,这两种都是使用的类的构造方法.还有使用clone方法和反序列化也是会新建对象,并且不会调用构造方法
一个对象想要降生,那必须有登记在册的类,所以第一件事情就是看看你是哪个类的呢?这个过程就是检查验证了,看看在方法区的常量池,拿着new的对象的字面量去找你这个类有没有被加载,解析,初始化过,如果没有则需要进行类的加载.
倘若类能够被正确加载过来并完成了解析和初始化的话,那么这个类就会调用构造方法在堆中创建一个新的对象啦~~
你不会以为很简单的就去创建了吧,在堆中创建一个对象可不是一件容易的事情,首先呢,你这个堆中的新对象要放在哪里呢,聪明的你会说是在堆的eden区里面.很棒!但是eden区的哪里呢?你会不会觉得我是只杠精,但是在实际的jvm分配内存去创建对象的话,这难道不是一个需要考虑的问题吗.jvm中分配内存有两种方法:第一种是指针碰撞,如果堆内存中没有内存碎片,使用的内存和未使用的内存分别在eden区的两端,这个时候,将中间分隔得指针向后移动对象大小,这种分配就叫指针碰撞.要是有内存碎片呢?那自然出现了第二种分配方法了:空闲列表:维护一个列表(比如位图)去标记哪些内存使用了,然后选出合适的连续的没有被使用的空间给堆去使用,这就是空闲列表了.(CMS垃圾收集器会出现内存碎片,PS和serial是不会产生内存碎片的)
可是,堆是线程共享的一个空间啊,那么要是这一块空间被两个线程同时得到了,这样不就有问题了吗?jvm在分配内存时会有cas操作去保证分配内存时不会出现吧并发问题,当然如果是小对象的话,会有TLAB(ThreadLocalAlloactionBuffer,每个线程会在堆中分配一小段空间属于线程私有去创建对象的,来避免并发问题).之后设置对象的对象头的信息,最后执行对象的初始操作,这样一个对象就这样子诞生了!
3.对象的访问:一个对象终于就此诞生了,对于栈中的单身狗来说,怎么联系这个对象呢?jvm的规范中并没有明确的规定该怎么访问对象.现在主流的对象访问分为两种:Hotspot用的是直接指针, 而除了直接指针以外还有使用句柄池访问的(详见百度).
4.对象的回收:世上谁人能不死?任你风华绝代,艳冠天下,到头来也是红粉骷髅;任你一代天骄,坐拥万里*,到头来也终将化成一抔黄土,对象也总有被回收的那一天.六道轮回(YoungGC),天地动荡(FullGC).
绝境中求生:想要不死,不能单单靠自己,堆中想长久,必须要有一位老祖(gc root)坐镇庇护,才能在一次又一次劫难中艰难求生.那什么样的境界才能变成这样的老祖呢?
老祖们是这些家伙:栈中的单身狗的对象(强引用),静态属性引用的对象,方法区中类静态属性引用的对象,Native中引用的对象.这些对象不仅自身不灭,只要是其一脉(有关联的对象)也是可保周全的.有真的老祖也有伪神,号称能庇护一方,实际是骗子!他们就是栈中的软引用,弱引用和虚引用,软应用的对象不怕youngGC但是fullGC便会杀死他,弱引用和虚引用却连fullGC都抵抗不了,十足的外强中干(软引用和弱引用常用于缓存的框架,虚引用貌似没什么大用).
jvm之天道的发展
道可道,非常道;名可名,非常名。无名,天地之始,有名,万物之母。jvm的堆中的天道便是垃圾回收器
三千堆世界,每个堆世界都有这么一些天道,他们以万物为刍狗,发起灭世之乱,动则六道轮回(YoungGC),甚至天地重塑(FullGC).天道无情,却有迹可循,让我们看看有哪些天道.
初代目:Serial/Serial Old串行收集组合,最古老的天道(垃圾回收器).单线程的收集方式以及有限的空间管理大小,令人发指"Stop the world"的时间,初代天道的力量终究是有点拿不出手哈.开启参数-XX:+UseSerialGC
二代目:parNew/Serial Old初代目天道苦学东瀛影分身之术,终于能够多线程的去回收垃圾了,他其实就是初代目Serial的多线程版本,在使用CMS作为垃圾回收器时会默认使用他收集新生代.因为时多线程收集,所以收集效率在cpu多核下比初代目好.开启参数:-XX:UseParNewGC或者开启CMS时也会默认使用parNew收集新生代,当CMS的空间碎片太多会启动Serial Old回收老年代
三代目:PS组合,ParallelScavenge(处理新生代)与ParallelOld(处理老年代),在jdk1.8中默认使用的垃圾回收器,并行垃圾回收器的巅峰,吞吐量优先的回收机制,GC自适应的调节策略(会根据设置的参数,动态设置新生代的大小来达到设置参数).ParallelScavenge提供了精确控制暂停时间和吞吐量的参数-XX:MaxGCPauseMillis,-XX:GCTimeRatio .不会jvm调优怎么办PS组合自适应的调节策略带你飞.怎么设置ps?都说了默认开启,不配置就行~~
四代目:CMS(Concurrent Mark Sweep),并行的时代终将结束.CMS是天道中划时代的存在,他的诞生开启了垃圾回收器的大并发时代!(并发:指的是不用stop the world 就可以进行回收了),PS组合已经将吞吐量做到了极致,CMS想要出头,必须另辟蹊径,既然吞吐量出不了头,那就去搞并发吧,CMS成功了,但是也失败了,他开启了一个时代,但是他并没有完善很多缺陷,最后不得不求助parNew/Serial Old帮他处理烂摊子.开启参数-XX:+UseConcurMarkSweep
五代目:Garbage-First(G1),继承了CMS的精神,贯彻落实CMS的并发思想,他成功了!开启参数:-XX:UseG1GC
jvm之大并发时代
对于垃圾回收器的并发收集,是需要更加深入理解的.特开一节,在这里我们聊一聊CMS和G1垃圾回收器的实现并发收集的细节以及一些坑与解决方法
标记清扫算法它是用可达性分析算法(可达性分析算法是jvm中垃圾回收器使用的判断对象是否可回收的一个算法)分辨出哪个对象是可回收的,哪个是不可回收的,但是因为标记清扫算法它每次回收完是首先将所有对象的标志位变为0,然后标记好不可回收的是1,可回收的为0.等回收完了以后,会把剩余对象的标志位1变成0方便下次标记清除.因为这个算法它在不同的时期标志位的0和1是有不同意思的,如果是并发的收集模式的话,新产生的对象标记位到底是0还是1呢?如果是0的话,此时如果结束了标记状态开始回收,是必然将新对象回收掉了,但是如果是1的话,万一没有被置为0,下次这个对像本来是要回收的.但是因为初始是1,那必然不会被回收掉.
垃圾回收器想要开启大并发时代,标记清除算法是不能够满足他的需求的,那就必须想想办法的,那么就需要有一个算法去替换原本的标记清除算法吧!
重要补充:可达性分析算法在java中的高效率是基于OopMap的数据结构去保存哪些地方存放着对象引用的,而oopMap的维护是在特定的指令的时侯,哪些指令呢?方法调用、循环跳转、异常跳转这些指令的时候会对oopMap进行维护,而这些指令就是安全点(安全点的选择是是否具有让程序长时间执行的特征,因此最明显特征就是指令序列复用,那么方法调用、循环跳转、异常跳转就是这样的点)
并发算法基础之三色标记法:三色标记算法的出现为并发提供了标记垃圾的可能,三色分别是:黑色,灰色,白色
下文中的直接相关的意思是对象里面有b对象的这个关联,例如:a对象有个属性是B类型的b,那么b是a的直接相关
黑色:本身不可回收,且与他直接相关的对象没有一个白色的对象(和他直接相关的不是黑色就是白色的对象)
灰色:本身不可回收,但是与他直接相关的对象是白色的对象
白色:没有被分析的对象(可能是可回收,也可能是未被分析的不可回收对象)
弯弯绕绕一大堆,这里解释一下吧:所谓三色标记法就是对标记清除算法的一种优化,既然标记清除算法因为状态只有两种不能做并发,那我就搞三种状态.之后一边运行其他业务线程,一边去标记可回收对象呗,三色标记法的过程是这样的.先拿三个能记录的东西(堆,栈,本子,箱子什么的),一个叫专门记录黑色的对象,一个专门记灰色的对象,一个专门记白色的对象.第一阶段把GCRoot们找出来,然后灰色的堆(假如用的堆)里面把GCRoot放进去,第二阶段就是把灰色的堆里面的对象放入黑色的堆里面去,怎么放呢?把灰色的对象一个一个拿出来,与他有直接相关的对象找出来全加到灰色的堆里面去,然后这个灰色对象就能去黑色堆里面去了.如此重复,直到灰色堆空掉,这个时候就标记完成了.
在CMS和G1中都是使用了三色标记法的,CMS和G1在收集时都有共同的点,初始标记和共同标记,笔者认为这就是对应了三色标记法的第一阶段和第二阶段**.初始标记阶段是要"stop the world "的**,不过以为有oopMap的存在这个阶段是很快速的,至于为什么要呢?这个怎么说吧,看看专业的解释吧(摘自<<深入理解java虚拟机>>)
三色标记法是运用在并发的垃圾回收器上的,在并发的条件下,一定有新的对象产生,不可回收的对象变成的可回收的对象,这是必须要解决的问题
CMS中对于新的对象的产生有一个重新标记的过程,这是也是会产生"stop the world" 的,这个过程也是很快的(相对于初始标记是慢的),这个过程就是将新的对象产生给重新标记好,不让他被回收,对不可回收的对象变成的可回收的对象的问题,也可以在此时进行解决
G1的话有一个最终标记,其中的处理看似和CMS的重新标记很相似,但是是有本质不同的,以为算法的原因,G1的最终标记是很快速的
CMS在并发标记和并发清除的时候都是不会stw的,那么,在并发清除阶段,不可回收的对象变成的可回收的对象就会变成浮动垃圾,不会在本次垃圾回收中被回收,而G1只有并发标记阶段不stw,所以没有浮动垃圾的问题
三色标记法本身还有一个问题是需要解决的,那就是对象的消失
线程1扫描的对象已经全部进入黑色的堆中了,所以不会再被扫描了(只有灰色对象会被扫描),此时线程2正在扫描灰色对象B,而灰色对象B有一个直接关联对象C,虽然对象C是白色的不确定对象,但是他实际上是未被扫描的不可回收对象.
如果此时,业务线程中对象B和对象A的引用关系没了,但是对象C也不是这样就变得可回收了,而是被对象A引用了.但是A是黑色的了,不会再被扫描了
对象B扫描完成变为黑色的后,启动回收,对象A因为不是黑色就会被回收,而他应该是不可回收的,这就是对象的消失(漏标)
CMS和G1都解决了对象消失的问题,CMS使用的是增量更新(Incremental Update)算法,G1是通过起始快照算法去解决的,对这两种算法笔者不做深入探讨,本博客中大致说一下思路:
增量更新:当对象C插入到对象A时,将对象A变为灰色,然后在重新标记的时候stw,对A对象再次扫描下,即可
起始快照:在并发标记阶段维护快照去解决的(笔者目前也还在研究中)
结
对于CMS和G1的对比,本博客只是从并发算法的角度浅尝一下,他们是有许多不同的地方的,奈何笔者现阶段学识有限,想要深入,心有余而力不足,可能以后会来完善的,在最后的总结中说一说CMS和G1的一些缺点吧
CMS:cpu敏感,只有多cpu才能发挥的好. 浮动垃圾,没有解决浮动垃圾问题.内存碎片,标记清扫算法会产生空间碎片.
G1:cpu敏感,同上,要求内存要大.
后续笔者需要深入理解的一些概念:G1垃圾回收器的实现细节,内存细节,起始快照算法的具体实现,G1的跨代引用使用的记忆集
历时3天终于,写完了,完结撒花,码字不易,求关注三连
很重要的事:笔者刚开始系统学习jvm,难免有些理解问题,望各位大佬指出,笔者一定仔细查询资料并改正.
本文地址:https://blog.csdn.net/weixin_41046569/article/details/109778911
推荐阅读