JDK源码中的help GC 与 JVM的可达性算法分析

时间:2022-12-27 12:19:49
首先提出一个问题,在下面代码中 help GC 注释的这行代码是什么作用?
这只是Jdk1.7 java.util.LinkedList类的一个方法, 完整代码见 Jdk1.7

/**
* Unlinks non-null first node f.
*/

private E unlinkFirst(Node<E> f) {
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC 这一行的代码是什么作用?
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

  我在研究Jdk源码时,注意到Jdk里面非常多地方会把引用置为null,然后标注help gc.
f.next = null很多人很自然的就认为是把next赋值为null了,然后JVM内存空间就释放了, 很多文档和网上的帖子都是这样介绍的, 这是完全错误的.
  当然这样理解对平时的Java代码开发也没什么影响,因为平时大家都是在使用Jdk里面的类和方法,不需要自己编写.但是自定义集合类,特别是在自定义线程安全的并发集合时这块就很重要了.面向java核心功能的开发,需要对JVM的原理有一定的理解.

1. 栈和堆

首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域:
java中内存的分配方式有两种,一种是在堆中分配,一种是在栈中分配,所有new出来的对象都是在堆中分配内存空间的。java中基本数据类型如int,float,double,char,byte等不是对象,除此之外一切都是对象。

  • 方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
  • 常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
  • 堆区:用于存放类的对象实例。
  • 栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的(栈的数据结构是先进后出的),栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。

2. java 和 c语言指针区别

  C语言里的指针大家都知道,就是用来操作内存的. 其实Java中也有指针,
可以认为Java中的引用就是指针,是一种限制的指针,不能指向任意位置的内存,并且不需要显示回收对象,对象回收的工作由JVM自动完成。

3. new关键字 和 gc

String s = new String("test");

分为多个步骤:
1. 用java关键字new创建了一个String对象,Jvm会在堆内存中为其分配内存空间.
2. 在栈中创建了一个指向String对象的引用s
3. 栈中变量(不管是引用还是基本数据类型)的生命周期都是方法级别的, 上面提到了当方法调用完成时,栈帧消失,栈中的引用消失,根据可达性分析算法,在堆中分配的对象不可达就会被垃圾回收。

4. 可达性分析算法

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的.

那么哪些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC Roots:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
    这个就是指每个运行中的方法中申明的指向对象的引用,这些都是GC Roots.
  • 方法区中的类静态属性引用的对象
    这个就是我们常写的Const类中,声明很多static变量,这些引用的对象永远不被回收.
  • 方法区中的常量引用的对象
    枚举对象就算一种常量对象
  • 本地方法栈中JNI(Native方法)的引用的对象
    上面第一条提到的是虚拟机栈,其实就是java的栈; 这里的本地方法栈是native方法的栈,native方法是非java代码编写的方法.

5. Object的finalize方法

  即使在可达性分析算法中不可达的对象,也不是马上就死的.要宣告一个对象死亡.至少要经历两次标记过程:如果对象在可达性分析后发现没有与GC ROOTS相连的引用链,那么他将会被第一次标记.并进行一次筛选.筛选的条件是此对象有没有必要执行finalize方法,如果对象没有覆盖finalize方法或者finalize方法已经被执行过了.虚拟机将视为”没有必要执行”,如果有必要执行finalize方法,那么这个对象会被放入F-Queue队列中,并在稍后由虚拟机创建一个低优先级的Finalizer线程去执行他,这里的执行时值虚拟机会触发这个方法,但不承诺会等待他运行结束.稍后会进行第二次小规模标记.被标记两次的对象基本上他就是真的被回收了.

6. 上面理论理解清楚了后,下面我们写个Demo验证下效果:

package gc;

import java.io.IOException;

public class SystemGc {

static class Node {

String name; //节点名称
Node next; //指向下一个节点

Node(String name) {
this.name = name;
}

@Override
protected void finalize() throws Throwable { //JVM GC时会调用finalize方法
System.out.println("I am removing by GC : " + this.name);
super.finalize();
}
}

private Node head;

public static void main(String[] args) throws IOException {
SystemGc systemGc = new SystemGc();
systemGc.removeHead();

System.gc();

System.in.read();
}

private void removeHead() {
Node a = new Node("node_a"); //新建两个node节点
Node b = new Node("node_b");

this.head = a; //head指向a
a.next = b; //a next执行b,形成一个链表
// a.next = null; //这一步完全可以不需要,a也会被回收!!

unlinkHead(); //其实链表头结点的删除动作,就是将head指向下一个节点,然后头结点不可到达,就会被GC
}

private void unlinkHead() {
Node first = this.head;
head = first.next; //head指向下一个节点.
}

}

执行结果: (VM options添加: -XX:+PrintGCDetails 打印GC日志)

[GC [PSYoungGen: 2601K->632K(75840K)] 2601K->632K(249280K), 0.0272617 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] 
[Full GC (System) [PSYoungGen: 632K->0K(75840K)] [ParOldGen: 0K->541K(173440K)] 632K->541K(249280K) [PSPermGen: 3210K->3208K(21248K)], 0.0222488 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]

I am removing by GC : node_a

 代码里重写了Node对象的finalize,用来检测对象什么时候被回收的.
 可以看到head指向node_b之后, node_a没有引用指向它, 在下一次的GC中就会被回收.
a.next = null; 这一步完全可以不需要,a也会被回收. 这里我们是采用System.gc()方法触发GC. 实际环境中,gc触发的机制相对复杂些,会分为新生代/老年代/永久代,在为对象分配内存空间时,如果空间不足就会触发gc.
 到这里开始提出的那个问题就可以解答清楚了: java中我们只能通过new关键字主动的创建对象,Jvm自动帮我们完成内存分配的工作. 对象内存空间回收的工作是由JVM GC完成的, 我们不能主动的回收对象, Jdk的置null的方法只是help GC帮助回收, f.next = null只是把next引用指向空,在下一次GC时,会由JVM回收堆内存.

7. 思考

 平时开发中我们确实不需要把引用置为null,因为方法执行完毕, 栈中的引用也就释放了. 但是在一些特殊的场景下,我们想帮助虚拟机尽快的回收一对象(比如我在我们创建一些非常大的对象, 并且方法执行的时间很长; 或者是在一个很多的循环中, 创建了很多大的对象), 我们可以在用完后立马将引用置空, 这样可以help gc.
 在我前面的一篇文章中,分析过netty的对象池技术,那里是自己主动进行对象回收和重复使用,在极端场景下, 想要更大的提高程序性能可以使用它.
链接: http://blog.csdn.net/levena/article/details/78144924