垃圾收集策略与算法
垃圾收集 Garbage Collection 一般被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,通过半个多世纪,目前已经十分红熟了。 在jvm 中,程序计数器、java虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出作入栈和出栈操做,实现了自动的内存清理,所以,咱们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部份内存的分配和使用都是动态的.java
程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具备肯定性,在这几个区域内不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。:而对于 Java 堆和方法区,咱们只有在程序运行期间才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,垃圾收集器所关注的正是这部份内存。算法
1.断定对象是否存活
若一个对象不被任何对象或变量引用,那么它就是无效对象,须要被回收。缓存
引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。安全
引用计数算法的实现简单,断定效率也很高,在大部分状况下它都是一个不错的算法。可是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是由于它很难解决对象之间循环引用的问题。jvm
举个栗子👉对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 而且 objB.instance = objA,因为它们互相引用着对方,致使它们的引用计数都不为 0,因而引用计数算法没法通知 GC 收集器回收它们。this
可达性分析法
全部和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。spa
GC Roots 是指:线程
- Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。设计
2.引用的种类
断定对象是否存活与“引用”有关。在 JDK 1.2 之前,Java 中的引用定义很传统,一个对象只有被引用或者没有被引用两种状态,咱们但愿能描述这一类对象:当内存空间还足够时,则保留在内存中;若是内存空间在进行垃圾手收集后仍是很是紧张,则能够抛弃这些对象。不少系统的缓存功能都符合这样的应用场景。code
在 JDK 1.2 以后,Java 对引用的概念进行了扩充,将引用分为了如下四种。不一样的引用类型,主要体现的是对象不一样的可达性状态reachable
和垃圾收集的影响。
强引用(Strong Reference)
相似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。可是,若是咱们错误地保持了强引用,好比:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。
咱们使用的大部分的引用都是强引用,这是使用最广泛的引用。若是一个对象具备强引用,那就相似于必不可少的生活用品,垃圾回收器毫不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足问题。
强引用就是指在程序代码之中广泛存在的,好比下面这段代码中的object和str都是强引用: Object object = new Object(); String str = "hello";
软引用(Soft Reference)
软引用是一种相对强引用弱化一些的引用,可让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 以前,清理软引用指向的对象。软引用一般用来实现内存敏感的缓存,若是还有空闲内存,就能够暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(Weak Reference)
弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,不管内存是否充足,都会回收只被弱引用关联的对象。
虚引用(Phantom Reference)
虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 之后,作某些事情的机制,好比,一般用来作所谓的 Post-Mortem 清理机制。
"虚引用"顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收。
特别注意,在程序设计中通常不多使用弱引用与虚引用,使用软引用的状况较多,这是由于软引用能够加速JVM对垃圾内存的回收速度,能够维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
3.回收堆中无效对象
Java中负责内存回收的是JVM
对于可达性分析中不可达的对象,也并非没有存活的可能。
断定 finalize() 是否有必要执行
JVM 会判断此对象是否有必要执行 finalize() 方法,若是对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。
若是对象被断定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保全部的 finalize() 方法都会执行结束。若是 finalize() 方法出现耗时操做,虚拟机就直接中止指向该方法,将对象清除。
对象重生或死亡
若是在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。若是没有,那么就会被垃圾收集器清除。
任何一个对象的 finalize() 方法只会被系统自动调用一次,若是对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。
finalize() 是Object中的方法,当垃圾回收器将要回收对象所占内存以前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象能够趁这个时机挣脱死亡的命运)。
4.回收方法区内存
方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少许的垃圾被清除。方法区中主要清除两种垃圾:
- 废弃常量
- 无用的类
断定废弃常量
只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。好比,一个字符串 "bingo" 进入了常量池,可是当前系统没有任何一个 String 对象引用常量池中的 "bingo" 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。
断定无用的类
断定一个类是不是“无用的类”,条件较为苛刻。
- 该类的全部对象都已经被清除
- 加载该类的 ClassLoader 已经被回收
- 该类的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个表明该类的对象:java.lang.Class。这个对象在类被加载进方法区时建立,在方法区该类被删除时清除。
5.垃圾收集算法
学会了如何断定无效对象、无用类、废弃常量以后,剩余工做就是回收这些垃圾。常见的垃圾收集算法有如下几个:
标记-清除算法
- 标记的过程:遍历全部的
GC Roots
,而后将全部GC Roots
可达的对象标记为存活的对象。 - 清除的过程:将遍历堆中全部的对象,将没有标记的对象所有清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。
这种方法有两个不足:
- 效率问题:标记和清除两个过程的效率都不高。
- 空间问题:标记清除以后会产生大量不连续的内存碎片,碎片太多可能致使之后须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
标记-整理算法(老年代)
- 标记:它的第一个阶段与标记/清除算法是如出一辙的,均是遍历
GC Roots
,而后将存活的对象标记。 - 整理:移动全部存活的对象,且按照内存地址次序依次排列,而后将末端内存地址之后的内存所有回收。所以,第二阶段才称为整理阶段。
这是一种老年代的垃圾收集算法。老年代的对象通常寿命比较长,所以每次垃圾回收会有大量对象存活,若是采用复制算法,每次须要复制大量存活的对象,效率很低。
复制算法(新生代)
为了解决效率问题,“复制”收集算法出现了。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,须要进行垃圾收集时,就将存活者的对象复制到另外一块上面,而后将第一块内存所有清除。
- 优势:不会有内存碎片的问题。
- 缺点:内存缩小为原来的一半,浪费空间。
为了解决空间利用率问题,能够将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。
可是咱们没法保证每次回收都只有很少于 10% 的对象存活,当 Survivor 空间不够,须要依赖其余内存(指老年代)进行分配担保。
分配担保
为对象分配内存空间时,若是 Eden+Survivor 中空闲区域没法装下该对象,会触发 MinorGC 进行垃圾收集。但若是 Minor GC 事后依然有超过 10% 的对象存活,这样存活的对象直接经过分配担保机制进入老年代,而后再将新对象存入 Eden 区。
增量算法
增量算法的基本思想是,若是一次性将全部的垃圾进行处理,须要形成系统长时间的停顿,那么就可让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。可是,由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低。
分代收集算法
根据对象存活周期的不一样,将内存划分为几块。通常是把 Java 堆分为新生代和老年代,针对各个年代的特色采用最适当的收集算法。
- 新生代:复制算法
- 老年代:标记-清除算法、标记-整理算法