浅谈Java GC

1.GC 基础

1.1什么是GC?

GC(Garbage Collection)——垃圾收集,GC就是找到内存空间的垃圾,然后回收垃圾,让程序员能够再次利用这部分空间的一种自动管理内存的机制

图1.1

如图1.1所示:当我们new 一个Object的时候,系统就为我们在内存中分配了一块空间。并且我们让Object类型的引用obj指向了这块空间。之后又将obj的引用置为null(置为null其实是让这个引用在栈中分配空间,但是没有指向堆中的某块空间)。此时之前new Object()的时候分配的那块空间就无法被引用了,就成为了垃圾,我们称他为死亡的对象。GC就是回收这种无法被引用的对象的机制。

1.2 为什么我们要学GC?

使用CG只有一个目的,就是“偷懒”——简化开发。提高编程效率。

我们先来看看不使用GC的语言(比如C++),如果没有GC,程序员需要手动管理内存,然而人为的手动释放资源容易出现问题,比如内存泄漏以及垂悬指针:

内存泄漏: 内存空间在使用完毕后未释放
垂悬指针(迷途指针): 当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址

有GC的语言(比如Java、Python、 Lisp 、Perl、Ruby、Haskell),自动管理内存;省去手动管理内存的麻烦,减少因内存分配引发的Bug。

既然GC都帮我们管理好了内存,为啥我们还要学习GC?是不是可以不用学习GC了?
——不,GC是Java程序员进阶必备知识之一。虽然GC能自动帮我们管理内存,但是GC不是万能的,GC知识一个辅助工具,一旦程序出现了内存方面的BUG,如果我们不理解GC机制,就很难定位BUG,并且,有效的整理GC,可以提高程序的运行效率。所以,我们需要学习GC。

2 对象的生存判定

既然GC要回收对象,那么GC就需要判定对象是否存活,这就是对象的生存判定。

我们先来简单研究一下GC中对象的结构。

对象这个词,在不同的使用场合其意思各不相同。比如,在面向对象编程中,它指“具有属性和行为的事物”,然而在 GC 的世界中,对象表示的是“通过应用程序利用的数据的集合”。 ——《垃圾回的算法与实现》

我们将对象中保存对象本身信息的部分称为“头”。头主要含有以下信息:对象的基本信息(大小、种类等)、哈希码、GC分代年龄、线程持有锁等。
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。

我们把对象使用者在对象中可访问的部分称为“域”,域中可以分为指针和非指针。

图2.1

注:在Hotspot中,将对象分为对象头,实例数据以及对齐填充(可选)如图2.2:

图2.2

2.1引用计数法

GC是释放无法被引用的对象的机制,那么我们可不可以记录下有多少指针指向自己?记录下对象的人气指数,从而让没有人气的对象消失——这就是引用计数法。

引用计数法的每个对象的对象头中有一个计数器,如图2.3,记录着这个对象被引用的次数,当这个计数器的值为0时,就会被判定为无用的对象,等待被回收。(下一次GC触发时就会回收这个对象)

图2.3

如图:将A指向B的引用改为A指向C,B的计数器就为0,就会回收B所占的内存。(如使用“空闲链表”的内存分配方法就会加入空闲链表)。
image.png

新建对象时,会初始化对象的计数器为1:

new_obj(size){
  obj = pickup_chunk(size, $free_list)  //遍历空闲链表$free_list,寻找>=size的分块
  if(obj == NULL)
    allocation_fail()//分配失败,销毁至今为止所有计算成功
  else
    obj.ref_cnt = 1
    return obj
}

更新对象指针时:

//更新指针 ptr 指向对象 obj,并更新计数器
update_ptr(ptr, obj){
  inc_ref_cnt(obj)   // 先新引用对象的计数器+1
  dec_ref_cnt(*ptr) //后旧引用对象的计数器-1
  *ptr = obj
}
// 增加对象计数器值
inc_ref_cnt(obj){
  obj.ref_cnt++
}
// 减少对象计数器值
dec_ref_cnt(obj){
  obj.ref_cnt--
  if(obj.ref_cnt == 0)
    for(child : children(obj))
      dec_ref_cnt(*child)
    reclaim(obj)
}

