Java垃圾回收机制

时间:2022-05-16 06:15:35

概述

Lisp 第一门真正使用内存动态分配和垃圾收集技术的语言;
为什么学习垃圾回收机制: 当垃圾收集成为系统达到更高并发量的瓶颈时, 我们就需要对这些自动化的技术实施必要的调节和监控。

垃圾收集的3件事情

  1. 哪些内存需要回收;
  2. 什么时候回收;
  3. 如何回收

哪些内存需要回收

首先总结一下Java虚拟机主要的内存区域(主要在第2章):

  1. 程序计数器:当前线程所执行字节码的行号指示器;(通过改变这个字节码, 选取下一条需要执行的命令), 分支, 循环,跳转,异常处理等都依赖它实现;

  2. 虚拟机栈: 方法执行的时候,存储的局部变量表, 操作数栈, 动态链接, 方法出口等信息; (可以理解为常说的栈内存

  3. 本地方法栈: 与虚拟机栈非常相似, 它是为native方法服务的

上面说的3个区域, 程序计数器,虚拟机栈, 本地方法栈,随线程而生,随线程而灭。每一个栈中分配多少内存基本上在类结构确定下来的时候就基本已经定了(虽然运行期间JIT编译器可以优化)

所以, Java垃圾收集器主要要考虑的是下面2块内存区域:

  1. Java堆: 存放对象实例, 几乎所有的对象都在这里分配内存。

  2. 方法区: 用于存储已经被虚拟机加载的类信息/常量/静态变量/编译后的代码等。

针对这两块区域, 我们只有在程序运行的时候才知道需要分配多少内存, 才能知道会创建哪些对象; 因此这部分内存的分配和回收都是动态的。

垃圾判断方法

垃圾判断是指在对对象进行回收前, 第一件事情就是要确定这些对象之中哪些还存活着,哪些已经死去。

1. 引用计数

判断是否存活: 对象添加一个引用计数器, 每当有一个地方引用它的时候, 计数器就加一; 引用实效, 计数器值就减1;任何时刻, 计数器为0的对象就是不可能再被使用的对象。
优点: 实现简单, 判定效率也很高
使用该方法进行内存管理的技术: COM技术/ActionScript3的FlashPlayer, Python语言 / Squirrel;
存在的问题: 很难解决对象之间, 相互循环引用的问题。 下面是针对循环引用问题的一个例子:

public class JvmGCTest {
public class RefereceCountingGC {
public Object instance = null;
}

public void testGC(){
RefereceCountingGC objA = new RefereceCountingGC();
RefereceCountingGC objB = new RefereceCountingGC();

objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

System.gc();
}
}

上述代码中, objA, objB 被赋值为null了之后, 这两个对象已经不能再被访问了, 是可以被回收的。但是如果按照引用计数器的方法, 他们还被引用着, 计数器的值仍为1, 是不能被回收的。

也正是这个问题, 所以主流的Java虚拟机都没有使用“引用计数器”的方法。

2. 可达性分析算法

目前主流实现中, 都是通过可达性分析算法来判断对象是否存活的。
算法思路:
1 把一系列"GC Roots"作为起点;
2 当一个对象, 到达GC Roots 没有任何的引用链相连;
3 那么这个对象就是不可达的, 就可以被回收。
下面这张图可以描述:

Java垃圾回收机制
GC Root


其中, Object 5, 6, 7为不可达的对象。

那么, 最重要的问题来了, 什么样的对象可以作为GC Roots 呢? 
Java语言中, 有以下4中对象: 1, 虚拟机栈中引用的对象。 2, 方法区中类静态属性引用的对象。 3, 方法区中常量引用的对象。 4, 本地方法栈中JNI引用的对象。

3. 引用

上述两种方法判断对象是否存活, 都与“引用”有关:
JDK1.2 之前, Java中引用的定义: 如果reference存储类型的数据中, 存储的数值代表了另一块内存的起始地址, 那么久称这块内存代表着一个引用。
在JDK1.2之后, Java对引用的概念进行了扩充, 将引用分为: 强引用,软引用, 弱引用, 虚引用。
这几个引用的概念,应该说是面试的常客,在这里就不再扩展, 重点强调一下软引用和弱引用区别:
1 软引用: 在系统将要发生内存溢出异常之前, 将会把只有弱引用关联的对象列进回收范围, 然后再判断一下内存情况, 如果还没有足够的内存, 再抛出OutOfMemory异常, 也就是说, 如果内存十分充足, 软引用和强引用几乎没有区别。
2 弱引用:如果一个对象, 只有一个弱引用的时候, 那么这个对象只能存活到下一次垃圾回收发生的时候了。用WeakReference类来实现。 不得不说,这个引用可以避免很多内存泄漏的情况, 在Android中应用实例最多的是Activity生命周期结束了, 但是Activity还被其他静态变量引用着, 如果不及时回收, 就会出现内存泄漏的情况, 所以, 我们一般把Activity在其他对象使用过程中申明为弱引用。

垃圾收集算法

前面描述了两种判断一个对象是否为垃圾(是否已死), 下面就是针对可以回收的对象进行收集的过程, 主要有4中常见的算法。

1. 标记-清除算法

标记-清除(Mark-Sweep)算法是最基础的收集算法, 顾名思义, 他的收集步骤主要是: 1. 标记出所有需要回收的对象, 在标记完成后统一收集所有被标记的对象。
这种方法的不足:
1, 效率问题, 标记和清除两个过程的效率都不高;
2, 空间问题, 清除之后会产生大量不连续的内存碎片。这个问题会导致程序运行过程中, 需要分配较大内存的时候, 无法找到足够的连续的内存而不得不提前出发另一次GC, 而我们的GC又是非常耗时,并且会造成系统短暂的停顿。

2. 复制算法

为了解决上述标记-清除算法的问题(既解决空间问题, 又解决效率问题), 复制算法将内存划分为大小相等的两块, 每次只使用其中的一块。具体步骤如下:
当一块的内存都分配完了, 就把这一块上面的还存活的对象, 复制到另一块内存(按顺序复制, 就空出了后面的整块内存);
然后, 把分配过的内存块一次全部清理掉。

这样做的代价是: 将内存缩小为原来的一半

  • 复制算法优化
    为了解决内存缩小为原来一半的问题, IBM专门研究表明, 新生代中的对象98%是“朝生夕死”, 所以不需要按照1:1的比例来划分内存空间, 而是分配一块较大的Eden空间, 两块较小的Survior空间。
    分配的时候, 将内存分配到Eden和一块Survior空间;
    回收的时候, 将Eden和Survior还存活的对象一次性复制到另一块Survior中。
    这样, 每次分配内存的时候90%的空间是可用的, 被浪费的空间只有10%;
    但是, 这样存在一个隐患, 就是当某一次, 存活的对象在另一块Survior中放不下的时候, 问题就来了。 这个时候我们需要“分配担保”, 放不下的内存会通过分配担保机制进入老年代。

3. 标记-整理算法

上面的复制算法虽然有效的解决了效率和空间问题, 但是也看到了一些不足: 1 需要额外空间进行担保; 2 复制操作过多的时候, 效率就会变得很低。
标记-整理算法, 标记过程还和标记-清除一样, 但是不是直接对垃圾对象进行收集, 而是让存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。

4. 分代收集算法

分代收集算法可以说是前面几种算法的集合, 当前的商业虚拟机垃圾收集都是分代收集。
他的主要思路是根据对象存活周期不同, 将内存分为几块: 新生代和老年代;
新生代, 每次垃圾收集的时候都会有大批对象死去, 少量存货, 那么就采用复制算法;
老年代, 对象存活率高, 没有额外的空间进行分配担保, 就使用“标记清理”或者“标记整理”算法进行回收。

以上就是Java垃圾回收机制使用的一些算法, 在这里做一个记录, 《深入理解Java虚拟机》书上还有更加详细的内容。