与其余语言相比,例如c/c++,咱们都知道,java虚拟机对于程序中产生的垃圾,虚拟机是会自动帮咱们进行清除管理的,而像c/c++这些语言平台则须要程序员本身手动对内存进行释放。java
虽然这种自动帮咱们回收垃圾的策略少了必定的灵活性,但却让代码编写者省去了不少工做,同时也提升了不少安全性。(由于像C/C++假如你建立了大量的对象,但却因为本身的疏忽忘了将他们进行释放,可能会形成内存溢出)。c++
刚才说了,虚拟机会自动帮助咱们进行垃圾的清除,那什么样的对象咱们才能够称为是垃圾对象呢?程序员
假如你建立了一个对象算法
Man m = new Man();
你用一个变量指向了这个对象,显然对于这个对象,你能够用变量m对这个对象进行利用,但过了一段时间,你执行了安全
m = null;
而且也并无新的变量来指向刚才建立的对象。此时对于这个没有任何变量指向的对象,你以为它还有用处吗?多线程
显然,对于这种没有被变量指向的对象,它是一点卵用也没有的,它只能在堆随风漂流。并发
所以,对于这样的对象,咱们就能够把它称为垃圾了,它迟早会被垃圾回收器给干掉。优化
假如代码是你本身编写的,你可能知道这个对象啥时候应该被抛弃,你能够随时让它成为垃圾对象。spa
可是,你毕竟是你,虚拟机则没那么智能。那虚拟机是如何知道的呢?线程
上面已经说了,没有变量引用这个对象时,它就是垃圾对象了,基于这个原理,咱们能够这样作啊:
咱们能够为这个对象设置一个计数器,初始值为0,假若有一个变量指向它,那么计数器就加1,若是这个变量不在指向它了,计数器就减1。那么咱们就能够判断,若是这个计数器为0的话,那它就是垃圾对象了,不然就是有用的对象。
对于这种方法,咱们称之为引用计数法。
好吧,咱们先来夸一夸引用计数法这种方法:
很差意思,接下来得说说它那个致命的缺点。
实际上,对于这种引用计数的方法,假如它遇到对象互相引用的话,是很难解决的。
先看一段代码:
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个步骤,包括:
其中初始标记、从新标记这两个步骤仍然须要暂停其余线程。但另外两个步骤能够和其余线程并发执行。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程 (说白了就是把整个图都遍历了,找出没有的对象),
而从新标记阶段则是为了修正并发标记期间,因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但远比并发标记的时间短。
因为整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,因此整体上来讲,CMS收集器的内存回收过程几乎是与与用户线程一块儿并发地执行。
(5).G1收集器
这个估计是最牛的收集器了。该收集器具备以下特色:
它的执行过程大致以下:
这个流程和CMS很类似,它也是在初始标记和最终标记须要暂停其余线程,但其余两个过程就能够和其余线程并发执行。
刚才咱们说了G1收集器哪些优势,例如可预测停顿,这也使得筛选回收,是能够预测停顿垃圾回收的时间的,也就是说,停顿的时间是用户本身能够控制的,这也使得通常状况下,在筛选回收的时候,咱们会暂停其余线程的执行,把全部时间都用到筛选回收上。
本次讲解到这里。
完
关注公个人众号: 苦逼的码农,获取更多原创文章,后台回复"礼包"送你一份特别的资源大礼包。