为什么要先调用 inc_ref_cnt() 函数,后调用dec_ref_cnt() 函数呢?
从引用计数算法的角度来考虑,先调用 dec_ref_cnt() 函数,后调用 inc_ref_cnt() 函数才合适吧。答案就是“为了处理 ptr 和 obj 是同一对象时的情况”。如果按照先 dec_ref_cnt() 后 inc_ref_cnt() 函数的顺序调用,ptr 和 obj 又是同一对象的话,执行 dec_ref_cnt(*ptr) 时 ptr 的计数器的值就有可能变为 0 而被回收。这样一来,下面再想执行 inc_ref_cnt(obj) 时 obj 早就被回收了,可能会引发重大的 BUG。因此我们要通过先对 obj 的计数器进行增量操作来回避这种 BUG。

引用计数器虽然简单,但是有着一些不可避免的缺点:

  • 计数器的宽度应该设置为多少合适?
    计数器位数太少,当存在被多次引用的对象时,就会溢出;当计数器位数太多,如果存在很多占空间比较小的对象时,空间利用率就大大下降。
  • 引用计数器最大的缺点就是循环引用无法回收。
class Person{                      // 定义Person类
  string name                      // 
  Person lover                     // 
}
taro = new Person("太郎 ")          // 生成 Person类的实例太郎
hanako = new Person("花子 ")        // 生成 Person类的实例花子
taro.lover = hanako                 // 太郎喜欢花子
hanako.lover = taro                 // 花子喜欢太郎
taro = null                         // 将 taro转换为 null 
hanako = null                       // 将 hanako转换为 null

图2.4
如图2.4,因为两个对象互相引用,所以各对象的计数器的值都是 1。但是这些对象组并没有被其他任何对象引用。因此想一并回收这两个对象都不行,只要它们的计数器值都是 1,就无法回收。
##2.2可达性分析
另一种对象生存判定的方法就是可达性分析;其基本思想是:通过一系列的“GC Roots”对象作为起点进行搜索(搜索走过的路径称为“引用链”),如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,比如图2.5中的object5、object6、object7。
图2.5 可达性分析

在Java语言中,可作为GC Roots的节点主要在执行上下文(例如栈帧中的本地变量表)与全局性的引用(例如常量或类静态属性)中,可分为以下几种:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 本地方法栈中JNI的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象

不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

2.3 一些题外话补充

###2.3.1两次标记过程
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1).第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
2).第二次标记
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
如图2.6:
图 2.6

###2.3.2 finalize()
上面的两次标记过程和finalize()方法有关,所以我们来看一下finalize()方法。

  • Finalize方法是什么?
    • finalize()是Object的protected方法,
    • 子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法,在此方法中对象可以将自己与一个有效引用绑定,这是对象自救的唯一方式
  • finalize()与C++中的析构函数不是对应的。
    • Java中的finalize的调用具有不确定性。并且不一定能够执行
    • C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉)

finalize()的使用:

  • 对象自救的唯一方式
public class FinalizeEscapeGC {  
    public static FinalizeEscapeGC SAVE_HOOK = null;  
    public static void main(String[] args) throws InterruptedException {  
        SAVE_HOOK = new FinalizeEscapeGC ();  
        SAVE_HOOK = null;  //1.第一次取消引用SAVE_HOOK与其实例的关联
        System.gc();  //2.触发GC
        Thread.sleep(500);  
        if (null != SAVE_HOOK) {//3.finalize()是否执行
            System.out.println(“Yes , I am still alive”); //4.对象执行了finalize()方法
        } else {  
            System.out.println("No , I am dead");  
        }  
        SAVE_HOOK = null; //5.二次.取消关联
        System.gc(); 6.二次触发GC
        Thread.sleep(500);  
        if (null != SAVE_HOOK) {//7.finalize()是否执行
            System.out.println("Yes , I am still alive");  
        } else {  
            System.out.println(“No , I am dead”); //8.finalize()最多执行一次
        }  
    }  
    @Override  
    protected void finalize() throws Throwable {  
        super.finalize();  
        System.out.println("execute method finalize()");  
        SAVE_HOOK = this;  
    }  
} 

