垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。 提起java的内存回收机制,就要问三个问题
Java内存的动态分配和回收技术已经相当成熟。但是当我们需要排查各种内存溢出和泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就有必要学习一下垃圾回收机制。 在Java内存运行时的各个部分中,程序计数器、虚拟机栈、本地方法栈这三个区域随线程生随线程死。栈中的栈帧随着方法的进入和退出有条不紊的进行着出入栈操作。这几个区域是不需要过多的考虑内存回收的问题,因为方法或者线程结束,内存自然就跟着被回收。 然而,Java堆和方法区则不同——一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样。这部分内存的分配是动态的,我们只有在程序运行期间才能知道会创建哪些对象。这部分内存,就是我们关注的重点
在堆里面存放着Java中几乎所有的对象实例,垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些已经死去(不可能再被使用的对象),哪些还存活着。在回收之前我们必须搞清楚哪些才是“垃圾”需要我们进行回收。
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间作为引用计数器来保存该对象被引用的次数(Reference Count)。每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一。而计数器为0的对象就是没有任何引用的“垃圾”。 客观的说,引用计数算法的实现简单,判断效率高,在大部分情况下都是一个很不错的办法。但是,它最大的弊端就是很难解决对象之间相互循环引用的问题:
public class GC { public Object obj; public static void main() { GC a = new GC(); GC b = new GC(); a.obj = b; b.obj = a; a = null; b = null; } } 复制代码
实际上 a
和 b
这两个对象都已经不可能再被访问了,但是他们因为互相引用,导致计数器不为0,于是它们永远不会被引用计数器算法标记为垃圾。
基于无法解决循环引用的问题,主流的Java虚拟机里没有选用引用计数算法来管理内存。在Java的主流实现中,都是通过可达性算法(Reachability Analysis)来判定对象是否存活。 在Java中:
tracing gc的基本思路是,以当前存活的对象集为root,遍历出他们(引用)关联的所有对象(Heap中的对象),没有遍历到的对象即为非存活对象,这部分对象可以gc掉。这里的初始存活对象集就是GC Roots。 为什么上述四种对象可以作为GC Roots对象可看Home3k的回答
无论是通过引用计数算法还是可达性算法判断对象是否存活,判定条件都与“引用”有关。最早的Java将引用定义为: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用
。后来Java对引用的概念进行扩充,将引用分为:
在确定了“垃圾”是什么——也就是哪些内存需要回收之后,垃圾回收器面临的下一个问题就是——如何进行回收。由于各个平台的虚拟机操作内存的方法各不相同而且涉及大量的程序实现,这里只介绍几种算法的思想。
标记清除算法(Mark-Sweep)——首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程就是上面讲的对象判定是否死亡)。执行过程如下图:
标记清除算法是最基础的收集算法,后续的收集算法都是基于这种思路并对其进行改进而得的。它的不足主要有两个: 效率问题:标记和清除过程的效率不高; 空间问题(碎片化):标记清楚之后会产生大量的不连续的内存碎片,碎片太多可能导致以后在程序在程序运行过程中需要分配大对象时无法找到足够的连续内存。
复制算法(Copying)的出现解决了标记清除算法的内存碎片问题。现在的商用虚拟机都是采用这种算法来回收新生代。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对这呢个半区进行回收,即提高了回收的效率,也解决了内存碎片化的问题。复制算法的执行过程如下:
然而,这种算法的代价是讲内存缩小为原来的一般,代价高到无法接受。幸运的是,研究表明,新生代中的对象98%都是朝生夕死的,所以并不炫耀按照一比一的比例来划分内存空间,而是将内存分为一块较大的Eden区的两个较小的Survivor区,每次使用Eden和其中的一块Survivor。当回收时,将Eden区和Survivor中还存活的对象全部复制到另一块Survivor区,最后清理掉Eden区和刚才用过的Survivor区。
如果Survivor区没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。 复制算法的缺点主要有:
复制算法的缺点使得它只适用于对象存活率较低的新生代。 标记整理算法(Mark-Compact)标记过程仍然与标记清除算法一样,但之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
当前商业虚拟机的垃圾收集算法都采用“分代收集”算法。这种算法并没有什么新的思想,只是根据对象存活周期不停将内存划分为几块,这样就可以根据各个年代的特点采用最适当的收集算法。 新生代(Young Generation):在新生代中,因为大量对象的声明周期都很短,每次回收垃圾时都有大批对象以及死去,只有少量存活,这里的GC采用复制算法,只需付出复制少量存活对象的成本就能完成GC。这个GC机制被称为Minor GC或叫Young GC。 老年代(Old Generation):老年代中存放的对象存活率高,使用复制算法不仅效率低下而且极度浪费内存空间。这里的GC一般使用标记清理或者标记整理算法。这里的GC叫做Full GC或者Major GC。 永久代(Permanent Generation):永久代中的对象生成后几乎是永生的,回收的东西有两种:常量池中的常量,无用的类信息。
对象的内存分配,往大了讲就是在堆上的分配。接下来我们学习几条普遍存在的内存分配规则
优先在Eden区分配 大对象直接进入老年代 长期存活的对象进入老年代 动态年龄判定 空间分配担保
到这里GC的基本概念已经讲完,更详细的内容请持续关注我的博客