资讯专栏INFORMATION COLUMN

JVM(2)--一文读懂垃圾回收

MRZYD / 2208人阅读

摘要:对象存不进去,会又一次触发垃圾回收。也就是说,它在进行垃圾回收时,必须暂停其他所有线程。我们来看一个名词吞吐量。吞吐量运行用户代码时间运行用户代码时间垃圾收集时间。也就是说,收集器会严格控制吞吐量,至于这个吞吐量是多少,这个可以人为设置。

与其他语言相比,例如c/c++,我们都知道,java虚拟机对于程序中产生的垃圾,虚拟机是会自动帮我们进行清除管理的,而像c/c++这些语言平台则需要程序员自己手动对内存进行释放。

虽然这种自动帮我们回收垃圾的策略少了一定的灵活性,但却让代码编写者省去了很多工作,同时也提高了很多安全性。(因为像C/C++假如你创建了大量的对象,但却由于自己的疏忽忘了将他们进行释放,可能会造成内存溢出)。

何为垃圾?

刚才说了,虚拟机会自动帮助我们进行垃圾的清除,那什么样的对象我们才可以称为是垃圾对象呢?

假如你创建了一个对象

Man m = new Man();

你用一个变量指向了这个对象,显然对于这个对象,你可以用变量m对这个对象进行利用,但过了一段时间,你执行了

m = null;

并且也并没有新的变量来指向刚才创建的对象。此时对于这个没有任何变量指向的对象,你觉得它还有用处吗?

显然,对于这种没有被变量指向的对象,它是一点卵用也没有的,它只能在随风漂流。

因此,对于这样的对象,我们就可以把它称为垃圾了,它早晚会被垃圾回收器给干掉。

怎么知道它已经是垃圾对象了?

假如代码是你自己编写的,你可能知道这个对象啥时候应该被抛弃,你可以随时让它成为垃圾对象。

但是,你毕竟是你,虚拟机则没那么智能。那虚拟机是如何知道的呢?

上面已经说了,没有变量引用这个对象时,它就是垃圾对象了,基于这个原理,我们可以这样做啊:

我们可以为这个对象设置一个计数器,初始值为0,假如有一个变量指向它,那么计数器就加1,如果这个变量不在指向它了,计数器就减1。那么我们就可以判断,如果这个计数器为0的话,那它就是垃圾对象了,否则就是有用的对象。

对于这种方法,我们称之为引用计数法

好吧,我们先来夸一夸引用计数法这种方法:

实现简单。

效率高(一个if语句就能解决的问题想不高效都难)。

不好意思,接下来得说说它那个致命的缺点

实际上,对于这种引用计数的方法,假如它遇到对象互相引用的话,是很难解决的。

先看一段代码:

Man m1 = new Man();
Man m2 = new Man();
//互相引用
m1.instance = m2;//假设Man有instance这个属性
m2.instance = m1;

m1 = null;
m2 = null;
System.gc();//按道理对象应该被回收

这段代码m1和m2都指向null了,按道理两个对象已经是无用对象,应该被回收,但是,两个对象之间彼此有一个instance的属性互相牵引的对方,导致两个对象并没有被回收。

这个缺点够致命吧?

所以,虚拟机并没有采用这种引用计数的方法。

可达性分析

除了这种方法,我们还有其他的方法吗?

答案是有的,必须得有啊。这种方法就是传说中的可达性分析,(我靠,听名字是真的高级啊)。它的工作原理是这样的:

在程序开始时,会建立一个引用根节点(GC Roots),并构建一个引用图。当需要判断谁是垃圾时,我们可以从这个根节点进行遍历,如果没有被遍历到的节点则是垃圾对象,否则就是有用对象。如下图:

这个方法可以解决循环相互引用的问题,但是这个方法并没有引用计数法高效,毕竟要遍历图啊。

总结下判断是否为垃圾对象的算法:

引用计数法。

可达性分析。

何时进行垃圾回收

可能有人会觉得这个问题很奇怪,觉得看到垃圾就回收不是很好。对于这个我只能说:

看到房间有一点垃圾你会马上扫?还是等到某个时间点或者当垃圾积累到一定的数量再扫?

虚拟机可没那么智能可以马上识别这个对象是垃圾对象,它还得遍历所有对象才能知道有哪些是垃圾对象。

所以说,你总不能几秒(我们假设几秒是贼短的时间)就让虚拟机遍历一下所有对象吧?

