JVM垃圾收集器

Java 与 C++ 之间有一堵由内存分配和垃圾收集技术所围成的墙,墙外面的人想进去,墙里面的人却想出来. —-《深入理解Java虚拟机》

我们思考三件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

    在java内存运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。所以这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,方法结束或线程结束时,内存自然的就跟随着回收了。

    而java堆和方法区不一样,只有在程序运行时才知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器关注的是这部分内存。

    垃圾回收主要在堆和方法区

对象已死?

引用计数算法(Reference Counting):

很多教科书判断对象是否存活:给对象添加一个引用计数器,当有一个地方引用它时,计数器加1;当引用失效时,计数器值减1;任何时候计数器都为0的对象就是不能再被使用的。
但是 java 语言没有使用引用计数法来管理内存,主要是因为很难解决对象之间的相互循环引用的问题。

objA = objB;
objB = objA;

这两个对象始终互相引用,导致引用计数不为0,无法回收。

根搜索算法

java使用根搜索算法(GC Roots Tracing)判断对象是否存活。
通过一系列的名为“GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为搜索链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连(用图论的话说是从GC Roots 到这个对象不可达)时,证明此对象是不可用的。
java中可以作为GC Roots 的有:

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

再谈引用

JDK1.2之前Java中的引用定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。
这种定义下对象只有被引用和没有被引用两种状态。
JDK1.2之后,Java对引用概念进行了扩充,将引用分为强引用(Strong Reference), 软引用(Soft Reference) ,弱引用(Weak Reference),虚引用(Phantom Reference),引用强度依次减弱。

  • 强引用:在程序代码中普遍存在的,类似Object obj = new Object() 这种,只要强引用还在,垃圾回收永远不会回收掉被引用的对象。
  • 软引用:描述一些还有用,但并非必须的对象。在Java中用java.lang.ref.SoftReference类来表示。对于软引用的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。因此,这一点可以很好地用来解决OOM的问题,这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
  • 弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  • 虚引用:一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。
    这里写图片描述

生存还是死亡?

在根搜索算法中不可达的对象,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize()方法进行自救,自救过程只需要重新与引用链上的任何一个对象建立连接即可,譬如把自己(this关键字)赋值给某个类变量或对象的成员变量。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

public class TestGC {

    public static TestGC testgc = null;
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("执行finalize()");
        testgc= this;
    }
    public static void main(String[] args) throws Exception {
        testgc = new TestGC();
        testgc = null;
        System.gc();
        //因为finalize方法优先级很低,暂停0.5秒,等待finalize执行
        Thread.sleep(500);
        if(testgc!=null){
            System.out.println("自救成功");
        }else{
            System.out.println("死了");
        }
        //和上面代码完全相同,但自救失败
        testgc = null;
        System.gc();
        Thread.sleep(500);
        if(testgc!=null){
            System.out.println("自救成功");
        }else{
            System.out.println("死了");
        }
    }
}

结果:

执行finalize()
自救成功
死了

需要注意:任何一个对象的finalize()方法都只会被系统自动调用一次。

垃圾收集算法

标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。标记和清除过程的效率都不高,标记清除后会产生大量不连续的内存碎片。

复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,将还存活的对象复制到另一块上面,然后把已用过的内存空间一次清理掉。这样每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片问题。但是这样的代价是将内存缩小为原来的一半。
大多数虚拟机都采用这种算法来回收新生代,新生代的对象98%是朝生夕死,所以不需要按照1:1的比例来划分内存空间,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用Eden和其中的一块Survivor。回收时,将Eden和刚才用过的Survivor中还存活的对象一次性拷贝到另一块Survivor上,最后清理掉Eden和刚才使用过的Survivor。Eden 和Survivor默认大小比例是8:1。

标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变得很低。所以老年代一般不能选用这种算法。
根据老年代的特点,提出“标记-整理”,首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

把java分为新生代和老年代,在新生代,每次垃圾回收都会有大量对象死去,只有少量存活,用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用“标记-清理” 或者“标记-整理”。

垃圾收集器

Serial收集器

针对新生代;
采用复制算法; 单线程收集;
进行垃圾收集时,必须暂停所有工作线程,直到完成;
即会”Stop The World”;

ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。
除了多线程外,其余的行为、特点和Serial收集器一样;
如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;
两个收集器共用了不少代码;

Serial Old收集器

Serial Old是 Serial收集器的老年代版本;
针对老年代;
采用”标记-整理”算法(还有压缩,Mark-Sweep-Compact);
单线程收集;

并发垃圾收集和并行垃圾收集的区别

并行(Parallel)

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;

并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);

Minor GC和Full GC的区别

Minor GC

又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

Full GC

又称Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;

吞吐量与收集器关注点说明

  (A)、吞吐量(Throughput)

          CPU用于运行用户代码的时间与CPU总消耗时间的比值;

          即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);    

          高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

    (B)、垃圾收集器期望的目标(关注点)

        (1)、停顿时间    

              停顿时间越短就适合需要与用户交互的程序;

              良好的响应速度能提升用户体验;

        (2)、吞吐量

              高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;

              主要适合在后台计算而不需要太多交互的任务;

        (3)、覆盖区(Footprint)

              在达到前面两个目标的情况下,尽量减少堆的内存空间;

              可以获得更好的空间局部性;

猜你喜欢

转载自blog.csdn.net/xiao_ma_csdn/article/details/80111608