一、为什么需要垃圾收集机制?
Java虚拟机的堆里存放这正在运行的java程序所创建的所有对象(new),但是没有明确代码释放它们。垃圾收集就是自动释放不再被程序所使用的对象的过程。
二、垃圾收集器的工作?
(1)当一个对象不再被程序所引用时,它所使用的堆空间可以被回收。释放过程中,垃圾收集器运行将要被释放对象的终结方法finalizer。
(2)处理堆碎块。
三、垃圾回收机制优缺点?
优点:提高用户工作效率;帮助程序保持完整性,避免程序员因错误释放内存导致java虚拟机崩溃。
缺点:加大程序负担,可能影响程序性能,因为java虚拟机必须追踪哪些对象被引用,并且动态终结并释放不再被使用的对象,这比明确释放内存相比,需要更多CPU时间;程序员对释放无用对象缺乏控制。
四、垃圾收集算法?
任何垃圾收集算法必须做两件事:检测垃圾对象(即标记);回收垃圾对象所使用的堆空间并还给程序(即清除)。
1、标记算法(2种)
(1)引用计数收集器(早期):
堆中每个对象都有一个引用计数。当一个对象被创建,且指向该对象的引用被分配给一个变量,该对象的引用计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1。当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。所以,此方法中,一个对象被垃圾收集后可能导致后续其他对象的垃圾收集行动。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
缺点: 无法检测出循环引用(即两个或者更多对象互相引用)。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,即使它们已经无法被执行程序的根对象触及,他们的引用计数永远不可能为0。此外,每次引用计数的增加或减少都带来额外开销。
(2)跟踪收集器(现在,标记并清除收集器):
追踪从根节点开始的对象引用图。在追踪过程中遇到的对象以某种方式打上标记。在对象本身设置标记或用独立的位图设置标记均可。当追踪结束,未被标记的对象就是无法触及的,可以被回收。
2、清除算法(4种)
(1)标记-清除算法
最基础的收集算法。首先标记需要回收的对象,之后统一回收所有被标记的对象。
缺陷:效率低、内存碎片。
(2)复制算法(适用于新生代)
复制收集器。“停止-复制”算法。
将内存平均分为两块A和B,每次使用一块。当A块用完,将还存活的对象复制到B块,然后把A块一次清除。
一般的拷贝收集器算法被称为“停止并拷贝”。堆被分为两个区域,任何时候都只是用其中之一,直到这个区域被耗尽,程序执行中止,堆被遍历,遍历时将活动对象拷贝到另一区域。这样反复“停止并拷贝”。
优点:对象可以在从根对象开始的遍历过程中随着发现而被拷贝,不再有标记和清除的区分,不用再考虑碎片问题。
缺点:内存任何时候只能用一半。此外,每一次收集,所有活动对象都必须被拷贝,对象存活率较高时效率低。
应用:现在采用这种方法回收新生代,因为新生代中98%以上是朝生夕死,所以不需要按照1:1比例划分内存,HotSpot虚拟机按照Eden:Survivor=8:1的比例,这样只浪费1/(1+8)=10%内存。将内存分为一块较大的Eden空间,和两块较小的Survivor空间。每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor,之后清除掉Eden和刚才用过的Survivor。当Survivor不够用时,需要依赖老年代进行分配担保,即如果另一块Survivor没有足够空间,那么这些存活对象将通过分配担保机制进入老年代。
(3)标记-整理算法(适用于老年代)
压缩收集器。标记过程与“标记-清除”算法一样。但不直接对可回收对象清理,把活动的对象越过空闲区滑动到堆的一端,在这个过程中,堆的另一端出现一个大的连续空闲区。也就是说,让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
(4)分代收集算法(当代商业虚拟机采用)
根据对象存活周期的不同将内存划分为几块。一般,把java堆分为新生代和老年代,根据各个年代的特点采用适当的收集算法。新生代使用“复制算法”,老年代使用“标记-清理”或“标记-整理”。
解决简单拷贝收集器浪费效率的问题——每次都把生命周期很长的对象来回拷贝。
把对象按照寿命分组,更多的收集短暂出现的年幼对象。堆被划分为两个或更多子堆,每一个子堆为一代对象服务,最年幼的那一代进行最频繁的垃圾收集。因为大多数对象都是短暂出现的,如果一个对象经历几次垃圾回收仍然存活,则就将改对象转移到寿命更高的一代的子堆中。
优点:应用于拷贝算法,标记并清除算法。提高这些基本的垃圾收集算法的性能。
虚拟机中的共划分为三个代:年轻代、年老代和持久代。
年轻代:分三个区(一个Eden区,两个 Survivor区)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个,这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等。
分代垃圾收集,什么情况下触发垃圾回收?
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:
Scavenge GC:一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对 年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因 而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC:对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:年老代(Tenured)被写满;持久代(Perm)被写满; System.gc()被显示调用。
五、终结方法finalize()
终结方法finalize()在垃圾收集器释放对象之前必须执行,且一个对象的finalize方法只执行一次。
(1)垃圾收集器检测出不再被引用的对象(第一遍扫描);
(2)检测不再被引用的对象是否声明了终结方法;如果时间允许,可能在这个时候垃圾收集过程就着手处理这些存在的终结方法。
(3)执行了所有终结方法之后,垃圾收集器必须从根节点开始再次检测不再被引用的对象(第二次扫描)。因为终结方法可能复活了某些不再被引用的对象,使它们再次被引用。
(4)垃圾收集器释放那些第一次和第二次扫描中均没有被引用的对象。
(5)为了减少释放内存的时间,在扫描到某些对象拥有终结方法和执行终结方法之间,垃圾收集器可以有选择地插入一个步骤:从需要执行终结方法的对象开始,运行一次小型追踪。第一遍扫描不可触及,并且小型追踪也不可触及的对象,不可复活立即释放。
(6)如果一个带有终结方法的对象不再被引用,且其终结方法运行过了,垃圾收集器必须记住。如果这个对象被自己或其他对象的终结方法复活,稍后再次不被引用时,垃圾收集器不能再次执行该对象的终结方法,而应将其作为无终结方法的对象对待。
(7)是垃圾收集器运行对象的终结方法。
------------摘自《深入理解java虚拟机 周志明》和《深入java虚拟机第二版》