目录
前言
Java虚拟机规范中提到:"Java堆中存储的对象由自动内存管理系统(垃圾回收器)负责收集,不可以被显式销毁"。这一规定描述了Java作为静态语言有别于C/C++的一大特色:自动管理和回收堆中内存,无需开发者手动触发。这既是Java语言的一大优点,让开发者可以专心于业务开发,但由于早期的垃圾回收器性能不尽如人意,导致程序运行停滞,也难免避免被人诟病。理解Java垃圾回收,对于开发者了解虚拟机运行原理必须跨过的一道坎。
垃圾回收有两个核心问题,一是如何确定回收对象,也就是定位垃圾,二是回收垃圾。在走进这两个主题之前,我们先要确定,垃圾回收的对象是谁?被回收的对象是如何产生的?
垃圾产生
Java运行时空间氛围几个区,分别是程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行时常量池和直接内存,可以用一个图来展示。
其中,堆是运行时所有线程共享的区域,存放的是对象实例和数组,因而实际占用的空间最大。在栈中声明的对象,都是在堆中进行内存分配和初始化。不同的垃圾回收器,内存分布和申请的方式有所区别,但总的来说,都可以理解为把内存分为不同的区块,需要使用对象则从区块中找到适当的区域放置该对象。
有出生,就有毁灭,否则内存无限膨胀,必然将物理内存耗尽。但是对象本身不会自生自灭,Java虚拟机又将清理对象的控制权从程序员手中夺走,因而就需要虚拟机或者说虚拟机中的垃圾回收器来完成对象回收。
堆中的哪一部分对象是有用的,哪一部分对象是已经无效的,这就是垃圾定位算法的主要工作。
垃圾定位
前面我们说了,每一个堆中的对象,当生命周期完成以后,就可能判定为垃圾,也就是等待被回收的对象。垃圾定位是找到这些无效对象的方式,垃圾定位经典的算法有两种,一种是引用计数,一种是可达分析。
引用计数
引用计数,见名知义。Java在虚拟机栈中声明对象,在堆中创建对象,栈中的对象引用指向堆中的实际对象。如在函数中创建一个对象。
private void testGC(){
Object ref1 = new Object();
}
ref1在栈中,是一个引用,实际的对象在堆中。这时堆中的对象就有一个引用指向自己。引用计数为一,如果此时再定义一个变量指向堆中的该对象,则引用计数加一。反之,如果该函数已退出调用,则ref1对应的计数减一。以此类推,当实际堆中的对象没有对它的引用时,这个对象就是可以被回收的。
引用计数实现原理很简单,但是也很容易发现其中可能出现bug的地方。假设有一个环形链表中两个节点a1,a2,每个节点的next引用都指向对方。当需要回收这两个节点对象的时候,发现引用计数永远都无法为零,那a1,a2这两个对象实际上就成了孤岛,既无法回收,也没有作用,这样就显然形成了内存泄漏。
Java虚拟机并没有采用引用计数作为标记算法。
可达分析
引用计数的一个缺点是对所有的对象一视同仁,不论引用来自于谁,都认为是有效引用。这显然是不合理的而且导致了不可回收的问题。那从这个角度下手,只有“有效”的引用,才可以让对象保持状态不被回收,就可以一定程度上解决问题。
这个需要寻找的所谓的"有效",就是"GC Roots"。如下图所示,与"GC Roots"相关联的对象,可以保证不被回收,否则就可能被标记为待回收的对象。
"GC Roots"的对象包括以下几种:
指的是方法栈帧中引用的对象,也就是一个方法中声明一个对象,在这个方法执行过程中,对象是不可以回收的,方法执行完成后,如果没有其他指向该对象的引用,对象可以被回收。
类中静态变量引用的对象,只要类还没有被卸载,那对象就不应该被回收。
主要指字符串常量和符号引用。
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
本地方法申请的对象,包括整个本地方法所申请的区域都不包含垃圾回收的范围中。
垃圾回收
找到垃圾对象以后,后面的工作自然是回收这些对象。怎么回收,采用何种算法能既快速,又准确地清理掉垃圾对象,就要看使用何种垃圾回收算法,下面看几种经典、常用的算法。
标记-清除算法
标记清除(Mark-Sweep)算法的思想是,先将内存分成若干个小的区块以供使用,需要回收时先对这些区块进行一次标记,满足回收条件的区域进行回收。整个过程可以用下图演示。
这种算法的优点是简单粗暴,符合回收条件的区块就可以回收。缺点也显而易见,垃圾回收后,内存中的可用区域可能不连续,空间碎片较多,不利于后面的对象申请。
复制清除算法
复制清除算法,相对来说是一种高效的回收算法。首先将内存划分成两块相等大小的区域,每次只使用其中一块,每一块又划分为若干个小区快,需要垃圾回收时,将使用中一块的有效区块拷贝到另一块上,然后再清除本块。
复制清除算法的优点是速度快,无空间碎片化问题,缺点是空间浪费,需要较大的内存空间。当内存中需要保留的小区块较多,复制的工作量较大时,这种算法就既不节省空间,效率也低了。
标记-整理算法
前面两种算法,要么空间碎片化程度高,要么空间占用多,标记-整理算法,就是同时解决这两者缺陷的一个搞笑算法。算法的思想是,标记出要清除的空间后,不进行回收而是将它们集中移到内存前段。这样既保留的原有的有效数据,又可以释放完整的空闲区间。算法表示见下图。
标记-整理算法,既节省了空间,又避免了内存的浪费,是一个优质高效的算法。但是当堆中留存较多块存活对象,拷贝的工作量较大,也影响了回收效率。
算法对比
这三种算法,在现在的商业垃圾回收器都有使用到,使用场景不同,很难说哪一种独步江湖,否则垃圾回收器的开发工作早该停止了。现在典型的垃圾回收期,会将堆划分为若干区域,如年轻代,老年代,年轻代中又进一步做划分。每一代根据其特性选择不同的回收算法,以期达到最优解决方案。