这里先说明一下,当垃圾回收器在进行垃圾回收的时候,为了保证垃圾回收不受干扰,是会暂停所有线程的,此时程序无法对外部的请求进行响应。(因为你想啊,当你在可达性分析的时候,那些引用关系还在不断着变化,那不很难受)。

而且频繁的垃圾回收,对于有一些程序,是很影响用户体验的,例如你在玩游戏,系统动不动就停顿一下,怕你是要把这游戏给删了。

所以说,垃圾回收是会等到内存被使用了一定的比例的时候,才会触发垃圾回收。至于这个比例是多少,这可能就是人为规定的了。

怎么回收?

当我们标记好了哪些是垃圾,想要进行回收的时候,该怎么回收比较好呢?

可能有一些人就觉得奇怪,这还不简单,看见它是垃圾,直接回收不就得了。

其实这也不无道理,简单粗暴,直接回收。

是的,确实有这样的算法,看哪些是被我们标记的垃圾,看见了就直接回收。这种算法我们称之为标记--清除算法

标记-清除算法工作原理:就是先标记出所有需要回收的对象,然后在统一回收所有被标记过的对象。

不过,那些人你可别得意啊,因为这种方法虽然简单暴力,但它有个致命的缺点就是:

标记清除过后,会产生大量的不连续内存碎片,如果不连续的碎片过多的话,,可能会导致有一些大的对象存不进去。这样,会导致下面两个问题:

有些内存浪费了。

对象存不进去,会又一次触发垃圾回收。

复制算法

为了解决这种问题,另外一种算法出现了---复制算法。就是说,它会将可用的内存按容量划分成两块。然后每次只使用其中的一块,当这一块快用完的时候,就会触发垃圾回收,它会把还存活的对象全部复制到另外一块内存中去,然后把这块内存全部清理了。

这样,就不会出现碎片问题了。

居然帮我们解决了我们必须夸一下它:不仅帮我们解决了问题,而且实现上也简单、运行也高效。

但是(凡事都有个但是的),它也是有缺点的,缺点很明显,发现了没有。假如每次存活的对象都很少很少,那另外一块内存不是几乎没有用到?所以说,这种方法有可能导致另外一半内存几乎没用了。内存那么宝贵,这可是很严重的问题。

优化策略:可以告诉你,有研究显示,其实有98%的对象都是朝生夕死的,也就是说,每次存活的对象确实很少很少。既然我们都知道存活的对象很少很少了,那我们干嘛还1:1的比例来分配?所以说,HotShot虚拟机是默认按8:1的比例来分配的。这样,就不会出现很多内存没用到的问题了。

可能有人会说,万一占比为1/9的内存不够用了怎么办?不就没地方存那些活的对象?实际上,当内存不够用时,可以向其他地方借些内存来使用,例如老年代里的内存。

这里说明一下新生代和老年代:说白了,新生代就是刚刚创建不久的对象,而老年代是已经活了挺久的对象。也就是说,有一些对象是确实活的比较久的,对于这种对象,我们另外给它分配内存来养老,而且垃圾回收时,我们不用每次都来这里查找有没垃圾对象,因为这些对象是垃圾的几率会比较小。

下面在简单介绍另外两种算法:

标记-整理算法:这种算法和标记-清除算法类似,不过它把垃圾清除了之后,会让存活的对象往一个方向靠拢,以此来整理碎片。

分代收集算法:所谓分代就是把对象分成类似上面说的老年代和新生代,在新手代一般每次垃圾回收时死的对象一般都会比较多,而老年代会比较少,基于这种关系,我们就可以采取不同的算法来针对了。

总结下垃圾回收的几种算法:

标记-清除算法。

复制算法。

标记-整理算法。

分代收集算法。

最后给大家几种垃圾回收器

对于垃圾的回收,你是想一边运行程序其他代码一边进行垃圾回收?还是想把垃圾全收好再来执行程序的其他代码?虽然说最终使用cpu的时间是一样,但两种方式还是有区别的。

下面简单介绍几种垃圾回收器,看看他们都使用哪种方。

(1).Serial收集器

serial(串行),看这个英文单词就知道这是一个单线程收集器。也就是说,它在进行垃圾回收时,必须暂停其他所有线程。显然,有时垃圾回收停顿的比较久的话,这对于用户来说是很难受的。

(2).ParNew

这个收集器和Serial很类似,进行垃圾回收的时候,也是得暂停其他所有线程,不过,它可以多条线程工作进行垃圾回收。

(3).Parallel Scavenge收集器

