文章目录
  1. 1. GC算法与种类
    1. 1.1. 运行环境
    2. 1.2. GC的概念
    3. 1.3. GC算法
      1. 1.3.1. 引用计数法
      2. 1.3.2. 标记-清除法
      3. 1.3.3. 标记-压缩法
      4. 1.3.4. 复制算法
      5. 1.3.5. GC算法总结
    4. 1.4. 可触及性
    5. 1.5. Stop-The -World

GC算法与种类

本文将介绍 GC的概念GC算法可触及性概念Stop-The-World现象

运行环境

1
2
java version:1.6.0_45
os:Linux centos6

GC的概念

GC是Garbage Collection 垃圾收集。

在Java中GC的操作是在堆和永久区进行的

GC算法

引用计数法

  • 引用计数法,老牌垃圾回收算法,通过计算引用者的数量来回收垃圾。

  • 使用者,python、ActionScript3

  • 引用计数法的问题

    • 引用和去引用伴随着加法和减法,影响性能
    • 很难处理循环引用

    计数清除法-循环引用.jpg)

2标记的这个对象,被根对象引用。当根对象引用消失了。这个时候按照引用计数法的规则,该标记的引用对象还是有一个引用对象。导致该无效的对象无法回收。

标记-清除法

标记清除算法是很多垃圾收集算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始可达的对象。因此,未被标记的对象就是未被引用的垃圾对象。清除阶段,清除所有未被标记的对象。

通过图例表示:

标记清除法图例

标记-压缩法

标记-压缩法适合用于存活对象较多的场合,如老年代。它在标记-清除算法上做了一些优化,和标记-清除算法一样,标记-压缩算法也是从根节点开始标记存活对象;但之后并不是简单的清除未被标记的垃圾对象,而是将所有的存活对象压缩到内存的一端。之后,清除边界外所有的空间

通过图例表示:

标记压缩法图例

上图没有表现标记阶段,上图第一部分表示了标记出存活对象后的移动到内存的边界,第二部分说明了清除边界外的所有空间。

相对于 标记-清除法 的优势,清除后的空间都是连续的内存空间,但是增加了内存的移动成本

复制算法

将内存分成大小相同的两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存中,之后,清除正在使用的内存块中所有的对象,交换两个内存块的角色,完成垃圾回收。

与标记-清除算法相比,复制算法是一种比较高效的回收算法;不适合对象较多的场合,如老年代

通过图例表示:

复制算法

通过上面的描述,可以看出复制算法的空间是浪费了一半,因为每次只能使用其中的一半。

复制和清除算法的应用

上图描述了堆中对象的晋升规则:eden区比较大的对象直接晋升到老年代,因为from和to两个区都比较小(比较小的原因是:使用了复制算法,只能使用一半的存储空间,越大浪费的越多),必须有内存进行担保,这里就是老年代进行担保。

eden区比较小的对象晋升到from或to区。

form或to区的对象经过多次(默认15次)GC还是存活的话,会晋升到老年代

其他对象就会清空

下图通过GC日志分析复制算法:

1
2
3
4
5
6
7
8
9
Heap
def new generation total 12288K, used 10678K [0x00000000f8600000, 0x00000000f9350000, 0x00000000f9350000)
eden space 10944K, 97% used [0x00000000f8600000, 0x00000000f906d820, 0x00000000f90b0000)
from space 1344K, 0% used [0x00000000f90b0000, 0x00000000f90b0000, 0x00000000f9200000)
to space 1344K, 0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9350000)
tenured generation total 27328K, used 0K [0x00000000f9350000, 0x00000000fae00000, 0x00000000fae00000)
the space 27328K, 0% used [0x00000000f9350000, 0x00000000f9350000, 0x00000000f9350200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2574K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb0839e8, 0x00000000fb083a00, 0x00000000fc2c0000)

新生代代的总大小:(0x00000000f9350000-0x00000000f8600000)/1024/1024 = 13m,但是上面显示年轻代总共大小是12m,少了幸存区的一半