输出

execute method finalize()
Yes , I am still alive
No , I am dead
  • 清理本地对象(通过JNI创建的对象);或作为确保某些非内存资源释放的一个补充。如FileInputStream中的一段代码:
    /**
     * Ensures that the <code>close</code> method of this file input stream is
     * called when there are no more references to it.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FileInputStream#close()
     */
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
}

类似的用法还存在于FileOutPutStream、Connection类中。

###2.3.3 引用

对象的存活和对象是否被引用有关,下面我们来看一下java中的引用。引用可以简要定义为:

数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

但是java中定义了四大引用,除了强引用外都是Reference类的子类

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用
    image.png
  1. 强引用在程序内存不足(OOM)的时候也不会被回收
public class MyReferenceTest {

    public static final int _1M =  1024 * 1024;
    // -Xms10m  -Xmx10m
    public static void main(String[] args) {
        // 使用List保证着对象的引用,避免因为无法引用而被GC
        List<Byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new Byte[_1M]);
            System.gc();
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.seaxll.learn.jvm.reference.MyReferenceTest.main(MyReferenceTest.java:25)

Process finished with exit code 1

像这样直接new一个对象就是一个强引用,这里既是发生了OOM异常也不收集这部分垃圾。
2. 当内存不足时,回收软引用:它的作用是告诉垃圾回收器,程序中的哪些对象是不那么重要,当内存不足的时候是可以被回收的。

public class SoftReferenceTest {

    public static final int _1M = 1024 * 1024;

    // -Xms10m -Xmx10m
    public static void main(String[] args) {
        SoftReference<Byte[]> softReference = new SoftReference<>(new Byte[_1M]);

        // 使用List保证着对象的引用
        List<Byte[]> list = new ArrayList<>();
        while (true) {
            if (softReference.get() != null) {
                list.add(new Byte[_1M]);
                System.out.println("list.add");
            } else {
                System.out.println("---------软引用已被回收---------");
                break;
            }
            System.gc();
        }
    }
}
list.add
list.add
list.add
list.add
list.add
list.add
list.add
---------软引用已被回收---------

软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
3. 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收
弱引用只能生存到下一次GC,作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。

public class WeakReferenceTest {
    static class TestObject {
    }
    public static void main(String[] args) throws InterruptedException {
        WeakReference<TestObject> weakReference =
                new WeakReference<>(new TestObject());
        System.out.println(weakReference.get() == null);//false
        System.gc();
        TimeUnit.SECONDS.sleep(1);//暂停一秒钟
        System.out.println(weakReference.get() == null);//true
    }
}
false
true

Process finished with exit code 0

弱引用最常见的用处是在集合类中,尤其在哈希表中。(WeakHashMap、ThreadLocal)
4. 虚(幽灵)引用的回收机制跟弱引用差不多,但它不能单独使用,虚必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,大多被用于引用销毁前的处理工作。不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。

public class PhantomReferenceTest {
    public static void main(String[] args) throws Exception {
        String str = new String("SuperMap");
        ReferenceQueue rq = new ReferenceQueue();
        PhantomReference pr = new PhantomReference(str, rq);
        str = null;
        System.out.println(pr.get()); // null
        System.gc();
        System.runFinalization();
        System.out.println(rq.poll() == pr);    // true
    }
}
null
true

Process finished with exit code 0

如下,JDK中虚引用唯一的构造函数必须传入一个ReferenceQueue

public class PhantomReference<T> extends Reference<T> {

    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }

