《深入理解Java虚拟机》[1]中,有下面这么一段话:
在JVM的各个区域中,如虚拟机栈中,栈帧随着方法的进入和退出而有条不紊的执行者出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译器可知的),因此这几个区域的内存分配和回收都具有确定性,在这几个区域内就不需要考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。
而Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存分配和回收是动态的。垃圾收集器所关注的正式这部分内存该如何管理,本文后续讨论的“内存”分配与回收也仅仅特指这一部分内存。
简单的说,就是线程私有区域的内存好回收,因为其分配多少内存、其中有多少对象、多大对象,因为是属于单个线程私有的内存区域,都是容易确定的;而如Java堆和方法区,因为属于线程公有的内存区域,则具有显著的不确定性,因此需要对于这部分区域设计垃圾收集器。
比如,我们该怎么确定一个对象已死呢?
一,如何判断对象已死
1,引用计数算法
引用计数算法(Reference Counting)
,在对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
这种算法原理简单,判定效率高,有一些比著名的应用案例,如Python语言、微软COM(Component Object Model)技术等。但是,在Java领域,至少主流的Java虚拟机里没有选用引用计数法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确的工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
2,可达性分析算法
当前主流的商用程序语言(Java、C#)的内存管理系统,都是通过可达性分析(Reachability Analysis)
算法来判定对象是否存活的。这个算法的基本思路就是通过一系列成为“GC Roots
”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链(Reference Chain)
”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括很多种,如虚拟机栈的栈帧的本地变量表中引用的对象,如字符串常量池中引用的对象等等。
另外,在JDK1.2之后,为了更清楚的描述对象的引用状态,对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这4种的引用强度逐渐减弱。其中我们最常使用的如"Object o = new Object()"是强引用,其他的引用必须使用对应的类来实现如软引用(SoftReference类)、弱引用(WeakReference类)、虚引用(PhantomReference类),JVM在对待这些引用会有不同的回收时机。
二,分代收集理论
一部分虚拟机是遵循“分代收集”(Generational Collection)的理论进行设计的。它建立在两个分代假说之上:
弱分代假说:绝大多数对象都是朝生夕灭的。
强分代假说:熬过月多次垃圾收集过程的对象就越难以消亡。
因此在应用分代收集理论的商用Java虚拟机中,设计者一般会至少把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。
三,JVM垃圾回收算法
垃圾回收算法这里介绍三种,分别是标记-清除(Mark-Sweep)算法、标记-复制(Semispace Copying)算法、标记-整理(Mark-Compact)算法。
其中,标记-清除(Mark-Sweep)
算法首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收未被标记的对象。其缺点是容易产生内存碎片,标记-清除需要两次扫描所有对象。
标记-复制(Semispace-Copying)
算法解决了标记-清除算法为了解决标记-清除算法效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace-Copyingÿ