杂谈JVM(万字手打)

学习jvm已有半月,为了防止本身学完就忘记,写此博客.java

jvm之运行时内存

jvm的运行时内存,是学习jvm一个不错的切入点,在此一一列出:算法

1.虚拟机栈: 一千我的眼中有一千个哈姆雷特,一千个线程有一千个虚拟机栈,在操做系统层面看的话,用户级线程即是分着不一样的栈去执行的,既然操做系统老大哥都这样,jvm的线程确定也是一个线程一个栈了.一个虚拟机栈中又有什么呢,看看老大哥的栈中,是一个一个的栈帧,jvm天然也是栈帧了(栈帧即方法).除了栈帧,jvm还有一个小的能够忽略的程序计数器(程序计数器记录每一个线程运行的位置,方便线程的切换),操做系统拥有着tcb(ThreadControllerTable),能够记录本身运行到哪儿了,因此不须要程序计数器.所以就没有这个概念了吧.那么栈帧里面又是什么呢,这里面jvm就分的很细致了,操做数栈,局部变量表,动态连接,返回地址.api

操做数栈是个啥呢?操做系统中,根据指令,将须要操做的数据放入寄存器组中,以后运算出来,运算中途的数据存入寄存器里面暂存,最后要是出告终果须要保存,那就把它保存到栈帧中(内存).那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虚拟机>>)
摘自深刻理解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,不免有些理解问题,望各位大佬指出,笔者必定仔细查询资料并改正.

相关文章
相关标签/搜索