目录
一、GC ROOT
1、虚拟机栈中的本地变量
2、static 成员
3、常量引用
4、本地方法栈中的变量
5、类加载器
6、线程
二、回收算法
1、标记和清除
2、复制算法
3、标记整理
三、垃圾收集器
1、新生代-复制算法
2、老年代-标记清除/整理
3、垃圾收集器分类
1、Serial(单线程)
2、parNew(多线程)
3、Parallel Svavenge(多线程的)
4、Serial old
5、Parallel Old
6、CMS-并发标记扫描 (CMS) 收集器 (重点)
1、cms过程
7、G1 (重点)
1、region(区域)
四、jvm的调优
五、收集器划分
1、停顿时间小
2、吞吐量优先,或者比较关注吞吐量的
3、串行收集器
六、总结
我们通过上一章讲解到什么样的对象是垃圾。其中涉及到两个维度。其中一个就是GC ROOT。就是从上帝视角去往下找,看你这个对象是不是在上帝这个视角之上。如果怎么找也找不到你,那么你就是垃圾。
那么问题来了,什么数据是GC root.就是能成为上帝视角的有哪些?
所以接下来我们要分析哪些对象时GC ROOT.
一、GC ROOT
能成为上帝视角的有哪些?
1、虚拟机栈中的本地变量
能成为 GC Root
那么这个是什么?这个不就是我们在栈针中看到的这个吗?
那么为什么他能成GCROOT呢?或者他为什么能成为指向的依据呢?
我这个局部变量表为什么存在,因为他有栈针存在,因为栈针中有局部变量表的存在。
那么然后呢,栈针又为什么存在?
它对应的是一个方法的执行,那么然后呢?
他既然是对应一个方法的执行的话,说明有线程在调用这个方法。也就意味着线程中(栈针)的局部变量正在使用着。如果把它称之为GC ROOT的候选者的话,我认为是合适的,因为如果把这里局部变量比如op2,为什么能够成为,因为他被正在引用着。这个对象又有可能在引用其他对象,然后被引用的其他对象又有可能在引用其他对象,只要由op2这个对象去触发,能够去找到最终的一个对象,假如你有一个对象preson.这个对象经过由op2触发的对象。这个就能称之为我们的GC ROOT。
为什么他能被称之为?因为由他去触发,然后噼里啪啦去直接或间接找到preson,能找到,他就不能够成为垃圾,为什么不能成为?因为有人自引用他。
而且在这条链路上所有的对象都不能够称之为垃圾。
那么除了这些能成为,还有那些也可以呢?
2、static 成员
注意:static 成员其实他是在我们的方法区中的,一般的他是不会随着对象的生命周期去创建。也就是说他会很长时间内存在我们的方法区中。如果由他引用着,或者是间接引用着,那么其实也是异曲同工的。
3、常量引用
常量引用也是存在我们的方法区中的,所以此时此刻你会发现他们都是一样的。
4、本地方法栈中的变量
注意指的是我们本地方法栈,也就是C语言的调用,我的线程如果去调用c语言的代码了,也是可以根据我们上面class文件那样去参考。
5、类加载器
为什么类加载器能够成为GC ROOT呢?
由他去找到一条线路,如果他找到一条线路,能够到达某个对象,也不能称之为垃圾。为什么?
你去想一想类加载器他在我们java中或者jvm中存在的角色是什么?
哎,它充当的角色是用来classLoader 用来加载类的,他的生命周期相对来说也比较长,你会发现他会一直存在我们java jvm的虚拟机里面。所以他也能成为GCROOT
6、线程
java是进程跑起来,这里面会有很多的线程,不管是垃圾回收线程,还是用户线程等等这样一个个线程,Thread 的这类也能称之为GCROOT,也就是说成为GCroot的会有很多。
至此我们已经知道到底什么是垃圾的。也就是知道了垃圾回收的第一步:判断垃圾
那么我们知道之后要去干什么呢?
那么接下来我们就要去进行回收或者去定义回收的策略。
那么应该怎么去定义这样一个回收的策略呢?
假如我们有一块内存区域。堆里面会去划分old和young区,然后就是对她进行回收。
如果是我们进行设计这样一个回收策略,那么我们应该怎么去设计呢?
那么首先好事之者一定会有一个回收算法。
二、回收算法
这个回收算法是人家帮我们去写好了,我们只需要去理解这个回收算法,而我们的垃圾回收机制就是利用这样一个回收算法进行的一个垃圾的回收。
首先我们可以通过下面一个几种垃圾回收算法图来感受一下。
1、标记和清除
- 标记
找出内存中需要回收的对象,并且把它们标记出来;此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
- 清除
清除掉被标记需要回收的对象,释放出对应的内存空间
存在问题:
- 空间碎片,内存不连续
- 标记和清除都比较耗时,效率还是比较低
2、复制算法
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
s0和s1就是复制算法的最好的体现
弊端:空间浪费
优势:空间连续
3、标记整理
标记过程仍然与”标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
让所有存活的对象都向一端移动,清理掉边界意外的内存。
现在我们搞出三个算法,而我们的堆犹如如下图这样的;那么现在我们到底用哪一个呢?
或者说,我整个堆内存空间,根据刚刚三个算法,他是不是进行不同组合,而且他们是在不同区域中分配组合使用,还是在整个堆中使用。所以此时此刻我们要想的一个问题是:要把这三种算法放到堆中的那一块区域去落地,最好是说我混合去使用。所以要去想这个问题。
但是这个问题又不需要我们去考虑,因为一定会有好事之者一定在这个回收算法的基础上,回去做这样一件事:就是把算法去进行去落地,因为我们没有去设计这个算法,也没有去做了解,所以一定会有好事之者进行落地。那么谁去落地?
我们友好的把它称之为:垃圾收集器
三、垃圾收集器
也就是说jvm把垃圾回收算法去进行落地,所以说我们不需要去考虑这些算法的细节。OK。既然我们不需要去考虑,垃圾收集器帮我们去实现了,所以我们去看看垃圾收集器。
首先我们去官网看,他有不同类型的垃圾收集器。既然他有这么多,所以我们思考下,这些垃圾收集器一定是还需要不同的代(代指分代:老年代和新生代)。
然后我们可以根据下面这个图去发现他们适用于的不同代的分布:
有图我们可以得知:上面是适用于新生代,下面是适用于老年代,而G1是适用于老年代和新生代的。
接下来我么来聊一聊,既然收集器都展示在上面了,我们不放来聊一聊;
我们先不聊垃圾收集器
我们先聊新生代和老年代这个事情。
1、新生代-复制算法
他既然做了新生代和老年代的垃圾收集器的划分,那么新生代会怎么样去选对应垃圾收集器呢?
或者说他到底用的是什么样的垃圾回收算法?这是我们应该思考的;
我们之前young中eden和s0、s1推倒的过程,他是不是就是复制的过程。我们不断的去创建对象,然后移到s0或s1等然后去清除eden.这其实就是复制的过程。
所以新生代采用的应该是复制算法。
那么我们推到是使用的复制。那为什么新生代就一定用的是复制呢?
假如说他不用复制算法,看他和用复制算法的优势是在哪里?
我们根据复制算法,他是把存活的复制到另一块空闲区域,那么他肯定是想移动少量的存活对象,而不是更多的。所以他适用少量的存活对象的场景。那么哪一个区域的对象的生命周期比较短呢?果然是新生代,因为他是朝生夕死的。
2、老年代-标记清除/整理
那么老年代用什么算法呢?
假如我们用复制算法,我们会发现有大量存活的对象会复制来复制去,这样效率太低了。
所以我觉得用标记,不管你是标记清楚,还是标记整理都可以。
为什么?
因为老年代大部分对象都是存活的或者说他的生命周期很长了。我们只要去做一个简单的标记就行了,我们不要去复制,如果你要去复制的话,就浪费效率了。
既然我们额有这样的一个结论,那么我们就有一个大胆的推理:我们上图中的young去的垃圾收集器都是用复制算法,下面old都是用标记清楚/整理的垃圾收集器。
然而事实就是这样的,而G1呢?我们先不管他。
3、垃圾收集器分类
我们一把考量垃圾回收器一般由几个维度
- 他的线程是多的还是单的
- 他采用的是什么算法,是根据什么算法去实现的
- 他适用的范围是什么代
- 最后再说他的优缺点,
大概就是这些维度。
所以接下来我们再去分析两新生代中的垃圾收集器:
1、Serial(单线程)
这个简单的过一下,没必要记,只是简单了解下,因为都不用了,就没必要去记他了,因为我们更多的是关注是:CMS,和G1的收集器,或者是jdk更高的版本。所以这个简单过一下。
所以根据上面考量维度可以总结如下:
这个呢:他是单线程的,他是对复制算法的赢而实现,他是适用于我们新生代的,
他的优点就是,单线程的收集效率会很高。但是缺点就是他会去进行暂停我们用户代码的线程,
所以这也是一个很古老的收集起来。他的图解如下:
他是停掉用户线程,然后去发起一个线程去收集,所以他是单线程的。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
总结:
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:cTient模式下的默认新生代收集器
2、parNew(多线程)
我们根据serial知道他是单线程的,所以我们你能不能用多线程呢?用多线程他的效率会不会高一点呢?所以一定会有一个ParNew收集器
它是对复制算法的收集器,使用范围使我们的新生代的,然后他是多线程收集的。
图解如下:
可以把这个收集器理解为Serial收集器的多线程版本
总结:
优点:在多CPU时,比seria7效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比seria1效率差。
算法:复制算法
适用范围:新生代
应用:运行在server模式下的虚拟机中首选的新生代收集器
不同之处,只不过中间用那个的是多线程。这个其实是伴随的多核多CPU的发展而进行慢慢加入的。因为serial他可不可以用多线程,可以。但是你是单核,用多线程没有效果。所以他就是可以利用多核来跑cpu.此时如果你采用单核cpu,其实他就跟serial差距不大。
3、Parallel Svavenge(多线程的)
同理他采用的也是复制算法,他也是我们的一个并行的。看上去和我们的ParNew是一样 的,但是有一个而不同的点是在于他会更加去关注吞吐量,这是他们的细微的差别。
吞吐量=用户业务代码执行的时间/(业务代码执行的时间+垃圾收集执行的时间)
再往下就是我们的老年代:
4、Serial old
他是serial的老年代版本,他们是对应的也是单线程的,但是不同的是他们采用算法的不同,他是采用"标记-整理"算法,除此之外他和serial收集器是一样。
这是不同代的垃圾收集的演示
5、Parallel Old
他是我们上面新生代Parallel Svavenge收集器的又一老年代的版本。也就意味着他算法是不一样的,他采用的也是“标记-整理”算法,同样也是关注与吞吐量
到此我们听也听了,除此我们还剩CMS和G!.主要是G1
所以重点来了
6、CMS-并发标记扫描 (CMS) 收集器 (重点)
全称是:Concurrent Mark Sweep
顾名思义,他是一个并发类的垃圾收集器,就是:用户线程和垃圾回收线程可以同时进行。
什么意思如图,他展示的是并行:
那么什么是并发?
这个称之为并发。
所以这里可以看出并发和并行之间的区别:
并发:用户和垃圾线程一起跑
并行:垃圾回收线程之间并行跑
所以上图中应用程序线程,就可以理解为用户线程之间并发触发。
所以在此之前的收集器,他们的用户线程和垃圾回收线程就没有一起跑过。
所以我们可以站在这个角度去理解并发和并行。
所以为什么cms是并发垃圾收集器了。
他比较关注的是停顿时间;
之前是吞吐量,现在又接触一个停顿时间。
所以我们在进行垃圾回收调优的时候:更多是调停顿时间和吞吐量。
同理我们去评论一个垃圾收集器的好和坏也是通过停顿时间和吞吐量
那停顿时间,何为停顿时间?
就是我到底停顿了多长时间。待会我们来聊这个内容。
所以我们会发现他是去追求更短的停顿时间。
那么这个停顿时间怎么理解呢
1、cms过程
简单理解是:像我们以前这样的收集器,我们会发现他的停顿都很长;那么为什么停顿时间会很长呢?因为很简单嘛:比如我要去做复制算法,我就会把你都给停掉了,或者说我整个应用程序的代码就很难执行,就是假如说单位时间内长度是一定的话,那么你这里的停顿的时间就会比较长。如图:红框
为什么呢?难道这个不是会更长吗?如图:
如图所示,你不是更加让我延长吗?
因为这个地方有一个优势的地方:就是他可以去进行并发标记:如上图中间区域;他在并发的时候并不是把所有的应用线程都要停掉。如果全部停掉的话,我的cpu就不能得到很好的利用。也就是说你在标记的时候,我们可以适当的执行我们业务代码。我认为你在标记的时候是没必要全部停止的。如果像以前,我们整个红框内都会全部停掉。
那么此时他即执行我们垃圾标记又执行我们应用线程,那么他不是会存在问题吗?此时又有新的对象产生,新的垃圾产生呢?他不就无法再去标记了吗;那么我们该怎么办呢?我们看似停顿时间段了,但是存在上面这个问题。
但是我们从整体去看:他一开始是初始化标记;这个其实很简单,就是把gc root能关联到的给他去标记一下。这个过程其实他会去stop the word.因为第一步为了保险起见,我得全部去进行标记一次,所以这个它是需要一个时间的。但是我们并没有把整个周期去停止。他认为我们初始标记之后,并不是马上切进行清理了。jvm认为我门没必要,去停顿整个周期,这样会很耗时间,怎么办?
jvm认为初始阶段你给我全部停顿初始标记一下,然后就去并发。
为什么第二步去做这样一个并发呢?
目的是为了能够更加确定的找到GCROOT的一个链。因为你初始阶段可能没有去深挖每一个引用(对象)。需要去进一步去保障你。那么此时标记线程和业务代码是并行的。你担心会有新的垃圾产生,新的对象穿件怎么办?所以我们又来了一次重新的标记。这个重新标记我就是用多线程来跑了,因为你有些新增的垃圾我可以去从新标记一下,这样然后我们在去做一个相应的整理。而不是直接采用1,3两步,而是采用1,2,3这样一个波浪的形式。这样一个而过程他的停顿时间就会比原来少了。
少在哪里?少在并发标记的阶段和并行重新标记的阶段以及并发清理的阶段。你会发现他的并发清理是和用户线程一起的。
那么为什么能够允许这样?
这就意味着:用户线程一边执行,一边会有垃圾存在。而这个他是允许你是有垃圾存在的。
他的这样一个停顿时间的缩短允许你标记不全,有垃圾存在,。他会放在下一次去清理。
所以他的这停顿的点是他允许你有误差,
所以说他为什么比较关注我们的停顿时间;从而降低了吞吐量的要求。
从上面我们会发现,他有四个阶段,前三个都是标记阶段,最后一个才是清理阶段。
我们接着往下看,会看到另一个重要的东西:G1
7、G1 (重点)
他是目前主流的,而且是jdk9默认的方式.而且1.8也会很普遍.
什么是G1,我们可以根据官网去看:垃圾优先垃圾收集器 (oracle.com)
官网说G1他是服务端的垃圾收集器,然后你可以去设置他的最小停顿时间的,所以这个也是他跟CMS最大的区别。但是他满足最小的停顿时间,同时还要满足更高的吞吐量。他的阶段分为以下几个阶段:
他和我们的cms很像,他是做了几个阶段;有初始标记、并发标记、最终标记、刷选回收这几个阶段。我们可以先聊一下,这几个阶段分别做了什么?
首先这里的标记大部分都是做的是标记整理的算法。
他首先去做初始的标记,就是判断那些对象是垃圾对象,就是找gc root他的链路,同样他也是是暂停了整个用户线程。这是G1的第一步。
那第二步是并发标记,就是我们Concurrent Mark,他就是和用户线程是同步进行的。这时候他会去找出gc roots,找出我们进一步存活的对象。
第三部他是一个最终的标记,他是来修复我们前面一步的并发标记的,他和cms对比:我们更容易理解,它可以看做为重现标记。其实我们发现他们的前三步都是差不多的。这一步都是去增量标记,标记一些遗漏的或者新产生的对象。
再往下,是我们的筛选回收;在cms中这一阶段,他是做并发清理。而这里为什么我不去做一个清理呢?或者整理呢?因为这里我们用的是标记整理算法嘛。
这个地方是因为他G1的设计有关。什么设计?这个筛选回收他是对整个堆中各个region(区域)去做这一块的回收价值和成本的排序。根据用所期望的停顿时间,去做一个回收的计划。这就是跟我们cms区别的地方。他就是去做了筛选回收。何为筛选回收?
说白了就是:我去做一个选择性的一个回收,但这里我前面都是标记完了,你跟我说,你要去做一个选择性回收,你不是开玩笑吗?注意在我们的cms中你最后一步是做用户线程和垃圾回收一起跑,但是G1在这里却做了另外一件事情,如果你是设置一个 pausetime=0.01s.时间设置为0.01秒。那么我就会尽可能满足你的停顿时间,从而选择性回收。
也就是说,你设计的回收总的停顿时间是10毫秒。你前面停顿时间已经用了6毫秒。那么到筛选回收这挺停顿你只能停顿4毫秒。因为你设置的或者期望的停顿时间为10毫秒,如果是cms的话,此时这一步他去做标并发清除,他傻乎乎的去清除,可能用的时间会更长。所以G1在这里做了设置你停顿的时间。如果你停顿了6毫秒,你只有4毫秒的回收时间。所以你只能去做筛选回收了。所以这叫有选择性的回收。
这个是懂了。但是我们刚刚提到了region这个概念。G1他对原来的内存布局做了调整。原来old、eden、s0、s1他们的布局不适合筛选回收,如果可以的话,cms早就这么做了。就是因为他原来的内存布局不适合我做一个筛选,所以他对原来的布局做了重新的整理,所以这里他采用了一个额概念叫region.
1、region(区域)
他对堆重新进行了布局,理论上或者逻辑上他还是存在eden区、s0、s1的概念,但是真实的物理上,他已经不是隔离的了。而这个region的优势就是能够保证我们那个过程能够正常运行。
那么我们官网优势如何描述的呢?这个region很重要
我们可以先根据官网去了解:根据官网说明他把原来的内存,给复制成一块块的小的区域,这些区域是连续的而且是用来存放对象的。g1他是可以判断每个区域中对象的回收的一个时间,如果回收你的时间比较长,他就会根据你设设置的回收时间去有选择的进行回收。这个就是region的大致用途和理解。
我们由图可知,G1他是适用于新生代和老年代的。
刚才也说的,G1的最大特点就是为了满足一个停顿时间,那么如果停顿小了,他的吞吐量好像也会有一定问题。所以吞吐量金和时间停顿应该使我们重点关注的点。
四、jvm的调优
所以像我们经常讲的jvm的调优;我们调的是什么?我觉得分为两个维度:
GC收集器:也就是说我们的吞吐量和停顿时间,我们要去不断的去调。在满足停顿低的情况尽可能去满足吞吐量
停顿时间:垃圾收集器进行垃圾回收终端(例如:浏览器)执行相应的时间
吐量和:运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间)
吞吐量和停顿时间这两个维度其实他们是有不同的适用场景的;
何为适用场景?
停顿时间:停顿时间少就意味着正在垃圾收集的停顿会比较少,这时候我的停顿时间越少,就意味着我能给终端响应的时间会比较快,响应时间比较快的话,他就能给用户带来很好地体验。能给用户更好的体验,所以这块就适用于和用户交互比较的场景。比如web程序,它跟用户交互多。所以为什么jdk8或者1.9及以后,java作为web开发,作为后端开发程序,为什么会去选择G1,因为G1让你有选择性的去设置这样一个停顿时间。如果你在尽可能范围去设置这个时间,你会发现他是一个很好的体验。
吞吐量:他场景是不适合跟我们用户交互太多,你想要的有高吐出量,就意味着你用户代码执行占用cpu资源的时间会比较长。比如跑任务,在后台运算的场景适合吞吐量有优先的。
注意我们的G1是适用于这两维度的。根据不同场景去选不同的维度。
所以纵观这个几种垃圾收集器可以根据这两维度去分类。
五、收集器划分
1、停顿时间小
CMS、G1
这两个收集器他的停顿时间会比较小,所以我们会发现JDK,1.7,1.8.1.9等等,他做垃圾收集器会用这两个。因为我的停顿时间比较小、而且G1 他可以去进行set停顿时间,这个也是他的精华之处。但是不要太严格。所以停顿时间比较少的,使用与web场景。 -------这一类我们称为并发类的收集器
2、吞吐量优先,或者比较关注吞吐量的
我们会发现这一一块他会有哪些收集器呢? Parallel Scanvent+Parall old -------这一类我们称之为并行类的收集器
再往前探索,就会发现串行收集器
3、串行收集器
何为串行收集器?就是单线程去执行的比如:Serial 和serial old 这些就不进行深入了解了,他不是现在使用的了。它的使用场景就是内存比较少,嵌入式的设备。
这些就是我们堆收集器的一个划分。
六、总结
随着收集器的不同种类发现,java越来越多是做web开发,也就是java越来越多的去做web开发,所以他才关注停顿时间,不断地减少停顿时间,否则用户体验会比较差。但是java最开始的并没有想到,他会去做web开发,最初的设计不是为了做web开发,所以他开始的收集的停顿时间都比较长,或者而他不会太在意停顿时间。也不会在意全部停顿业务线程。所以随着java的使用方向的改变,所以才慢慢出现不同种类的收集器。直至现在,常用于web开发的收集器,甚至1.9默认使用G1.所以随着jdk的不断升级,也来越意识到web开发,越来越意识到挺停顿时间,以至于去不断地去优化收集器。
根据官网介绍的垃圾收集器选择的依据看,并不是最新的就是最好的,这个要看你的适用场景和要求。官网推荐默认使用jvm自己选择的收集器。如果满足不了你,你可以根据堆大小调节,或者收集器的更换去优化。
具体时间选型可看官网推荐的方案。
JVM-java对象内存分布(二)_平凡之路无尽路的博客-CSDN博客