文章目录
垃圾回收 GC
垃圾回收 : Garbage Collection(GC)
定义什么是垃圾?
java进程运行,如果某个类型(方法区中的类信息,堆中的类对象),常量(常量池),对象(堆),如果失去了应用,就被称为垃圾
什么是垃圾回收?
java线程启动之后,GC垃圾回收的守护线程会回收以上垃圾。
垃圾回收的时机
第一种:创建对象时,如果在对应的内存区域分配内存空间不足,就触发该区域的GC。
回收的内存区域 :
方法区 : 频率低,回收条件苛刻
1.7叫做方法区,1.8叫做元空间
在GC层面叫做永久代
堆 (GC堆):频率高。
**第二种:**System.gc()---->建议jvm进行垃圾回收,但是不保证一定执行。
如何判断什么是垃圾?
引用计数法
给每一个对象增加一个引用计数器,有一个地方引用的话,计数器就+1。引用失效的话,计数器就-1。当计数器为0的时候,就代表这个对象死了。
a->b , b->a 此时无法进行内存回收。无法解决循环引用问题。
可达性分析
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的
可以作为GC Roots的对象包含下面这几种
-
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线 程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
-
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变 量。 ·在方法区中常量引用的对象,譬如字符串常量池(String Table) 里的引用。
-
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
-
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些 常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还 有系统类加载器。
-
所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地 代码缓存等。
引用类型
- 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
- 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回
收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。 - 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
- 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
finalize()方法
- 效率比较低
- 手动调用方法,不会造成对象的死亡。
finalize()方法的作用
即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程 : 如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。
package JVM;
public class GCfinalize {
public static GCfinalize gc;
public void isAlive(){
System.out.println("I an alive :");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
gc = this;
}
public static void main(String[] args) throws InterruptedException {
gc = new GCfinalize();
gc = null;
System.gc();
Thread.sleep(500);
if(gc != null){
gc.isAlive();
}else {
System.out.println("no,i an dead:");
}
gc = null;
System.gc();
Thread.sleep(500);
if (gc != null) {
gc.isAlive();
}else {
System.out.println("no,I am dead : ");
}
}
}
/**
finalize method executed
I an alive :
no,I am dead :
*/
从代码中我们可以看出:finalize方法确实被JVM触发了,并且对象在回收之前已经逃脱。
但是从输出结果中我们发现,两个完全一样的代码片段,却只有第一次成功逃脱,是因为一个对象的finalize()方法只会被系统自动调用一,如果相同的对象逃脱一次后再次面临一次回收,它的finalize()方法不会被执行。
回收方法区(永久代)
- JDK1.7的方法区在GC中一般称为永久代(Permanent Generation)。
- JDK1.8的元空间存在于本地内存,GC也是即对元空间垃圾回收。
- 永久代或元空间的垃圾收集主要回收两部分内容:废弃常量和无用的类。此区域进行垃圾收集的“性价比”一般比较低。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :
- 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
同时满足三个条件JVM就可以进行回收,但是只是可以,而不是必然。
堆的垃圾回收算法
-
JVM使用垃圾回收线程来进行垃圾回收工作
-
基于垃圾回收器执行,垃圾回收工作,回收不可达对象。
内存回收区域划分
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:
-
新生代(Young Generation):又可以分为Eden空间、From Survivor空间、To Survivor空间
- 新生代的垃圾回收又称为Young GC(YGC)、Minor GC。
- 指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
-
老年代(Old Generation、Tenured Generation)
- 老年代垃圾回收又称为Major GC。
- 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
- Major GC的速度一般会比Minor GC慢10倍以上。
-
Full GC:
- 在不同的语义条件下,对Full GC的定义也不同,有时候指老年代的垃圾回收,有时候指全堆(新生代+老年代)的垃圾回收,还可能指有用户线程暂停(Stop-The-World)的垃圾回收(如GC日志中)。
标记清除算法(老年代的回收算法)
流程
分为标记和清除两个阶段
标记:标记不可达对象
清除,统一清理掉被标记掉的对象
不足:
- 效率问题:标记和清除这两个过程效率都不高。(因为标记要遍历整个内存空间,清除也要遍历所有标记的内存块)
- 空间问题:标记清除会产生大量不连续的内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时候,因为无法找到足够连续的内存而不得不提前触发另一次垃圾收集。
复制算法(新生代回收算法)
流程:
"复制"算法是为了解决"标记-清理"的效率问题和内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :
好处:
效率高(新生代对象朝生夕死,存活的对象不多,复制的也很快,清除也很快)
不足:
空间利用率不高,只有50%;
标记整理算法(老年代回收算法)
流程:
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
好处:
- 存活对象比较多的时候,移动效率比较高
- 没有内存碎片的问题
分代收集算法
在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
现在的商用虚拟机(包括HotSpot都是采用这种收集算法来回收新生代)新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,
HotSpot默认Eden与Survivor的大小比例是8 : 1,(可以在JVM参数中进行设置)也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot实现的复制算法流程如下:
- 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
- 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
- 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
垃圾回收的过程
- Eden空间不足,触发Minor GC:用户线程创建的对象优先分配在Eden区,当Eden区空间不够时,会触发Minor GC:将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
- 垃圾回收结束后,用户再次在Eden区分配用户线程,当Eden区空间不足是,重复上次的步骤进行Minor GC.
- 当有对象在上述过程中重复存活15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15)的时候,将会将这个对象晋升到老年代
- 当Survivor空间不足,存活对象通过分配担保机制进行老年代
- 当老年代空间不足,将会触发Major GC:当创建大对象(需要大量连续空间的java对象)或者新生代长期存活的对象进入老年代时候,老年代空间不足,就要对老年代进行垃圾回收,也就是触发Major GC