问题1:什么是垃圾回收机制?

    在java的虚拟机当中,在我们进行实例化的时候,堆会给我们开辟新的空间存放实例。而由于堆,方法区是线程公有,不会像栈区(线程私有)一样随着线程的销毁而销毁。因此在java虚拟机中必须要有垃圾回收的机制,定时清理内存,防止内存溢出(OutMemory)的情况。

问题2:哪些运行时数据区中的哪些内存需要被GC?

    在运行时数据区中,分别存在以下的区域:

  

    虚拟机栈,本地方法栈中的内存会随着线程的销毁而清空,而方法区和堆不会自动情况这是垃圾收集器所关注的部分,因此需要JVM进行GC。

问题3:如何判断哪些内存需要被GC?

    1、引用计数算法。

     当创建对象实例时候,就会给该变量的实例创建一个变量(计数器),初始值为1。当其他变量用这个对象进行赋值的时候,这个对象的变量就会+1。当这个对象过了生命周期或者赋了新的值后,该计数器就会减1.当计数器的值为0,该对象也就会被回收。

    优点:对线程的运行影响不大,而且执行快。

    缺点:无法检测循环的引用。如父对象引用子对象,子对象引用父对象。这种情况下计数器不可能为0,也不可能被回收。


/*虚拟机参数:-verbose:gc*/
public
class GCDemo { private Object instance = null; public static void main(String[] args) { GCDemo gcDemoParent = new GCDemo(); GCDemo gcDemoChild = new GCDemo(); gcDemoParent.instance = gcDemoChild; gcDemoChild.instance = gcDemoParent; gcDemoChild = null; gcDemoParent = null; System.gc(); } }

 

  以上的代码可以看出父子互相调用,且执行了System.gc()。

  输出结果:

[Full GC (System.gc())  1216K->560K(15872K), 0.0165474 secs]  

 

    1216K代表执行之前的内存,560代表执行GC后的内存,15872代表虚拟机的内存,最后的代表执行时间。通过输出结果可以得知,该虚拟机不是通过引用计数算法来判断对象是否存活。

    2、可达性算法:

    在我学习的过程中,只知道引用计数算法和可达性算法两种算法判定对象是否存活。

    这个图很好的阐释了可达性算法的算法思路。他就像一颗树,不断的进行引用,从GC Root(根集合)开始,不断的通过引用链进行引用。当有对象没有被引用链的时候,就会出现对象不可达的情况,此时就代表对象是不可用的(ObjD Obje)。

                         

   

  什么可以作为GC Root呢。从网上的资料查找得出,GC的对象包括:

    1、虚拟机栈中的引用对象(栈帧中的本地变量表)。

    2、方法区中类静态引用的对象。

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

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

   

  第一次标记:在对象被发现没有被引用时,会被标记第一次,并不会立刻执行GC。

  第二次标记:在对象被标记后进行筛选,看该对象是否有必要执行finalize()方法(拯救自己机会只有一次,如将自己赋值给其他对象),若在该方法中也没有进行连接,则就要挂了。

   

public class GCDemo {

 static GCDemo gcDemo = null;


    protected void finalize() throws Throwable {
        gcDemo = this;  //给gcDemo加了强引用
        System.out.println("执行了finalize");
    }
    public static void main(String[] args) throws InterruptedException {
       gcDemo = new GCDemo();
       gcDemo = null;   //去掉强引用
       System.gc(); //垃圾回收
        //睡眠一秒,以便于垃圾回收线程清理gcDemo对象。
       Thread.sleep(1000);
       if(null != gcDemo) {
           System.out.println("第一次gc后,alive");
       } else {
           System.out.println("第一次GC后,die");
       }
        gcDemo = null;   //去掉强引用
        System.gc(); //垃圾回收
        //睡眠一秒,以便于垃圾回收线程清理gcDemo对象。
        Thread.sleep(1000);
        if(null != gcDemo) {
            System.out.println("第二次gc后,alive");
        } else {
            System.out.println("第二次GC后,die");
        }
    }
}

 

  以上的代码很好的演示了对象在通过finalize()方法进行自救,以及第二次自救是否成功。

  当然,输出结果为:

