程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束以后也会消失,所以不须要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。java
判断一个对象是否可回收算法
给对象添加一个引用计数器,当对象增长一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。数组
两个对象出现循环引用的状况下,此时引用计数器永远不为 0,致使没法对它们进行回收。缓存
public class ReferenceCountingGC {安全
public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objectA = new ReferenceCountingGC(); ReferenceCountingGC objectB = new ReferenceCountingGC(); objectA.instance = objectB; objectB.instance = objectA; }
}
正由于循环引用的存在,所以 Java 虚拟机不适用引用计数算法。多线程
经过 GC Roots 做为起始点进行搜索,可以到达到的对象都是存活的,不可达的对象可被回收。并发
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 通常包含如下内容:框架
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象分布式
不管是经过引用计算算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否可被回收都与引用有关。函数
Java 具备四种强度不一样的引用类型。
(一)强引用
被强引用关联的对象不会被垃圾收集器回收。
使用 new 一个新对象的方式来建立强引用。
Object obj = new Object();
(二)软引用
被软引用关联的对象,只有在内存不够的状况下才会被回收。
使用 SoftReference 类来建立软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
(三)弱引用
被弱引用关联的对象必定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生以前。
使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采起的是分代缓存,常用的对象放入 eden 中,而不经常使用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。
public final class ConcurrentCache<K, V> {
private final int size; private final Map<K, V> eden; private final Map<K, V> longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { V v = this.eden.get(k); if (v == null) { v = this.longterm.get(k); if (v != null) this.eden.put(k, v); } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { this.longterm.putAll(this.eden); this.eden.clear(); } this.eden.put(k, v); }
}
(四)虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用取得一个对象实例。
为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
由于方法区主要存放永久代对象,而永久代对象的回收率比新生代差不少,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件不少,须要知足如下三个条件,而且知足了也不必定会被卸载:
该类全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,也就没法在任何地方经过反射访问该类方法。
能够经过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都须要虚拟机具有类卸载功能,以保证不会出现内存溢出。
finalize() 相似 C++ 的析构函数,用来作关闭外部资源等工做。可是 try-finally 等方式能够作的更好,而且该方法运行代价高昂,不肯定性大,没法保证各个对象的调用顺序,所以最好不要使用。
当一个对象可被回收时,若是须要执行该对象的 finalize() 方法,那么就有可能经过在该方法中让对象从新被引用,从而实现自救。
垃圾收集算法
将须要回收的对象进行标记,而后清理掉被标记的对象。
不足:
标记和清除过程效率都不高;
会产生大量不连续的内存碎片,致使没法给大对象分配内存。
让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块上面,而后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
如今的商业虚拟机都采用这种收集算法来回收新生代,可是并非将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。若是每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时须要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
如今的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不一样块采用适当的收集算法。
通常将 Java 堆分为新生代和老年代。
新生代使用:复制算法
老年代使用:标记 - 清理 或者 标记 - 整理 算法
垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器能够配合使用。
Serial 翻译为串行,能够理解为垃圾收集和用户程序交替执行,这意味着在执行垃圾收集的时候须要停顿用户程序。除了 CMS 和 G1 以外,其它收集器都是以串行的方式执行。CMS 和 G1 可使得垃圾收集和用户程序同时执行,被称为并发执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工做。
它的优势是简单高效,对于单个 CPU 环境来讲,因为没有线程交互的开销,所以拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,由于在用户的桌面应用场景下,分配给虚拟机管理的内存通常来讲不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间能够控制在一百多毫秒之内,只要不是太频繁,这点停顿是能够接受的。
它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能缘由外,主要是由于除了 Serial 收集器,只有它能与 CMS 收集器配合工做。
默认开始的线程数量与 CPU 数量相同,可使用 -XX:ParallelGCThreads 参数来设置线程数。
与 ParNew 同样是并行的多线程收集器。
其它收集器关注点是尽量缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则能够高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,致使吞吐量降低。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不须要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。
是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。若是用在 Server 模式下,它有两大用途:
在 JDK 1.5 以及以前版本(Parallel Old 诞生之前)中与 Parallel Scavenge 收集器搭配使用。
做为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,均可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
特色:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。
分为如下四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,须要停顿。
并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程当中耗时最长,不须要停顿。
从新标记:为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,须要停顿。
并发清除:不须要停顿。
在整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,不须要进行停顿。
具备如下缺点:
吞吐量低:低停顿时间是以牺牲吞吐量为代价的,致使 CPU 利用率不够高。
没法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段因为用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。因为浮动垃圾的存在,所以须要预留出一部份内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。可使用 -XX:CMSInitiatingOccupancyFraction 来改变触发 CMS 收集器工做的内存占用百分,若是这个值设置的太大,致使预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
标记 - 清除算法致使的空间碎片,每每出现老年代空间剩余,但没法找到足够大连续空间来分配当前对象,不得不提早触发一次 Full GC。
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是将来能够替换掉 CMS 收集器。
Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 能够直接对新生代和永久代一块儿回收。
G1 把新生代和老年代划分红多个大小相等的独立区域(Region),新生代和永久代再也不物理隔离。
经过引入 Region 的概念,从而将原来的一整块内存空间划分红多个的小空间,使得每一个小空间能够单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。经过记录每一个 Region 记录垃圾回收时间以及回收所得到的空间(这两个值是经过过去回收的经验得到),并维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region。
每一个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。经过使用 Remembered Set,在作可达性分析的时候就能够避免全堆扫描。
若是不计算维护 Remembered Set 的操做,G1 收集器的运做大体可划分为如下几个步骤:
初始标记
并发标记
最终标记:为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段须要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段须要停顿线程,可是可并行执行。
筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所指望的 GC 停顿是时间来制定回收计划。此阶段其实也能够作到与用户程序一块儿并发执行,可是由于只回收一部分 Region,时间是用户可控制的,并且停顿用户线程将大幅度提升收集效率。
具有以下特色:
空间整合:总体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片断内,消耗在 GC 上的时间不得超过 N 毫秒。
更详细内容请参考:Getting Started with the G1 Garbage Collector
内存分配与回收策略
对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数状况下也可能直接分配在老年代中。
Minor GC:发生在新生代上,由于新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度通常也会比较快。
Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,所以 Full GC 不多执行,并且执行速度会比 Minor GC 慢不少。
(一)对象优先在 Eden 分配
大多数状况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
(二)大对象直接进入老年代
大对象是指须要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
常常出现大对象会提早触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
(三)长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并通过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增长 1 岁,增长到必定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
(四)动态对象年龄断定
虚拟机并非永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,若是在 Survivor 区中相同年龄全部对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象能够直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
(五)空间分配担保
在发生 Minor GC 以前,虚拟机先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是条件成立的话,那么 Minor GC 能够确认是安全的;若是不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否容许担保失败,若是容许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将尝试着进行一次 Minor GC,尽管此次 Minor GC 是有风险的;若是小于,或者 HandlePromotionFailure 设置不容许冒险,那这时也要改成进行一次 Full GC。
对于 Minor GC,其触发条件很是简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有如下条件:
(一)调用 System.gc()
此方法的调用是建议虚拟机进行 Full GC,虽然只是建议而非必定,但不少状况下它会触发 Full GC,从而增长 Full GC 的频率,也即增长了间歇性停顿的次数。所以强烈建议能不使用此方法就不要使用,让虚拟机本身去管理它的内存。可经过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc()。
(二)老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上缘由引发的 Full GC,调优时应尽可能作到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要建立过大的对象及数组。
(三)空间分配担保失败
使用复制算法的 Minor GC 须要老年代的内存空间做担保,若是出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
(四)JDK 1.7 及之前的永久代空间不足
在 JDK 1.7 及之前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的状况下也会执行 Full GC。若是通过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError,为避免以上缘由引发的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
(五)Concurrent Mode Failure
执行 CMS GC 的过程当中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多致使暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
免费Java资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。
传送门: https://mp.weixin.qq.com/s/Jz...