1、垃圾回收
(1)哪些对象可以被回收
答:如何判断哪些对象可以被回收?通常来说有两种算法:引用计数法 和 可达性分析法。引用计数法:给对象添加一个引用计数器,每当一个地方引用它时,计数器就+1,;当引用失效时,计数器就-1;任何时刻计数器为0的对象是不能再被使用的,即可回收的。但是,引用计数法存在一个BUG,就是当两个在其他地方无用的对象互相引用时,那么它们的引用计数器都为1,那么他们就会永远不能被回收。可达性分析法:通过一系列的被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots到这个对象不可达时),则证明这个对象是不可用的,可以被回收掉。可以作为GC Roots的对象包含以下几种:①虚拟机栈(栈帧中的本地变量表)中引用的对象②方法区中类静态属性引用的对象③方法区中常量引用的对象④本地方法栈中JNI(即Native方法)引用的对象。
(2)什么时候回收
答:当JVM无法为新对象分配内存时,就会触发GC操作。
(3)如何回收
答:常见的回收算法有以下几种:标记清除算法、复制算法、标记整理算法分代收集算法。
(1)标记-清除算法:
如它的名字一样,算法分为“标记” 和 “清除”两个阶段。首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。标记-清除算法是最基础的收集算法,后续的收集算法都是在它的基础上改进完善而得到的。它的不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;第二个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致在程序运行过程中需要分配较大的对象时,因为无法招到足够的连续内存而不得不提前触发垃圾收集动作,如果程序中的大对象比较多,可能会导致JVM频繁的触发垃圾收集动作。
(2)复制算法
为了解决标记-清除算法的效率问题和内存碎片问题,复制算法产生了。复制算法将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象 复制 到另一块内存,然后把已使用的这块内存整体一次清理掉。这样既提高了效率,也避免了空间碎片的产生。但是,这样却要牺牲掉一半的内存,代价未免太高了一些。但是,根据IBM公司的研究,新生代中的对象98%是朝生暮死的,所以不需要按照1:1的比例来划分内存空间。而是将内存空间划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor,当回收时,将Eden和Survivor上还存活的对象全部 复制 另一块Survivor空间上,然后一次回收Eden和用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor空间的大小比例是8:1:1,这样就只会浪费10%的内存空间,这个代价是可以接受的。
(3)标记-整理算法
复制算法在 对象存活率较高 的情况下需要进行大量的复制操作,而且如果每次回收时都有多余10%的对象存货,那么仅仅一块Survivor空间是不够用的,还需要依赖其他内存进行分配担保。这样效率就会变得很低。于是有人提出了另一种算法,就是 标记-整理 算法。它的标记过程与 标记-清除 算法一样,但是后续步骤不是直接对可回收对象直接进行回收清理,而是让所有的存活对象向一端移动,然后直接清理掉边界以外的内存。避免了空间碎片的产生,而且不需要浪费内存。
(4)分代收集算法
当前商业虚拟机都采用的是分代收集算法。分代收集算法并没有新的算法思想,只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,根据各个年代的特点采用最适当的垃圾收集算法。在新生代中,每次都有大量的对象死去(需要被回收),就选用复制算法,这样只需要付出少量存活对象的复制成本就可以完成收集和清理;而老年代中对象存活率较高且没有额外空间对它进行空间分配担保,就必须要使用 标记-清理 或者 标记-清除 算法来完成。