欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

梳理下jvm的垃圾回收,暂不涉及性能调优等高逼格的东西

时间:2023-06-21

先说脉络,再说细节。先说下jvm如何工作的,后面再谈下jvm的垃圾回收。

java virtual machine 简称jvm,它是虚构出来的一台计算机,包含两个模块:1.字节码指令集(汇编语言),2.内存管理(堆,栈,方法区等)。乍一看这名字啊,java虚拟机好像是属于java语言的虚拟机,其实不是,jvm跟java语言没什么关系,至少是没有直接的关系。jvm上面是运行.class文件的,任何语言包含java语言只要是能编译成对应的.class,那么这个.class文件就能运行在jvm上面,jvm是运行在各物理服务器上面的,所以java是跨平台的。如下图:

说一下java从编码到执行:首先是编译java源代码*.java,经过编译后成*.class文件,然后运行java *.class命令 ,这个时候一个jvm就启动了。jvm启动了,首先要运行的class文件以及java类库里面需要用到的文件 ,注意啊,class文件加载进jvm之后还要经过翻译,翻译成汇编语言之后再交给执行引擎,执行引擎再调用服务器硬件,开始执行逻辑。如下图:

先说语言翻译吧 :翻译的方式有两种:1、编译,2、解释。两种方式只是翻译的时机不同。

编译型语言写的程序执行之前,需要一个专门的编译过程,把程序编译成为机器语言的文件,比如exe文件,以后要运行的话就不用重新翻译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高。

解释则不同,解释性语言的程序不需要编译,省了道工序,解释性语言在运行程序的时候才翻译,比如解释性basic语言,专门有一个解释器能够直接执行basic程序,每个语句都是执行的时候才翻译。这样解释性语言每执行一次就要翻译一次,效率比较低。

上面比较了编译和解释,再回到jvm里里面,道理是一样的:

上面标红加粗的文字,jvm里面还要对class文件进行翻译,注意啊,不是对java语言进行翻译,翻译的工具有两种,分别是:字节码解释器 和 JIT即时编译器。

道理是一样的,字节码解释器开始一句一句的解释执行,注意啊用到的时候才去解释,每用到一句就解释一句,每次都要解释所以效率比较低。那如果有热点代码呢,比如频繁调用某一方法或者循环调用某一段代码呢,那这样是不是总在做重复的翻译啊,那么怎么办呢?

所以就用到了JIT即时编译器,它把热点代码只做一次翻译,保存起来,用到的时候直接调用就行了,不用每次重复的翻译。

下面说jvm的垃圾回收

我们写程序啊,在运行的时候,是需要在内存里面申请空间的,如果在执行的过程中,总会有程序或者叫数据吧,它们完成了自己的使命,没有用了,白占用空间,这个时候就成了垃圾,如果不清理,空间占满了,那后面的程序就没法申请了。

c和c++是需要手动回收垃圾,就是依赖程序编写人员去管理内存的垃圾,程序员自己写代码释放垃圾。c是直接free,c++是先new,然后delete。这样就会带来两个问题:1、重复回收,因为逻辑复杂需要各种循环和判断,很容易多删一次,你刚刚删完,别人里面申请了那块空间,然后你又重复删,这样就误删了别人的数据。2、忘记回收,这就叫内存泄漏。

所以为了解决这个问题,就推出了java和go两种语言。java主要做业务开发,go主要做中间件,运维方面等开发。所以jvm主要解决了java的两个问题,一个是跨平台,一个是垃圾回收。

谁来干着活呢?jvm里面有一个专门的哥儿们叫GC,那么GC是怎么找到垃圾的呢?专业术语叫Root Searching —— 根可达算法。举个简单点例子:main方法就是入口,main方法里面定义的引用就是根,从根对象开始找起,看根对象里面是否还指向其它的对象,如果从根开始搜,某一个对象搜不到,断线了,那说明该对象没有被引用,那这个对象就是垃圾,哪怕两个对象里面互相引用,只要从根儿上断线了,从根上搜不到你两,那这两个对象就是一对垃圾。

GC roots有哪些?有哪些是根儿?mian方法启动后,new的对象(jvm栈里面的),调用的native方法(本地方法栈里面的),运行中创建的常量池里面的constant,方法区里面的静态对象,还有加载进来的clazz对象。

找到垃圾后,怎么清理呢?主要用到3种算法,甭管是哪种垃圾回收器,都逃不出这3种算法,无非就是这3中算法的灵活组合使用。

