首页 > 技术文章 > 【六】垃圾回收

guchunchao 2019-03-09 16:01 原文

一、如何判定对象为垃圾对象?

1. 引用计数法

   在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效时,计数器的值就-1。(但是目前GC没有用这种算法的)

 

判定是否有被回收,需要打印垃圾回收的日志信息。 

 

package com.everjiankang.gc;
public class Test {

    Object instance;

    public Test() {
        byte[] m = new byte[20 * 1024 * 1024];
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = new Test();

        t1.instance = t2;
        t2.instance = t1;

        t1 = null;
        t2 = null;
        System.out.println("hello world");
        System.gc(); //手动进行垃圾回收
    }
}

 

虚拟机参数:-verbose:gc -XX:+PrintGCDetails

/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -verbose:gc -XX:+PrintGCDetails 。。。。。。
hello world            //由下面红色数值可见,确实是被回收了,而若是使用引用计数法的话是不可能被回收的,因为t1和t2对象之间依然有引用,由此证明不是使用引用计数法
[GC (System.gc()) [PSYoungGen: 23142K->512K(38400K)] 43622K->21000K(125952K), 0.0011692 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 512K->0K(38400K)] [ParOldGen: 20488K->437K(87552K)] 21000K->437K(125952K), [Metaspace: 3307K->3307K(1056768K)], 0.0055045 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
 ParOldGen       total 87552K, used 437K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x000000074006d6e8,0x0000000745580000)
 Metaspace       used 3313K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 365K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

  

2. 可达性分析法

顺着栈中的GCRoot开始找,找不到的就被回收

 

哪些对方可以作为GCRoot进行向下查找呢?

  1、虚拟机栈(栈帧中的局部变量表)

  2、方法区的类属性所引用的对象

  3、方法区中的常量所引用的对象

  4、本地方法栈中引用的对象

 

现在主流的垃圾回收器所采用的判定垃圾对象的算法基本上都是可达性分析法。

将对象的引用置为空,

  

二、如何回收?

1. 回收策略

  1)标记-清除法(效率不高)

    标记:由可达性分析法 查找到的对象被标记

    红色:正在使用的

    黄色:被标记的可以清除的 图二被清除掉了

      

    带来的问题:

      (1)空间问题   : 内存区域会产生越来越多的不连续的内存空间

      (2)效率问题  : 分配一个大对象的时候,由于不连续空间太多,寻址的过程效率低下。若是一直都找不到,会再次触发垃圾回收动作,然后再寻址。

      

 

  2)复制算法(新生代)

    用以解决标记-清除算法的效率问题

    

 

原理:

  复制算法将内存分为2块区域,只在其中一块区域存储,另一块区域不用。图中红色的代表被标记要被回收。当回收时,会将所有没有标记回收依然要使用的对象,复制到另一块未使用的区域并按顺序排列,这样可以保证未使用的区域是连续空间。待将所有尚使用的对象复制完成后,再将之前的区域清空。

然后循环这个过程。

 

带来的问题:

  内存区域只使用了一半,造成了内存区域极大的浪费。

解决办法:

  将内存分成了3块区域

  

    (1)新生代

      1))Eden区

      2)Survivor存活区

      3)Tenured Gen

    (2) 老年代

   新生代内存的垃圾回收

  新生成的对象都会扔到Eden区,Eden区满了后会还会用其中的一个Suvivor区S0,垃圾回收发生时,Eden区的一般都会清空,存活的对象大概在10%左右,就都扔到S1区了,并顺序排列。且清掉s0和Eden区。下次创建对象的时候继续往Eden区存,当Eden区的内存达到一定阈值时,垃圾回收又开始工作了,会把Eden存活的对象继续往S1区域放,并且检测S1里的对象是否依然存活。如果都存活就可能进入Tenured Gen 区域。然后将S1区的对象在移到S0区。这样在使用的过程中,内存并不会浪费太多。仅仅是浪费了10%,这是可接受的。

  如果Eden区经过垃圾回收,存活的对象占内存10%+,那么Survivor区域就放不下了,强行放会产生内存溢出,这时候怎么办呢?这时候就需要内存担保。就跟银行贷款一样,有个中间担保人,你若换不上,就找担保人要。 解决办法是扔到老年代里去。

 

  3)标记-整理算法(老年代)

    可以理解为:标记-整理-清除算法

 

  上面的算法对新生代内存回收比较高效,但是对老年代内存回收效率则很低,因为老年代内的对象存活几率大, 可能有90%的存活,若是按照上面的方式回收,则每次都需要内存担保,反而会导致过程变慢。

  让没有被定位垃圾对象的对象向内存的一端去移动,被标记为垃圾对象的对象移到另一端,这样就分开了,然后把所有的垃圾对象清除,且没有散列的空间,留下的是连续空间。

  

  4)分代收集算法  =  标记-整理算法 + 复制算法

    不同的代分别用不同的垃圾回收算法,新生代用复制算法,老年代用标记整理算法

  

2.垃圾回收器

  1、Serial(新生代)

    在1.3之前,唯一的一个回收新生代内存的垃圾回收器。最基本,发展最悠久的。单线程。桌面应用。

 

  2、Parnew(新生代)

 

多线程收集器在一些比如客户端情况下,性能还是不如Serial的。

 

  3、CMS(老年代)Concurrent Mark Sweep (标记清除)

在jvm1.5的时候,Sun公司发明了一个非常

推荐阅读