分代思想

依照对象的存活周期进行分类,短生命周期的对象归为新生代,长生命周期的对象归为老年代。

根据不同的代的特点,选取合适的收集算法:

  • 少量对象存活,适合复制算法
  • 大量对象存活,适合标记清理或者标记压缩

GC算法总结

  • 引用计数
    • 没有被Java采用
  • 标记-清除
    • 老年代使用
  • 标记-压缩
    • 老年代使用
  • 复制算法
    • 新生代使用

可触及性

  • 可触及的

    • 从根节点可以触及到这个对象
  • 可复活的

    • 一旦所有引用被释放,就是可复活状态
    • 因为finalize()中可能复活该对象
  • 不可触及的

    • 在finalize()后,可能会进入不可触及状态
    • 不可触及的对象不可复活
    • 可以回收

    通过代码进行上述描述的演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class CanReliveObj {
    private static CanReliveObj obj;
    @Override
    protected void finalize() throws Throwable {
    super.finalize();
    System.out.println("CanReliveObj finalize method called");
    obj = this;
    }
    public static void main(String[] args) throws InterruptedException{
    obj = new CanReliveObj();
    obj = null;
    System.gc();
    Thread.sleep(1000);
    if(obj==null){
    System.out.println("obj is null");
    }else {
    System.out.println("obj is not null");
    }
    System.out.println("second gc");
    obj = null;
    System.gc();
    Thread.sleep(100);
    if(obj==null){
    System.out.println("obj is null");
    }else {
    System.out.println("obj is not null");
    }
    }
    }

    输出结果如下:

    1
    2
    3
    4
    CanReliveObj finalize method called
    obj is not null
    second gc
    obj is null

    对象通过调用finalize方法确实实现了复活,并且只会调用一次。

    通过上面的代码我们需要知道,关于finalize方法的相关经验:

    • 经验:避免使用finalize(),操作不慎可能导致错误。
    • 优先级低,何时被调用, 不确定
      • 何时发生GC不确定
    • 可以使用try-catch-finally来替代它

根节点

有哪些对象可以作为根

  • 栈中引用的对象
  • 全局对象
  • JNI(Java native interface)方法栈中的对象

Stop-The -World

stop the world,Java中一种全局暂停的现象;全局停顿,所有Java代码停止运行,native代码可以运行,但不能和JVM交互;多半由于GC引起

通过下面代码描述 stop the world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.HashMap;
public class MyThread extends Thread {
HashMap<Long, byte[]> map = new HashMap<Long,byte[]>();
@Override
public void run() {
while(true){
if (map.size() * 512 / 1024 / 1024 >= 450) {
//大于450m的时候清理内存
System.out.println("======准备清理======:" + map.size());
map.clear();
}
for (int i = 0; i < 1024; i++) {
map.put(System.nanoTime(), new byte[512]);
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public static final long startTime = System.currentTimeMillis();
@Override
public void run() {
while (true) {
System.out.println("t:" + (System.currentTimeMillis() - startTime));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new PrintThread();
t1.start();
t2.start();
}
}

预期,每秒钟有10条输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
time:2018
time:2121
time:2221
time:2325
time:2425
time:2527
time:2631
time:2731
time:2834
time:2935
time:3035
time:3153
time:3504
time:4218
======before clean map=======:921765
time:4349
time:4450
time:4551

在这之中有从3153直接到3504的,中间这些时间就是GC所消耗的时间

文章目录
  1. 1. GC算法与种类
    1. 1.1. 运行环境
    2. 1.2. GC的概念
    3. 1.3. GC算法
      1. 1.3.1. 引用计数法
      2. 1.3.2. 标记-清除法
      3. 1.3.3. 标记-压缩法
      4. 1.3.4. 复制算法
      5. 1.3.5. GC算法总结
    4. 1.4. 可触及性
    5. 1.5. Stop-The -World