    /**
     * Creates a new phantom reference that refers to the given object and
     * is registered with the given queue.
     *
     * <p> It is possible to create a phantom reference with a <tt>null</tt>
     * queue, but such a reference is completely useless: Its <tt>get</tt>
     * method will always return null and, since it does not have a queue, it
     * will never be enqueued.
     *
     * @param referent the object the new phantom reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

#3.垃圾回收算法
了解了GC如何判定存活,我们再来看一下垃圾回收算法,常用的垃圾回收算法有三种:

  • 标记清除算法
  • 标记整理算法
  • 复制算法
    ##3.1标记清除算法
  • 算法描述:
    • 先标记出所有需要回收的对象(图中深色区域);
    • 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
  • 不足:
    • 效率问题:标记和清理两个过程的效率都不高。
    • 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。如图3.1即使剩余16块空间,但是却无法完整的分配大小为3的对象。
      图3.1
      ##3.2解决碎片化——标记整理算法
  • 算法描述:

    • 标记方法与 “标记 - 清除算法” 一样;

    • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。

  • 不足: 存在效率问题,适合老年代。

图3.2 标记整理算法
##3.3解决效率问题——复制算法

  • 算法描述:
  • 将可用内存分为大小相等的两块,每次只使用其中一块;
    • 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
  • 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
    图3.3 复制算法

##3.4 回收策略——分代回收算法

  • 新生代: GC 过后只有少量对象存活 —— 修改的复制算法
  • 老年代: GC 过后对象存活率高 —— 标记 - 整理算法

新生代改进版复制算法:

  • 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
  • 把内存划分为:
    • 1 块比较大的 Eden 区;
    • 2 块较小的 Survivor 区;
  • 说明:
    • 每次使用 Eden 区和 1 块 Survivor 区;
    • 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
    • JVM 参数设置:-XX:SurvivorRatio=8 表示 Eden 区大小 / 1 块 Survivor 区大小 = 8
  • 好处:
    • 减少进入老年代的几率,降低Full GC 触发的频率(Full GC的触发伴随着Stop The World)

如下图:为一个新生代搜集的大致流程,当对象年龄到达一定年龄(默认为16)时,会将其升代到老年代。并且,大对象也会直接分配到老年代。
图3.5 新生代的复制算法

新生代的GC是一个循环的过程:
Eden + From -> To 然后 交换From 与 To
Eden + From -> To 然后 交换From 与 To

或理解为:
Eden + S0 -> S1
Eden + S1 -> S0

Eden + S0 -> S1
Eden + S1 -> S0

4.HotSpot 中 GC 算法的实现

通过前对于判断对象生死和垃圾收集算法的介绍,我们已经对虚拟机进行 GC 的流程有了一个大致的了解。但是,在 HotSpot 虚拟机中,高效的实现这些算法也是一个需要考虑的问题。所以,接下来,我们将研究一下 HotSpot 虚拟机到底是如何高效的实现这些算法的,以及在实现中有哪些需要注意的问题。

通过之前的分析,GC 算法的实现流程简单的来说分为以下两步:

  1. 找到死掉的对象;
  2. 把它清了。

想要找到死掉的对象,我们就要进行可达性分析,也就是从 GC Root 找到引用链的这个操作。

也就是说,进行可达性分析的第一步,就是要枚举 GC Roots,这就需要虚拟机知道哪些地方存放着对象引用。如果每一次枚举 GC Roots 都需要把整个栈上位置都遍历一遍,那可就费时间了,毕竟并不是所有位置都存放着引用。所以为了提高 GC 的效率,HotSpot 使用了一种 OopMap 的数据结构,OopMap 记录了栈上本地变量到堆上对象的引用关系,也就是说,GC 的时候就不用遍历整个栈只遍历每个栈的 OopMap 就行了。

在 OopMap 的帮助下,HotSpot 可以快速准确的完成 GC 枚举了,不过,OopMap 也不是万年不变的,它也是需要被更新的,当内存中的对象间的引用关系发生变化时,就需要改变 OopMap 中的相应内容。可是能导致引用关系发生变化的指令非常之多,如果我们执行完一条指令就改下 OopMap,这 GC 成本实在太高了。

因此,HotSpot 采用了一种在 “安全点” 更新 OopMap 的方法,安全点的选取既不能让 GC 等待的时间过长,也不能过于频繁增加运行负担,也就是说,我们既要让程序运行一段时间,又不能让这个时间太长。我们知道,JVM 中每条指令执行的是很快的,所以一个超级长的指令流也可能很快就执行完了,所以 真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等,虚拟机一般会将这些地方设置为安全点更新 OopMap 并判断是否需要进行 GC 操作。

此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即 Stop The World(传说中的 GC 停顿),因为如果在我们分析可达性的过程中,对象的引用关系还在变来变去,那是不可能得到正确的分析结果的。即便是在号称几乎不会发生停顿的 CMS 垃圾收集器中,枚举根节点时也是必须要停顿的。这里就涉及到了一个问题:

如何让所有线程跑到最近的安全点再停顿下来进行 GC 操作呢?

主要有以下两种方式:

  • 抢先式中断:

    • 先中断所有线程;

    • 发现有线程没中断在安全点,恢复它,让它跑到安全点。

  • 主动式中断: (主要使用)

    • 设置一个中断标记;

    • 每个线程到达安全点时,检查这个中断标记,选择是否中断自己。

除此安全点之外,还有一个叫做 “安全区域” 的东西,一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于 Sleep 或者 Blocked 状态的线程是没办法自己到达安全点中断自己的,我们总不能让 GC 操作一直等着这些个 ”不执行“ 的线程重新被分配资源吧。对于这种情况,我们要依靠安全区域来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。

当线程执行到安全区域时,它会把自己标识为 Safe Region,这样 JVM 发起 GC 时是不会理会这个线程的。当这个线程要离开安全区域时,它会检查系统是否在 GC 中,如果不在,它就继续执行,如果在,它就等 GC 结束再继续执行。

至于如何清理垃圾,我们来看一下垃圾收集器

4.1 七大垃圾收集器

垃圾收集器

###4.1.1 新生代垃圾收集器

  1. Serial 收集器:最古老的的收集器,一种单线程收集器
    单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束;
    图4.1Serial垃圾收集器
  • 优点:简单高效,Client模式下新生代收集器常用
  • 缺点:GC时Stop The World,用户体验感差。
  1. ParNew 收集器:多线程收集器,Serial 收集器的多线版本
    图4.2 ParNew收集器
  • 优点:多CPU环境收集效率比Serial收集器强, 单CPU下线程切换开销会降低其效率.
  • 缺点:GC时Stop The World,用户体验感差
  1. Parallel Scavenge 收集器:专注于吞吐量控制的多线程收集器
    无法与老年代的CMS收集器搭配工作,Parallel Old出现之前,只能与Serial Old搭配使用
    吞吐量:CPU用于运行用户代码的时间与CPU消耗的总时间(用户+GC)的比值

可以通过参数来打开自适应调节策略:

  • -XX:MaxGCPauseMillis :最大停顿的时间,但最大停顿时间过短必然会导致新生代的内存大小变小,垃圾回收频率变高,效率可能降低。
  • -XX:CGTIMERatio :吞吐量大小(0-100),默认为99。
  • -XX:+UseAdaptiveSizePolicy:自动调节开关

图4.3 Parallel Scavenge收集器

  • 优点:可以精确控制吞吐量
  • 缺点:可能会停顿时间变短, 但收集次数变多.原本10s收集一次, 每次停顿100ms, 设置完参数之后可能变成5s收集一次, 每次停顿70ms.

为什么Parallel Scavenge不能与CMS搭配使用?

  • HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用而ParallelScavenge与G1则不在这个框架内,而是各自采用了自己特别的框架。这是因为新的GC实现时发现原本的分代式GC框架用起来不顺手。
    Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用。

4.1.2 老年代垃圾收集器

老年代的收集器有部分新生代对应的:如

  1. Serial Old——对应新生代的Serial,都是串行收集器
    图4.4 Serial Old
  • 优点:简单高效
  • 缺点:停顿时间长
  1. Parallel Old——对应新生代的Parallel Scavenge收集器,都是关注吞吐量的多线程收集器
    图 4.5 Parallel Scavenge
  • 优点:
    1.多线程收集
    2.弥补了之前Parallel Scavenge + Serial Old的尴尬组合.
  • 缺点:GC时停顿
  1. CMS收集器( 重视服务的响应速度)

执行过程分为四个阶段:

  • 初始标记:标记老年代中所有的GC Roots对象和年轻代中活着的对象引用到的老年代的对象,时间短;
  • 并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象;
  • 重新标记:用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象,时间短;
  • 并发清理:清除那些没有标记的对象并且回收空间。

图4.6 CMS

  • 优势:并发、低停顿
  • 缺点:
    1.CMS对CPU资源非常敏感
    2.CMS无法收集浮动垃圾
    3.使用标记-清除算法,产生碎片

###4.1.3 G1收集器(Garbage First)
从最早的串行到高顿吞吐量的并行,为了解决高延迟又演化出了CMS(Concurrent Mark Sweep),为了解决碎片的问题,后来又开发了G1.
图4.7 G1

相比CMS收集器, G1收集器有两个改进点

  1. G1基于标记-整理算法, 不会产生空间碎片
  2. G1可以精确控制停顿, 能让使用者明确指定在一个长度为M毫秒的时间片段内, 消耗在垃圾收集器上的时间不得超过N毫秒.

G1收集器和CMS收集器类似,分为四个步骤

  • 初始标记:仅仅只是标记一下GC. Roots能直接关联到的对象,且修改TAMs( Next Top at Mark, Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region中创建新对象,这阶段需要停顿线程,但耗时很短
  • 并发标记:是从 GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那部分标记记录;虚拟机将这段时间对象变化记录在线程 Remembered Set. Logs里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set p中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对各个 Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信悬来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收三部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

G1的优点:

  • 并行(多CPU)与并发
  • 分代收集(新生代和老年代区分不明显)适用于整个堆区。
  • 空间整合
  • 限制收集范围,可预测的停顿

4.2 垃圾收集器的搭配使用

  • UseSerialGC是“ Serial” +“ Serial Old”
  • UseParNewGC是“ ParNew” +“ Serial Old”
  • UseConcMarkSweepGC是“ ParNew” +“ CMS” +“ Serial Old”。大多数情况下,使用“ CMS”来收集保有权代。并发模式失败时使用“ Serial Old”。
  • UseParallelGC是“ Parallel Scavenge” +“ Serial Old”
  • UseParallelOldGC是“ Parallel Scavenge” +“ Parallel Old”

5.内存调试工具

图5-1 命令行调试工具
jps、jstat、jinfo、jstack用得较多,另外两个少用,所以这里记录四种常用的

5.1 命令行调试工具

5.1.1 jps 虚拟机进程状态工具

命令格式:jps [options ] [ hostid ]

[options]选项 :
-q:仅输出VM标识符,
-m:输出main method的参数
-l:输出完全的包名,应用主类名,jar的完全路径名
-v:输出jvm参数
-V:输出通过flag文件传递到JVM中的参数
-Joption:传递参数到vm,例如:-J-Xms512m

image.png

5.1.2 jstat 虚拟机统计信息监视器

命令格式:jstat - [-t] [-h] [ []]

option:

  • class:统计classloader的行为
  • compiler:统计hotspot just-in-time编译器的行为
  • gc:统计gc行为
  • gccapacity:统计堆中代的容量、空间
  • gccause:垃圾收集统计,包括最近引用垃圾收集的事件,基本同gcutil,比gcutil多了两列
  • gcnew:统计新生代的行为
  • gcnewcapacity:统计新生代的大小和空间
  • gcold:统计旧生代的行为
  • gcoldcapacity:统计旧生代的大小和空间
  • gcpermcapacity:统计永久代的大小和空间
  • gcutil:垃圾收集统计
  • printcompilation:hotspot编译方法统计

其他选项

  • -h n 每n个样本,显示header一次
  • -t n 在第一列显示时间戳列,时间戳时从jvm启动开始计算
  • 就是进程号
  • interval是监控时间间隔,单位为微妙,不提供就意味着单次输出
  • count是最大输出次数,不提供且监控时间间隔有值的话, 就无限打印

image.png

内存分析规律总结:
S0/S1:幸存者0/1区;E:Eden;O:Old;P:永久代;M:元空间…

  • 默认为已使用的占当前容量百分比
  • 加C:总容量,单位字节
  • 加U:已使用容量,单位字节
  • 加MN:初始(最小)
  • 加MX:最大

GC/YGC/FGC/…

  • 默认次数
  • 加T:所用时间
  • NGC/OGC/PGC…
  • 默认大小
  • 加MN:初始(最小)
  • 加MX:最大

其他

  • DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
  • TT: 持有次数限制
  • MTT : 最大持有次数限制

比如下面详细的一些说明:

S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
EC:年轻代中Eden(伊甸园)的容量 (字节)
EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
OC:Old代的容量 (字节)
OU:Old代目前已使用空间 (字节)
PC:Perm(持久代)的容量 (字节)
PU:Perm(持久代)目前已使用空间 (字节)
YGC:从应用程序启动到采样时年轻代中gc次数
YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
FGC:从应用程序启动到采样时old代(全gc)gc次数
FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
GCT:从应用程序启动到采样时gc用的总时间(s)
NGCMN:年轻代(young)中初始化(最小)的大小 (字节)
NGCMX:年轻代(young)的最大容量 (字节)
NGC:年轻代(young)中当前的容量 (字节)
OGCMN:old代中初始化(最小)的大小 (字节)
OGCMX:old代的最大容量 (字节)
OGC:old代当前新生成的容量 (字节)
PGCMN:perm代中初始化(最小)的大小 (字节)
PGCMX:perm代的最大容量 (字节)
PGC:perm代当前新生成的容量 (字节)
S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
O:old代已使用的占当前容量百分比
P:perm代已使用的占当前容量百分比
S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)
S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
TT: 持有次数限制
MTT : 最大持有次数限制

5.1.3 jinfo

Jinfo的作用是实时查看虚拟机的各项参数信息jps –v可以查看虚拟机在启动时被显式指定的参数信息,但是如果你想知道默认的一些参数信息呢?除了去查询对应的资料以外,jinfo就显得很重要了。

命令格式:jinfo [option] pid,详细如图
命令格式

  • pid: 对应jvm的进程id
  • executable core :产生core dump文件
  • [server-id@]remote server IP or hostname :远程的ip或者hostname,server-id标记服务的唯一性id

option:

  • -flag name 输出对应名称的参数
  • -flag [+|-]name 开启或者关闭对应名称的参数
  • -flag name=value 设定对应名称的参数
  • -flags 输出全部的参数
  • -sysprops 输出系统属性
  • no option 输出全部的参数和系统属性

Javacore,也可以称为“threaddump”或是“javadump”,它是 Java 提供的一种诊断特性,能够提供一份可读的当前运行的 JVM 中线程使用情况的快照。即在某个特定时刻,JVM 中有哪些线程在运行,每个线程执行到哪一个类,哪一个方法。
应用程序如果出现不可恢复的错误或是内存泄露,就会自动触发 Javacore 的生成。
image.png
如上图举例- flag参数 :开启了PrintDetails,没有卡其PrintHeapAtGC参数

5.1.4 jstack

jstack,用于JVM当前时刻的线程快照,又称threaddump文件,它是JVM当前每一条线程正在执行的堆栈信息的集合。生成线程快照的主要目的是为了定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部时长过长导致线程停顿的原因。通过jstack我们就可以知道哪些进程在后台做些什么

  • -F:当正常输出的请求不响应时强制输出线程堆栈
  • -l:除堆栈信息外,显示关于锁的附加信息
  • -m:显示native方法的堆栈信息

如下图利用jstack分析死锁
image.png
image.png

/**
 * 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,
 */
public class Demo15_DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldThread(lockA,lockB),"Thread-AAA").start();
        new Thread(new HoldThread(lockB,lockA),"Thread-BBB").start();

        /**
         * linux ps -ef|grep xxxx
         * windows下的java运行程序也有类似ps的查看进程的命令,但是目前我们需要查看的
         */
    }
}

class HoldThread implements Runnable {

    private String lockA;
    private String lockB;

    public HoldThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得:" + lockA);
            }
        }
    }
}

5.2 可视化工具

5.2.1 jconsole

运行:
image.png
或双击
image.png
打开jconsole
image.png
可以选择本地连接与远程连接,这里连接刚刚分析的产生死锁的进程,界面如下,可以查看进程概览、各个区内存使用情况、线程分析等
image.png
image.png

这里利用jconsole分析死锁:
image.png
可以点击线程窗口下面的检测死锁直接找到产生死锁的线程
image.png

#参考(推荐)书目
垃圾回收的算法与实现
深入理解Java虚拟机

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

非理性地界生物

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值