Java GC Root

GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

GC Root

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

一个对象可以属于多个root,GC root有几下种:

  • Class – 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  • Thread – 活着的线程
  • Stack Local – Java方法的local变量或参数
  • JNI Local – JNI方法的local变量或参数
  • JNI Global – 全局JNI引用
  • Monitor Used – 用于同步的监控对象
  • Held by JVM – 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于”JVM持有”的了。

看java 代码的时候,很多人都会看见用完一个树或者链表后 head = null;这样的代码。有人说为了加快gc,这里为你揭秘为什么这么写。
代码一定要跑,别的不重要。
代码一定要跑,别的不重要。
代码一定要跑,别的不重要。

  • 首先来看看名词的含义,GC(Garbage Collection )
  • root 根,这个root是不是很熟悉,root用户,数据结构中树的顶级节点
    也叫 root。
  • roots 自然就是多个root了。
  • 从我们熟悉的root里有两个概念,一个是用户,一个是节点,
  • 在GC 里的含义和树里的含义差不多,开始或者顶级的节点。
    概念大概就这样,然后我们看看怎么去理解这个东西。
  • 不知道或者不熟悉的同学可以看这里 点这里了解树

怎么遍历这棵树这里就不阐述了。这里的每个节点你可以理解成每个类里的属性。

下面开始做理解gc roots

这里摘抄R大的一些回答

root大概有这些
所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
JNI handles,包括global handles和local handles(看情况)
所有当前被加载的Java类(看情况)
Java类的引用类型静态变量(看情况)
Java类的运行时常量池里的引用类型常量(String或Class类型)(看情况)
String常量池(StringTable)里的引用
所有Java线程当前活跃的栈帧,静态引用等指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。

假设hashMap 在扩容的时候,正巧发生了gc,在标记阶段,要找出所有的root,这里的 oldTab 就是root,通过这个root,然后遍历他的所有属性,如果它里面的内容不为null 我们继续遍历内容里的对象字段是否为null,标记出所有不为null的节点。继续下图,子节点

静态引用这样的代码也会成为root

private static Object objoct;
这里可以看到Node里还有Node,标记的时候就这这样,遍历每个属性的子属性,一直到遍历完,则这些被标记的为有引用的。gc标记的过程就是一棵树在遍历他所有的节点,这样应该很清楚了吧。最后没有被标记的就给清楚掉。
/**
 * @author 铁拳阿牛
 * @createTime 2018/7/14 下午3:33
 **/
public class Node {

    private Object object;
       //如果这个Node被引用他的属性又被引用,属性为对象且不为null,这样一直遍历下去,
     
    private Node node;
}

理论扯完了,接下来实践


/**
 * @author 铁拳阿牛
 * @createTime 2018/7/14 下午3:46
 *
 * 请把以下参数设置到jvm里
 * -Xmx4000M -Xms4000M -Xmn1300M  -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC  -XX:+UseCMSInitiatingOccupancyOnly  -XX:CMSInitiatingOccupancyFraction=75 -XX:+PrintGCDetails
 **/
public class Test {

    private static final int _1MB = 1024 * 1024;

    private static final int LENGTH = 40000000;

    public static void main(String[] args) {
        Node next = null;
        for(int i = 0; i <= LENGTH; i++){
            Node node = new Node(i,next);
            next = node;
        }
        //如果不设置为null这里将会又大批量的遍历,打开这里和不打开这里,gc时间完全不一样,现在你直到为什么要设置为null了吗?
//        next = null;
        triggerGC();
    }

    /**
     * 不触发gc看不见效果
     * new 很多小对象。不然直接到 old区去了。
     */
    private static void triggerGC(){
//        byte[] all = new byte[2000 * _1MB]; //这里为什么没又直接new 一个大对象?它可能直接就到old区去了。
        for(int i = 0 ; i < 500 ; i++){
            byte[] bytes = new byte[2 * _1MB];
        }
    }

    //POJO 不用看这里
    static class Node {
        
        private int valuel;
        
        private Node node;
        
        public Node(int valuel, Node node) {
            this.valuel = valuel;
            this.node = node;
        }

        public int getValuel() {
            return valuel;
        }

        public void setValuel(int valuel) {
            this.valuel = valuel;
        }

        public Node getNode() {
            return node;
        }

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

}

第一次我们要把eden 的空间尽可能的占满将这两行代码注释掉,发现eden已经用了94% 很不错。

//        next = null;
//        triggerGC();
~~~~~~~~~~~~~~~~~
Heap
 par new generation   total 1198080K, used 1001063K [0x00000006c6000000, 0x0000000717400000, 0x0000000717400000)
  eden space 1064960K,  94% used [0x00000006c6000000, 0x0000000703199e78, 0x0000000707000000)
  from space 133120K,   0% used [0x0000000707000000, 0x0000000707000000, 0x000000070f200000)
  to   space 133120K,   0% used [0x000000070f200000, 0x000000070f200000, 0x0000000717400000)
 concurrent mark-sweep generation total 2764800K, used 0K [0x0000000717400000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2674K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

然后我们开始测试了,发现gc时间非常的短, [Times: user=0.04 sys=0.00, real=0.01 secs] ,gc基本没有占用我们的时间。

        next = null;
        triggerGC();
~~~~~~~~~~~~~~~~~
[GC (Allocation Failure) [ParNew: 1063732K->399K(1198080K), 0.0101658 secs] 1063732K->399K(3962880K), 0.0102629 secs] [Times: user=0.04 sys=0.00, real=0.01 secs] 
Heap
 par new generation   total 1198080K, used 933048K [0x00000006c6000000, 0x0000000717400000, 0x0000000717400000)
  eden space 1064960K,  87% used [0x00000006c6000000, 0x00000006feeca630, 0x0000000707000000)
  from space 133120K,   0% used [0x000000070f200000, 0x000000070f263d38, 0x0000000717400000)
  to   space 133120K,   0% used [0x0000000707000000, 0x0000000707000000, 0x000000070f200000)
 concurrent mark-sweep generation total 2764800K, used 0K [0x0000000717400000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2674K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

然后我们对比测试,发现gc时间非常的短,[Times: user=145.85 sys=1.89, real=20.17 secs] ,额这个gc时间。。。

//        next = null;
        triggerGC();
~~~~~~~~~~~~~~~~~
[GC (Allocation Failure) [ParNew: 1063732K->133119K(1198080K), 20.1710025 secs] 1063732K->938404K(3962880K), 20.1710585 secs] [Times: user=145.85 sys=1.89, real=20.17 secs] 
Heap
 par new generation   total 1198080K, used 1065769K [0x00000006c6000000, 0x0000000717400000, 0x0000000717400000)
  eden space 1064960K,  87% used [0x00000006c6000000, 0x00000006feeca630, 0x0000000707000000)
  from space 133120K,  99% used [0x000000070f200000, 0x00000007173ffff8, 0x0000000717400000)
  to   space 133120K,   0% used [0x0000000707000000, 0x0000000707000000, 0x000000070f200000)
 concurrent mark-sweep generation total 2764800K, used 805284K [0x0000000717400000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2676K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

把这些对象标记后其他的都是要清除的,所以现在知道为什么要设置为null
了吗?就是为了加快标记阶段。如果你有个超大的map或者list的时候这样
做会有一点点帮助。(普通开发过程中基本没有必要考虑)

明白java的源码里为什么会有这些操作了吗?