执行了finalize
第一次gc后,alive 
第二次GC后,die

 

问题3:java虚拟机中存在哪几种引用?

    1、强引用:如Object ob = new Object()。通过new出来的实例,就是强引用,如果该对象还在引用,就不会被回收。(可达性算法,引用计数算法都是基于强引用)

    2、软引用:有用非必须的对象,JAVA中的SoftReference作为软引用对象。如果内存足够不会被回收,如果内存要溢出时候,就会被回收。如果回收后内存依然溢出,就会出现OutMemory的异常。

    3、弱引用。说白了只能活一天的对象。JAVA中的WeekReference作为弱引用对象。在一次生命周期的结束时,无论内存是否足够,都被回收。

    4、虚引用。最弱的引用关系,JAVA中的PhantomReferene作为虚引用对象。我不知道干嘛的。

 

问题4:方法区的垃圾如何回收?

           方法区中要回收的主要是两类型:

    1、废弃常量。

    如何判断废弃常量呢?以字面量回收为例,如果一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象引用了叫做“abc”的字面量,那么,如果发生垃圾回收并且有必要时,“abc”就会被系统移出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

    2、无用的类。

      如何判断无用的类,满足一下三个条件。

      1、该类的所有实例被回收。堆中没有该类的任何实例。

      2、加载该类的ClassLoader被回收。

      3、该类对于的java.lang.Class对象没有在任何地方被引用,任何地方无法通过放射调用该方法。

  

