我们使用Java的时候不会考虑垃圾对象的释放问题,因为在java程序中有垃圾回收机制帮我们管理内存(叫GC,gabbage collection),垃圾收集器会在适当的时候对没用的对象进行整理与回收。下面简单整理下jvm 中GC相关知识点来学习。
一、什么是垃圾对象
我们知道gc回收垃圾对象,那么什么是垃圾对象,什么样的对象才会被视为垃圾从而被处理掉,如何判断对象可被回收?
1.引用计数法
引用计数法的算法思路:给对象增加一个引用计数器,每当对象增加一个引用计数器+1,失去一个引用-1,所以当计数器是0的时候对象就没有引用了,就会被认为可回收垃圾
引用计数法,可能会出现A引用了B,B又引用了A,这时候就算他们都不再使用了,但因为他们相互引用,计数器=1,永远无法被回收,这是这种方法很大的劣势,但效率较高
在主流的JVM中没有选用引用计数法来管理内存(比如java),最主要的原因是引用计数法无法解决对象的循环引用问题。
2.可达性分析算法
Java并不采用引用计数法来判断对象是否是垃圾,而采用“可达性分析”来进行判断
可达性分析的算法思路:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(即从 GC Roots 到这个对象不可达)时,证明此对象不可用(视为垃圾)。以下图为例:
虽然Object5-7是有联系的,但是他们到gc roots不可达,因此他们会被判定为可回收的垃圾对象
java中可作为GC Roots的对象包含以下几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中静态引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我救赎的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是由虚拟机自动创立的Einalizer队列中判断是否需要执行finalize()方法
当对象变成(GC Roots)不可达时,GC会判断对象是否覆盖了finalize()方法,若未覆盖则直接将其回收。否则若对象未执行finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完毕后,GC会再次判断对象是否可达,若不可达则进行回收,否则对象"复活"
每个对象只能除法一次finalize()方法z
由于finalize()方法运行代价奥昂不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘。
二、内存空间的回收
上面说了如何判定对象是否可以被回收,接下来就要看如何来回收,回收的方法是什么,一般现代的虚拟机有以下的回收算法,他们的具体适用场景也不一样,下面进行简单的整理归纳。
1、标记清除算法。
该算法直接对要回收的对象进行标记,在GC执行时,直接将标记的对象进行回收。这个算法相对来说实现起来比较简单,执行起来速度也快。但是,由于要回收的对象之间的内存块是不连续的,在进行回收之后,会造成很多空间碎片,不便于大对象的创建。
2、标记复制算法。
复制算法的做法和标记整理的算法不同,它是直接将还有用的对象复制到另一个区域,剩下的就是无用的对象,可以直接被回收。复制算法针对的情形是:回收的对象远远多于存活的对象,这种情况在大多数的新生代区域(该区域所有对象都没有经历过垃圾回收)是满足的。
一般来说,虚拟机会将堆中的内存划分为8:1:1(这个比例的确定是因为大多数时候在新生代进行垃圾回收时,回收对象和存活对象大小之比大概为9:1)的大小区域,用于执行垃圾回收算法,一般将它们分别命名为eden和survior区域,看下面示意图:
在上面的内存划分示意图中,eden区域存储的是刚刚new出来的对象(没有经历过垃圾回收),而survior中的一个区域是作为上一次(或更早之前)存活对象的场所,另一个区域是用作回收时的中间站。在进行GC时,会将eden和相应的survivor区域中的存活对象统一复制到另一个作为中间站的survivor中,然后清除剩余的对象,重复这个过程。(当然,survior中的存活对象有可能会不断增多,这是,当有些存活对象年龄足够大时,会被移到老年区,老年区相对与新生代区域垃圾回收的频率会低很多。)当然,如果在新生代出现了存活对象大于10%的情况(例如在某个时刻有一个很大的对象,它在第一次GC时不能进行回收)时,survivor中的内存不够用了,GC程序会提前让对象进入老年区(也就是会采用老年代的区域进行对象的复制),以保证内存空间的充足。所以,很多时候,大对象会直接进入老年区,这样可以防止大对象的频繁复制损耗性能。
显然,复制算法有效解决了内存碎片化的问题,但是,复制对象针对的是存活对象一般保持在10%左右的新生代区域罢了,在老年代区域中,存活对象会更多,并不适合采用该算法,一般会采取下面介绍的标记整理算法。
3.标记整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
4.分代收集算法
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
面试题: 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?
Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。