JAVA GC(Garbage Collection,垃圾回收)机制,里面包含的知识点很多,面试中也会经常问道。jvm 虚拟机,回收算法,引用,新生代老年代,什么时候回收,那些需要回收,垃圾回收器和 GC 日志等等。
下面我们来一一介绍研究一下,以后去面试就不怕啦。
对象存活还是已经死去
如何判断那些资源需要回收
引用计数
给对象中添加一个引用计数器,每当一个地方引用它时,计数器就加1。当引用失效时就减1。技术器如果是0,就表示对象不再被使用,可以回收了。
但是,引用计数有个缺点:
public class ReferenceCountingGC{
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
以上是对象之间循环引用,实际上这两个对象都不可能在被访问,但是互相引用对方,导致引用计数器都不是0。
运行之后,通过 GC 日志可以看出来,虚拟机并没有因为这两个对象互相引用就不回收他们,说明虚拟机比不是用技术法来判断对象是否存活。
可达性分析
这个算法基本思路是通过一系列成为 “GC Root” 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Root 没有任何引用链相连,则此对象就可以回收了。
那么那些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC Roots:
1.虚拟机栈(栈桢中的本地变量表)中的引用的对象;
2.方法区中的类静态属性引用的对象;
3.方法区中的常量引用的对象;
4.本地方法栈中JNI(Native方法)的引用的对象。
使用OopMap记录并枚举根节点
HotSpot首先需要枚举所有的GC Roots根节点,虚拟机栈的空间不大,遍历一次的时间或许可以接受,但是方法区的空间很可能就有数百兆,遍历一次需要很久。更加关键的是,当我们遍历所有GC Roots根节点时,我们需要暂停所有用户线程(stop the world),因为我们需要一个此时此刻的”虚拟机快照”,如果我们不暂停用户线程,那么虚拟机仍处于运行状态,我们无法确保能够正确遍历所有的根节点。所以此时的时间开销过大更是我们不能接受的。
基于这种情况,HotSpot实现了一种叫做OopMap的数据结构,这种数据结构在类加载完成时把对象内的偏移量是什么类型计算出,并且存放下位置,当需要遍历根结点时访问所有OopMap即可。
用安全点Safepoint约束根节点
如果将每个符合GC Roots条件的对象都存放进入OopMap中,那么OopMap也会变得很大,而且其中很多对象很可能会发生一些变化,这些变化使得维护这个映射表很困难。实际上,HotSpot并没有为每一个对象都创建OopMap,只在特定的位置上创建了这些信息,这些位置称为安全点(Safepoints)。
为了保证虚拟机中安全点的个数不算太多也不是太少,主要决定安全点是否被建立的因素是时间。当进行了耗时的操作时,比如方法调用、循环跳转等时会产生安全点。此外,HotSpot虚拟机在安全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。
引用
无论哪种引用计数还是可达性分析,都跟引用分不开。一下介绍四种引用
1.强引用(StrongReference)
强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用:
Object object = new Object();
String str = "hello";
只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。比如下面这段代码:
public class Main {
public static void main(String[] args) {
new Main().fun1();
}
public void fun1() {
Object object = new Object();
Object[] objArr = new Object[1000];
}
}
当运行至Object[] objArr = new Object[1000];这句时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当fun1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的.
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
Object oldValue = elementData[index];
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work
return (E)oldValue;
}
2.软引用(SoftReference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:
import java.lang.ref.SoftReference;
public class Main {
public static void main(String[] args) {
SoftReference sr = new SoftReference(new String("hello"));
System.out.println(sr.get());
}
}
3.弱引用(WeakReference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference sr = new WeakReference(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
4.虚引用(PhantomReference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new String("hello"), queue);
System.out.println(pr.get());
}
}
内存分区
内存主要被分为三块:新生代(Youn Generation)、老年代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。
新生代(Youn Generation):大致分为Eden区和Survivor区(8:1),Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发Minor GC(也称作Youn GC)。
旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。
GC 过程
1. Minor GC
(1) Minor GC过程
假设现在Heap内存大小为20M,其中年轻代为10M,老年代为10M,年轻代中Eden区6M,From区2M,To区2M,新创建的对象首先往Eden区分配,当再次分配一个对象,假设大小为1M,此时Eden区已经没有足够空间来给这个对象分配内存,如图所示:
这时候触发一次Minor GC,把Eden区的存活对象转移到From区,非存活对象进行清理,然后给新创建的对象分配空间,存入Eden区
随着分配对象的增多,Eden区的空间又不足了:
这时候再触发一次Minor GC,清理掉Eden区和S1区的死亡对象,把存活对象转移到S2区,然后再给新对象分配内存:
From区和To区是相对的关系,哪个区中有对象,哪个区就是From区,比如,再进行一次Minor GC,会把存活对象转移到S1区,再为转移之前,S2区是From区,S1区是To区,转移后,S2区中没有存活对象,变为To区,而S1区变为From区:
2. 对象进入老年代的4种情况
(1) 假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代
(2) 大对象直接进入老年代
假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代
(3) 长期存活的对象将进入老年代
此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳,那么将被移动到To区,并且把对象的年龄设置为1,对象没"熬过"一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中
(4) 动态对象年龄判定
还有一种情况,如果在From空间中,相同年龄所有对象的大小总和大于From和To空间总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认):
3. Full GC
如果某个(些)对象(原来在内存中存活的对象或者新创建的对象)由于以上原因需要被移动到老年代中,而老年代中没有足够空间容纳这个(些)对象,那么会触发一次Full GC,Full GC会对整个Heap进行一次GC,如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM抛出OutOfMemoryError
垃圾收集算法和垃圾收集器
内容较多,篇幅有限不赘述了。建议看这片博文,很详细。注意不光要记住每种算法和不同的收集器,还要几种在新生代老年代分别用的是什么。
http://www.importnew.com/23035.html
参考文献
https://www.jianshu.com/p/314272e6d35b
http://www.importnew.com/23035.html
https://www.cnblogs.com/dolphin0520/p/3784171.html
https://blog.csdn.net/anjoyandroid/article/details/78609971