java垃圾回收机制详解

我们都知道java虚拟机具有强大的垃圾回收功能,使得我们能放心的使用对象而不用担心内存泄露(当然这不是绝对的),
虽然了解垃圾回收机制并不会提升我们编写java代码的能力,但不可否认,一个java程序员要从码农进阶到高级程序员或是架构
师,了解虚拟机的垃圾回收机制是很有必要的,更重要的是了解回收机制将有利于我们定位和分析问题。

1 GC(garbage collection)的起源

GC来自比较古老的语言Lisp(List Processer).java是基于C++开发的,sun在开发java时为了避免C++的内存管理的复杂性,
引入了GC这门技术,实现对象的自动回收.事实上,很多语言垃圾回收机制都是源于Lisp。垃圾回收需要解决以下几个问题:
1)回收那些内存?
2)何时回收?
3)怎么回收
接下来我们将会一一分析这几个问题。

2 回收那些内存?
java虚拟机的内存主要分为堆内存和栈内存两大区域 ,栈里的数据只在方法调用时有用,完成后就可以清空,因此就不存在垃圾
回收这一概念。堆里存储了所有的java对象,这些对象可能被某个静态变量或常量引用,也可能正在参与某个方法的运行,方法调
用完后不会也不能自动清除,随着程序的运行,会有越来越多的对象堆积在堆中,如果不能及时清理,我们的程序就会很快崩溃。事
实上,这些对象中,有许多是不会再被使用的,这些对象应该被清除出堆内存,但是虚拟机怎么判断哪些对象不再被使用呢?
一种比较简单的方法:引用计数判断法:为每个对象增加一个引用计数器,当有引用时加1,引用解除时减1,在内存吃紧时扫描对象,
把那些引用计数为0的对象清除。 这种方法是非常高效的,实现起来也较为容易,但是,他无法解决循环引用的问题。
有如下代码:

public class Node {
private Node parent;

private Node child;

public void setParent(Node node) {
this.parent = node;
}

public void setChild(Node node) {
this.child = node;
}

public Node getParent() {
return parent;
}

public Node getChild() {
return child;
}
.....
public static void main(String[] args) {
Node root = new Node();
Node node = new Node();
//root引用note,note的引用计数加1
root.setChild(node);
//node引用root,root的引用计数加1
node.setParent(root);
//node和root不再被任何对象引用
root = node=null;
}

}

上面的代码中,在执行root = node=null;后,node和root都成为无用对象,但是由于二者相互引用,计数   始终大于0,所以无法被回收,造成内
存泄露。java虚拟机当然不会采用这种方法(详见另一篇文章<<java的内存分配>>),观察上面的代码,我们可以看出,只要一个对象不再被任何对象
引用时,他就应该被回收,但是一个对象一旦没有和任何对象关联,我们也没法追踪这个对象了,实际上我们完全可以这样考虑,既然无法追踪哪些
对象不可用,那我就追踪哪些对象可用,一旦判断了哪些对象可用,我们就可以把其余的对象清除了,在java中一个对象有且仅有以下三种情况是可
用的:
1) 被一个静态变量引用,如: private static CacheManager cmgr = new CacheManager();

2) 被一个常量引用 ,如:final Config cmgr = new Config();

3) 被一个方法引用,如:public void test()
{
Node root = new Node();
....
}
我们可以以这三个引用作为起点开始搜索,把所有与此相连的对象进行标记,剩余的对象就可以被清楚了,这就是著名的GC-ROOT搜索判断法。大部分
虚拟机都采用这种方法对对象进行判定。

3 何时回收?
有了垃圾对象的判定方法,我们现在面临的问题就是在什么时候回收这些对象,一种方案是由程序触发:在运行过程中,如果创建对象时内存
不够就执行一次垃圾清理, 在执行清理是必须停止所有对象的创建,这种方式虽然简单,但是却会造成程序周期性的无响应;另一种方案是主动检测
起一个低优先级别的线程周期性检测并清除无用对象,这个回收线程可以设计成与程序线程并行的,这样方式可以提高程序的响应,但是只靠这种
方案也是不行的,因为这个线程回收的空间可能不能容纳新建的对象,所以需要把两者结合起来,回收线程不间断的运行,为我们的对象准备内存,
创建对象时先检测内存是否够用,如果不够用,就显示地触发一次回收。

4 怎么回收?
在我们知道那些对象需要回收,在什么时候回收后,怎么回收这些对象就显得尤为重要了,java虚拟机主要有以下几种回收方法:
1) Mark-Sweep法
这是最简单的算法,就是采用GC-ROOT搜索标记后,把没有与GC-ROOT关联的对象清除,但是这种算法有个缺陷:会产生
内存碎片。甚至可能出现回收后有大量的空闲内存,但是没有一块连续的内存分配一个大对象。
2) Copy法
这种方法是把堆划分为2份,每次只使用其中一份,使用完时把当前存活的对象copy到另一份中并清空 当前区域,这种方法回收
和分配效率都比较高效,但是将可用的内存减少了一半。
3) 改进的Copy法
这是对方法2的一种改进, 加入分代机制:把堆划分为new generation 和tenured generation,二者的比例大约是1:9,
新生代又划分为一个eden和两个survivor区,比例为8:1:1 每次使用eden和其中的一个survivor区,回收时将存活的对象
copy到空闲的survivor区,这种划分是基于这样一项研究:new generationd 的对象90%都是朝生夕灭的,即是标记之后,要copy的
对象非常少,因此这种方法既发挥第二种方法的高效又充分利用了内存, 但是这种方法也存在两个缺陷:
1可能在某个时刻空闲的survivor不足以容纳存活的对象,这就需要另外一块内存备用,目前的sun的实现是在tenured generation
划一小块内存作为备用
2 如果每次回收后存活的对象过多,就需要很大的空闲区域来容纳这些对象,并且复制这些也很耗时,显然这种方式并不适合tenured
generation。
4) Mark-Compact法
这种方法主要是解决 第三种算法遗留的tenured generation对象回收问题,这种方式是把搜索后的存活对象尽可能的移动到一块连续
的内存中,清除内存之外的对象, tenured generation的对象往往是经历几次回收后依然存活的对象,所以对该区域的内存回收并不会释
放大片的内存区域,而且每次对该区域的回收也较为耗时,所以虚拟机会优先回收 new generation,在新生代回收后依然不足的情况下才会
回收老年代,如果回收后依然不足,虚拟机会尝试回收非堆区域,如方法区等,如果内存还是不够,就会抛出OOM.

java虚拟机的回收机制已介绍完毕。随着回收算法的不断改进,JVM性能也不断提升,相信java会越走越好。

原文地址:https://www.cnblogs.com/czpblog/p/3449509.html