问题1:什么是垃圾回收机制?
在java的虚拟机当中,在我们进行实例化的时候,堆会给我们开辟新的空间存放实例。而由于堆,方法区是线程公有,不会像栈区(线程私有)一样随着线程的销毁而销毁。因此在java虚拟机中必须要有垃圾回收的机制,定时清理内存,防止内存溢出(OutMemory)的情况。
问题2:哪些运行时数据区中的哪些内存需要被GC?
在运行时数据区中,分别存在以下的区域:
虚拟机栈,本地方法栈中的内存会随着线程的销毁而清空,而方法区和堆不会自动情况这是垃圾收集器所关注的部分,因此需要JVM进行GC。
问题3:如何判断哪些内存需要被GC?
1、引用计数算法。
当创建对象实例时候,就会给该变量的实例创建一个变量(计数器),初始值为1。当其他变量用这个对象进行赋值的时候,这个对象的变量就会+1。当这个对象过了生命周期或者赋了新的值后,该计数器就会减1.当计数器的值为0,该对象也就会被回收。
优点:对线程的运行影响不大,而且执行快。
缺点:无法检测循环的引用。如父对象引用子对象,子对象引用父对象。这种情况下计数器不可能为0,也不可能被回收。
/*虚拟机参数:-verbose:gc*/
public classGCDemo {private Object instance = null;public static voidmain(String[] args) {
GCDemo gcDemoParent= newGCDemo();
GCDemo gcDemoChild= newGCDemo();
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 classGCDemo {static GCDemo gcDemo = null;protected voidfinalize() throws Throwable {
gcDemo= this; //给gcDemo加了强引用
System.out.println("执行了finalize");
}public static voidmain(String[] args) throws InterruptedException {
gcDemo= newGCDemo();
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前都进行碎片整理。