我们要知道程序计数器、虚拟机栈、本地方法栈,为线程独享的,随着线程的生存而生存的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。
而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。
当我们要进行垃圾回收的时候,首先我们要判断一个对象是否需要被回收。那我们应该如何判断呢?
下面介绍两个基础的方法:
1.引用计数器计算:给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的。不过这个方法有一种情况就是出现对象的循环引用时GC没法回收。
2.可达性分析计算:在JVM中,是通过 可达性分析算法来判断对象是否存活。此算法的基本思想是通过一系列的称为“GC Roots”的对象作为起始点。从这些节点向下开始搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,则证明此对象是不可用的。(这种方法能够解决循环引用的问题,可它的实现需要耗费大量资源和时间)
在Java语言汇总能作为GC Roots的对象分为以下几种:
- 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量)
- 方法区中静态变量所引用的对象(静态变量)
- 方法区中常量引用的对象
- 本地方法栈(即native修饰的方法)中引用的对象
- 已启动的且未终止的Java线程
当我们知道哪些对象需要被回收时,我们要用什么方法把他们回收呢?
1.标记-清除算法:就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。这个套路很简单,但也有缺陷,后续的算法都是以此基础来改进的。其实它就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要new一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。不足的方面就是标记和清除的效率比较低下。且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存。
2.复制算法:把内存分为两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环。这样可以减少内存碎片,垃圾对象多的情况下,效率较高,但是内存使用效率低,只有原来的一半。
3.标记 - 整理算法:的标记过程与“标记 - 清除”算法相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法:这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。