1、标记清除:先标记出来,然后直接干掉。这种最简单,但会导致内存空间的碎片化。因为运行时申请的空间块是连续的,回收的时候,垃圾块的位置是随机,直接标记清理后,就成了花块状的了。这就导致:明明总空间是够的,但是来了一个大对象在后面放不下,前面的碎片空间没法利用,这要导致空间浪费。如下图:

 2、拷贝或者叫复制:就是每次用一半内存,然后将有用的数据拷贝到另一半的空间里面,那原来那一半的都剩下垃圾了,清理掉,速度非常快,因为一片连续的空间都是垃圾。这样高效率简单,但是空间浪费,典型的以空间换时间。

3、标记压缩:我不仅找到了垃圾,并将它清理掉,然后将有用的对象都挪到前面去。好处就是弥补了标记清除的不足之处,空间腾出来了,后面可放新对象。缺点就是效率较低。

内存调优管理针对特定的垃圾回收器,所以垃圾回收器才是首位的。目前有10种垃圾回收器,其实就是灵活运用上面的3种算法。垃圾回器是配合使用的,下面的连线是指可以两两配合。

常用的分代模型有三种组合,三种组合都是两两配合使用。

两两配合,就是一个负责Young区的回收,另一个负责Old区的回收,这哥儿两必须得互相配合。

ParNew + CMS,Serial + Serial Old,Parallel Scavenge + Parallel Old。这三种配合都是分代模型。目前jdk1.8默认的就是PS + PO这个组合。啥叫分代模型啊,还有为啥要分呢? 如下图所示:

分代模型就是在堆内存中按照新生代和老年代划分空间,这个划分是逻辑划分,不是物理隔开啊。新生代和老年代按照1 : 2的比例分配,新生代里面又分为eden区和两个survivor区(或者叫from和to),比例是8 : 1 : 1。为啥要这样分呢? 这是划分是为了提升回收效率。

新生对象首先进入eden区,eden区会产生大量的对象,所以占比较大,但是里面的多数对象甚至90%以上的对象都会成为垃圾,那eden区的垃圾非常多,回收应该用什么算法呢?既然多就要考虑到速度,所以选用拷贝算法,清理的速度快。如果用标记清除或者标记压缩,那90%的垃圾,你都得先标记,然后清除,所以不划算。

首先开始,新对象都进入eden区,第一次垃圾回收,那怎么回收呢?将eden区一分为二,只用一半吗?不是的,因为eden区存活的对象不到10%,剩下这么一点对象大概占5%—10%,survivor区完全有足够的空间去承接。所以它是将不到10%的有用对象拷贝到survivor区(from),eden区全剩下垃圾,很快就能干掉!紧接着会有新对象产生,进入第二次回收,此时回收就是eden区和from区了,将eden区和from区的有用对象复制到另外一个survivor区(to),然后将eden区和from区都清掉!接着运行,到第三次回收,此时from区已经腾空了,就回收eden区和to区了,将eden区和to区的有用对象复制到from区,将eden区和to区清理掉。以此类推,两个survivor区来回换。但是对象在两个survior区来换的时候,没换一次年龄就加1,当年龄到15之后就进入老年代了,那老年代用什么算法呢,老年代的空间很大,是新生代的2倍,可能是过了很久之后老年代被占满了才回收,这个时候用标记压缩或标记清除。从年轻代到老年代的年龄是多大呢?CMS默认是6,其他的都是默认15,这跟选择哪种垃圾回收器是相关的。这个是在jdk的java对象布局里面设置的,这个年龄阀值是可以设置的,通过-XX:MaxTenuringThreshold来设置,为啥最大是15呢,因为里面给age这个参数分配了4个bit位,最大就是15。

注意,上面说的垃圾回收是指分代模型的内存垃圾回收的方式,里面是用到什么算法,以及为啥要选择该算法,不是针对哪种垃圾回收器。

现在再说一下这几种垃圾回收器吧:

算法是对应到哪种垃圾回收器上,所选用哪种算法是配置不了,取决于GC选用哪种垃圾回收器。

Serial:字面意思是串行化嘛,单线程执行,只用一个GC线程,并且是STW:我要扫垃圾了啊,    你们先别玩了,业务线程都停下。那它用什么算法呢?它不是工作在年轻代吗?当然是用拷贝算法了,单线程拷贝算法。黄箭头就指垃圾回收阶段。

Serial Old:看名知义,Old嘛,就是工作在老年代! 老年代应该啥算法呢?老年代用的是标记清    理或者标记压缩算法,所以它是单线程,用的是标记压缩算法,也是STW。这两种Serial 系列的垃圾回收器组合,现在基本不用了,以前是因为内存太小,几十兆的那种,所以清理起来很快,但现在内存都是几个G了,这个组合太慢了,导致STW的时间太长了,STW期间,用户得不到任何反应,完全卡死了,现在基本不用了这种组合了。卡死了怎么办?重启呗,这也叫jvm调优(别怀疑),最最简单的调优。