问题5:常用的垃圾回收算法?

    1、标记-清除算法(Mark-Sweep)

    

  分为两个阶段:如图所示,当GCRoot没有引用到B时候,B被进行了标记,然后被清除。存活的对象不进行移动。

    缺点:会产生内存碎片。会使得大对象无法创建。

    2、复制算法(Copying)

           

 

    他将内存分为两块(a1,a2),每次值使用其中一块(a1),当其中一块(a1)内存不足时候,就会将存活对象复制给另一块(a2),这样a1就变成了由原来的满内存变成空闲内存,a2由空闲内存变成有对象的内存。在下一次进行Copy的算法时候,就会在新的有对象的内存(a2)中进行回收。

    3、标记整理算法(Mark-Compant)

 

          

    过程和标记-清楚算法一样,唯一不同的是会进行对象的移动。解决了内存碎片的问题。

 

  4、分代回收算法。

    

    图可得:将对象的生命周期作为内存划分成若干个不同的区域。新生代(YoungGeneration),年老代(OldGeneration),永久代(PermanentGernation)

      在新生代中:分为了Eden区(好像是夏娃住的地方,伊甸),survivor1区(From space),survivor2区(To space)(8:1:1的比例)。大部分对象在Eden区中生成,在进行Minor GC时候,将Eden区的存活对象转移至Survivor1区中,其他对象进行回收,当survivor1区中的内存满时候,将Eden区和Survivor1区中的存活对象传至survivor2区中,其他对象进行Minor GC。完成后将两个survivor区进行交换,以此往复。

    当survivor2区中的内存不足以放下eden区和survivor1区中对象时候,会讲对象放置在年老代中,若年老代也也放不下了,就执行FullGC,将新生代,年老代的对象进行回收。

    Full GC的效率低,因为对象多,但是频率低,常在年老代中进行,System.gc()会触发该GC。

    Minor GC的效率高,频率高,常在新生代中进行(不一定等Eden区满才执行)。

 

      年老代中:在年轻代中经历了N次的回收,仍然存活的对象,会被放入年老代中,因此可以认为年老代放的对象的生命周期都很长。而内存也比新生代大,约为新生代的2倍。

      永久代中:永久代存放的是静态文件,如java类,方法等。回收的方法在问题4中。

 

  5、垃圾回收器。

    

    上图展示了7种不同的垃圾收集器。如果两个收集器之间有线连起来的话,就代表能搭配使用。

       年轻代中的收集器包括:Serial收集器、ParNew收集器、ParallelScavenge收集器。老年代中的收集器包括:CMS收集器、MSC收集器,Parallel Old收集器。

        1、Serial收集器

                一款年轻代中使用的单线程垃圾收集器。使用的是标记-复制算法。在进行GC时候,其他的用户线程会进行stop the world(STW),会线程的运行,程序会出现卡住的现象。

                如果长时间、频繁的STW,会导致系统的反应迟钝。

                     

      

 

       2、ParNew收集器:

                由图可得,这是Serial收集器的对线程版,同样是标记-复制算法。在很多时候作为年轻代的首选收集器,且和老年代中的CMS收集器搭配使用。

                     但如果运行在单核的机器中,他的性能不会比Serial线程好,相反会更差。 PareNew收集器默认开启的垃圾回收线程和当前机器的CPU数量一样,为了控制GC线程的                                                                       数量,我们可以通过-XX:+ParallelGCThreads来控制垃圾收集的线程。

                   

                  3、ParallelScavenge收集器:

                  同样是一款年轻代的垃圾收集器,同样是多线程操作,同样是标记-复制算法。但是却和ParNew收集器有很大的不同,ParallelScavenge收集器更注重于缩短回收的时间,关注如何控制系统的

                  吞吐量(CPU用于运行应用程序和CPU总时间的对比),吞吐量=应用程序运行时间/(应用程序运行时间+GC时间)。

      

 

      总结:年轻代中分为三种不同的收集器,Serial收集器,ParNew收集器, ParallelScavenge收集器,采用的算法都是标记-复制算法,但是性能会在不同数量的CPU下会有所不同。

      4、Serial Old收集器:

         由图可得,他是老年版本的Serial收集器,一款单线程收集器,但使用的是标记-整理算法。

                      

 

 

     5、Parallel Old收集器:

        一款老年版本的Parallel Scavenge的收集器,使用标记-整理算法。工作原理和Parallel Scavenge相似,都是关注吞吐量。如果搭配这两款收集器,将可以实现JAVA对吞吐量优先的收集策略。

      

    6、CMS收集器:

        由图可得,这是一款在老年代中采用标记-清除算法的一款多线程收集器。

      

 

         CMS收集器一共分成四个阶段:

          1、初始标记,在线程到达safepoint时候,会进行第一阶段,因为这里是单线程的操作,因此会发生STW,但是速度很快,基本不会影响线程的运行。

          2、并发标记,这个阶段是一个并发阶段,标记过程也比较耗时,但也不影响系统的运作。

          3、重新标记,由图可知这是一款多线程的标记工作,但是同时也会STW,重新标记的耗时会比初始标记场,但是远小于并发标记。

          4、并发清理,和并发标记一样,会相对耗时,但不影响系统运作。

 

       CMS收集器实现了低延迟并发收集工作,但是也会有不足。

CMS默认开启的垃圾回收线程数量是(CPU+3)/4,随着CPU的增加,垃圾回收线程占用CPU的资源会减少,但是如果CPU少于4个的时候,垃圾回收的占比就好愈来愈大。比如目前CPU有2个,回收的线程占比将会大于50%。因此在在采取该收集器,应当考虑系统是否依赖CPU。

其次,在并发标记的过程中会产生浮动垃圾,由于在标记的过程中产生了垃圾,而CMS也无法进行标记,可能会导致老年代发送MajorGC(Full GC).。在JDK5中,当老年代到达68%的内存时候,就好激发MajorGC,而我们可以通过-XX:CMSInitiatingOccupancyFraction进行控制。

同时,CMS收集器因为算法的问题,在垃圾回收时会产生内存碎片,因此提供了-XX:+UseCMSCompactAtFullCollection参数在有必要时用于压缩处理,但因为该操作是单线程,所以会引起STW。虚拟机还提供了一个"-XX:CMSFullGCsBeforeCompaction"参数,来控制进行过多少次不压缩的Full GC以后,进行一次带压缩的Full GC,默认值是0,表示每次在进行Full GC前都进行碎片整理。