parallel,并行的意思。也是可以多线程进行垃圾回收处理,但是它与ParNew不同。它会严格控制垃圾回收的时间与执行其他代码的时间之间的比例。我们来看一个名词:吞吐量

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

也就是说,Parallet Scavenge收集器会严格控制吞吐量,至于这个吞吐量是多少,这个可以人为设置。

下面两个收集器重点介绍下

(4).CMS(Concurrent Mark Sweep)收集器

CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)

并发标记(CMS concurrent mark)

重新标记(CMS remark)

并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要暂停其他线程。但另外两个步骤可以和其他线程并发执行。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程 (说白了就是把整个图都遍历了,找出没有的对象)

而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程几乎是与与用户线程一起并发地执行。

(5).G1收集器

这个估计是最牛的收集器了。该收集器具有如下特点:

并行与并发:G1能充分利用现代计算器多CPU,多核的硬件优势,可以使用并发或并行的方式来缩短让其他线程暂停的优势。

分代收集:就是类似像分出新生代和老年代那样处理。

空间整合:采用了复制算法+标记-整合算法的特点来回收垃圾。就是整体采用标记-整理算法,局部采用复制算法

可预测停顿:这个就牛了,就是说,它能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

它的执行过程大体如下:

初始标记。

并发标记。

最终标记。

筛选回收。

这个流程和CMS很相似,它也是在初始标记最终标记需要暂停其他线程,但其他两个过程就可以和其他线程并发执行。

刚才我们说了G1收集器哪些优点,例如可预测停顿,这也使得筛选回收,是可以预测停顿垃圾回收的时间的,也就是说,停顿的时间是用户自己可以控制的,这也使得一般情况下,在筛选回收的时候,我们会暂停其他线程的执行,把所有时间都用到筛选回收上。

本次讲解到这里。

关注公我的众号:苦逼的码农,获取更多原创文章,后台回复"礼包"送你一份特别的资源大礼包。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76844.html

相关文章

  • 一文了解JVM

    摘要:而使用虚拟机是实现这一特点的关键。每个字节码指令都由一个字节的操作码和附加的操作数组成。字节码可以通过以下两种方式转换成合适的语言解释器一条一条地读取,解释并执行字节码执行,所以它可以很快地解释字节码,但是执行起来会比较慢。 一、什么是JVM JVM是Java Virtual Machine(Java 虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实...

    whatsns 评论0 收藏0
  • 一文了解JVM全部垃圾回收器,从Serial到ZGC

    摘要:是目前的实验收集器。也是需要暂停程序一切的工作,然后多线程执行垃圾回收。与最大的不同,它关注的是垃圾回收的吞吐量。这里的吞吐量指的是总时间与垃圾回收时间的比例。筛选回收,评估标记垃圾,根据模式回收垃圾。 《对象搜索算法与回收算法》介绍了垃圾回收的基础算法,相当于垃圾回收的方法论。接下来就详细看看垃圾回收的具体实现。 上文提到过现代的商用虚拟机的都是采用分代收集的,不同的区域用不同的收集...

    jasperyang 评论0 收藏0
  • 必知必会JVM垃圾回收——对象搜索算法与回收算法

    垃圾回收(GC)是JVM的一大杀器,它使程序员可以更高效地专注于程序的开发设计,而不用过多地考虑对象的创建销毁等操作。但是这并不是说程序员不需要了解GC。GC只是Java编程中一项自动化工具,任何一个工具都有它适用的范围,当超出它的范围的时候,可能它将不是那么自动,而是需要人工去了解与适应地适用。 拥有一定工作年限的程序员,在工作期间肯定会经常碰到像内存溢出、内存泄露、高并发的场景。这时候在应对这...

    LuDongWei 评论0 收藏0
  • Java11的新特性

    摘要:从版本开始,不再单独发布或者版本了,有需要的可以自己通过去定制官方解读官方细项解读稳步推进系列六的小试牛刀一文读懂的为何如此高效弃用引擎 Java语言特性系列 Java5的新特性 Java6的新特性 Java7的新特性 Java8的新特性 Java9的新特性 Java10的新特性 Java11的新特性 Java12的新特性 Java13的新特性 序 本文主要讲述一下Java11的新...

    April 评论0 收藏0
  • Java基础篇——JVM之GC原理(干货满满)

    摘要:此外,从结果我们可以得知,一个堆对象的放在局部变量表中的第一项引用会永远存在,在方法体内可以将引用赋值给其他变量,这样堆中对象就可以被其他变量所引用,即不会被回收。 原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10697554.html,多多支持哈! 一、什么是GC? GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地...

    liaorio 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<