Parallel Scanvge:parallel嘛,多个线程并行清理垃圾!注意哦,业务线程依然要停止,是并行清理,不是并发执行业务线程,依然有STW。这个是工作在年轻代,多线程拷贝算法。黄线的箭头并行清理,如下图:

 Paralle Old: 通常与上面的配合使用(jdk1.8默认的这个组合),多线程并行清理,Old,作用在老年代,用的啥算法呢?老年代常用的两种算法:标记清理和标记压缩。Paralle Old是用标记压缩算法,多线程标记压缩,compacting,并行清理,所以STW的时间会比Serial要短。如下图:

但是随着内存的越来越大,PS和PO组合并行清理也解决不了卡顿时间长的问题啊?怎么办呢,这个时候就诞生了:ParNew和CMS。特别是CMS是具有承前启后的作用,CMS是并发回收垃圾,就是垃圾回收线程和工作线程并发执行,学好了CMS才能更好的理解G1,jdk1.9默认G1。

ParNew:这个是作用在年轻代,用复制算法,多线程并行清理,其实就是PS的变种,专门配合CMS的。默认的线程数为CPU的核数。为啥不多搞点线程啊,这样清理的不是更快吗,STW不是更短吗?不是的,如果ParNew线程太多了,CPU切换线程反而浪费时间,因为是配合CMS使用,业务线程是不停止的。

ParNew主要是配合CMS,注意,是配合使用,使STW短,能够尽可能快速的响应给用户。而Parallel Scanvege并行清理主要是增加清理的吞吐量。

CMS:concurrent mark sweep,并发标记清理,啥意思?是并行标记清理吗?不是的,就是业务线程和垃圾回收线程并发执行。从线程的角度,黄箭头清理垃圾,蓝箭头是执行业务。它工作如下图:

仔细想一下啊,这会不会带来很多问题啊?你一边往房间里面搬东西,同时有人在房间里扫垃圾,会不会扫错了呢? 并发标记清理,既然是标记清理算法,那你首先得标记啊,对不对?会不会标记错了呢,因为复杂的业务逻辑,此刻是垃圾,被你标记了,过一会说不定就不是垃圾了,那怎么办呢?是不是?所以说啊,CMS虽然是并发标记清理,但也不是整个过程完全并发,还是有STW的,记住:先标记,在清理。如上图,初始标记的时候,就需要STW(很短),初始标记主要是针对根对象的,什么是根儿,上面已经解释过了。首先是初始标记,然后是并发标记,并发标记没有STW,所以容易出错,错了怎么办?改呗!后面是重新标记,既然是改,那肯定时不能再出错了,所以重新标记的时候要STW(很短很短,毕竟错误只是少数)。标记完了,然后并发清理,并发清理也没有STW。

思考:为啥初始标记需要STW,但是并发标记不需要STW呢?

因为初始标记,只标记根对象,根对象的范围是固定的,上面已经说过,范围很小而且可控,所以即使有STW也是非常非常短。并发标记就不一样了,并发标记是从根对象开始,一层一层的找,太多太多了,随着业务的复杂度不可控,所以业务线程不能停!

重新标记也非常快,因为错误毕竟是少数,所以STW也非常短,而且是纠错阶段,要确保万无一失,业务线程也要停止,是停止的时间非常短。重新标记用的是三色扫描算法(不是标记清理吗?注意,重新标记只是说标记,标记用的是三色扫描算法)。

只要标记没问题,并发清理就没问题,不会清理错了,而且清理的时间较长,所以业务线程也不能停止!

所以,记住并理解CMS的四个阶段:初始标记,并发标记,重新标记,并发清理。

CMS的并发标记清理解决了STW时间长的问题,就万事大吉了吗 ???不是的,不要忘记了,CMS使用的是标记清理算法。既然是标记清理算法,必然会导致老年代内存碎片化!那有人会说,那为啥不用标记压缩算法呢? 这样不就可以避开碎片化的问题吗? 为啥不用?你要搞清楚啊,你都并发清理了,难道你要一边执行业务代码,一边在内存中挪动它们的位置吗??????虽然有参数可以指定,什么时候清理碎片,但是这个问题依然是存在的。所以鱼和熊掌不可兼得。

注意有几个GC的概念容易混淆啊:

这是设置空间大小的,-Xms -Xmx是设置整个堆内存的大小,-Xmn是设置堆里面的年轻代的空间大小。FullGC就是整体进行垃圾回收,YGC就是年轻代垃圾回收,知道是啥意思就行,不扣概念。

今天先说这么多吧,后续再更新,欢迎大佬们指正!

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。