做网站需要域名嘉兴seo优化
一、前言
GC算法可以说是各大互联网公司面试必问的大项,为什么面试官这么喜欢问这类问题呢?
GC全称是Garbage Collection,意指垃圾回收,Java语言有一个显著的特点是由JVM虚拟机来管理内存,与C++直接由开发者管理内存迥然不同。
如果开发者新建了变量,开辟了内存空间进行存储之后,JVM会贴心地为Java开发者进行打扫,从而避免了c++开发者那样,需要手动编写代码来清理内存。
但是,如果在高并发的场景下,Java开发者如果遇到线上的java.lang.OutOfMemoryError: Java heap space,该如何排除原因?会否想到:
不经考虑的大对象分配
在代码中触发内存泄露导致程序无法找到一块可容纳新对象的内存空间
闯过一关后,接下来,该如何解决或优化?
所以,遇到这一系列的连珠炮,我们还是需要静下来,一步步理清一下思路。别紧张,我们的专栏就是为梳理而生。
下面将为大家细致地研究一下GC的算法。
二、GC的触发点与考量算法
GC会发生在什么内存区域呢?
一般来说,程序计数器,虚拟机栈,本地方法 栈都是跟从线程生灭,GC主要发生在方法区与堆空间中。这一点很重要,让我们聚焦于特定的内存区域来分析GC问题。
那怎样去决定特定区域的对象是否能够GC呢?
这里就会需要有算法来判断。一般来说,是引用计数法与可达性分析。JVM使用的是可达性分析法。
可达性分析
在JVM中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。基本思路是通过被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
下面用个图例来解析有关可达性分析。
由图示可得,对象3与对象4是从GC Roots是不可到达的,,说明其可以被回收。而对象1与对象2则不可回收。在Java中,可作为GC Root的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
引用计数法
引用计数法是指,为每个对象加一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时则代表该对象可以回收。
以图解与代码演示。
/** * @author felix.ou * @since 2020-4-28 * VMOptions: -XX:+PrintGCDetails */public class GcObject { public Object instance = null; public static void testGC() { // Step1 此处等于建立虚拟机栈的引用入口到堆的GcObject的实例1中 GcObject object1 = new GcObject(); // Step2 此处等于建立虚拟机栈的引用入口到堆的GcObject的实例2中 GcObject object2 = new GcObject(); // 下面先相互引用 // Step3 object1引用2, 引用数+1 object1.instance = object2; // Step4 object2引用1, 引用数+1 object2.instance = object1; // 下面将消除实例与引用入口的关联,如果是单实例,则引用数变为0 // 但由于上面有相互引用,引用数仍相互为1 // Step5, object1 = null; // Step6 object2 = null; // 从分析中可以看出,其实Java虚拟机不会使用引用计数法,因为其仍然进行了GC System.gc(); } public static void main(String[] args) { testGC(); }}
由于引用计数法存在这样的问题,主流的JVM虚拟机并没有采用该方式,但Python等会采用。
因为引用计数法效率高,显著缺点就是无法避免上述的循环引用的问题。
三、最终判定
那如何确认一个对象最终会回收?
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,可达性分析后不可达的对象暂时处于“死缓”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法。虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
当对象没有覆盖finalize方法。
finalize方法已经被虚拟机调用过。
2. 第二次标记
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环,将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
四、引用
上面反复提及引用,如何理解Java的引用?
在JDK1.2之后,Java对引用的概念进行了扩充,可以将引用分为以下四类:
强引用(Strong Reference)
强引用指在程序代码中普遍存在的,例如Object obj = new Object()。
只要存在强引用,垃圾收集器永远不会回收被引用的对象。宁愿出现OOM,也不会回收这些对象。
软引用(Soft Reference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference
类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get()); }}
弱引用(Weak Reference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:
使用场景上,WeakHashMap
使用了弱引用,ThreadLocal
类中也是用了弱引用。
下面文段摘自《Java核心技术卷1》,可以用于解释WeakHashMap的使用缘由。
设计
WeakHashMap
类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再 使用了, 将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引 用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。为什么垃圾回收器不能够删除它呢?难道删除无用的对象不是垃圾回 收器的工作吗?
遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射对象是活动的, 其中的所有桶也是活动的, 它们不能被回收。因此,需要由程序负责从长期存活的映射表中 删除那些无用的值。或者使用
WeakHashMap
完成这件事情。当对键的唯一引用来自散列条 目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。
下面是这种机制的内部运行情况。
WeakHashMap
使用弱引用(weak references
) 保存键。WeakReference
对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的 对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象 已经没有他人引用了,就将其回收。然而, 如果某个对象只能由WeakReference
引用, 垃圾 回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。WeakHashMap
将周期性地检 查队列, 以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用, 并 且已经被收集起来。于是,WeakHashMap
将删除对应的条目。
import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReference<String> sr = new WeakReference<String>(new String("hello")); System.out.println(sr.get()); System.gc(); //通知JVM的gc进行垃圾回收 System.out.println(sr.get()); }}
虚引用(Phantom Reference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference
类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用对象被回收之前,会被放入ReferenceQueue
中。其它引用是被JVM回收后才被传入ReferenceQueue
中。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。
虚引用创建的时候,必须带有ReferenceQueue
。可参见其构造函数,其必须和一个引用队列一起存在。get()方法永远返回null,因为虚引用永远不可达。
public class PhantomReference<T> extends Reference<T> { /** * Returns this reference object's referent. Because the referent of a * phantom reference is always inaccessible, this method always returns * null
. * * @return null
*/ public T get() { return null; } /** * Creates a new phantom reference that refers to the given object and * is registered with the given queue. * *
It is possible to create a phantom reference with a null
* queue, but such a reference is completely useless: Its get * method will always return null and, since it does not have a queue, it * will never be enqueued. * * @param referent the object the new phantom reference will refer to * @param q the queue with which the reference is to be registered, * or null if registration is not required */ public PhantomReference(T referent, ReferenceQueue super T> q) { super(referent, q); }}
使用例子:
import java.lang.ref.PhantomReference;import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue); System.out.println(pr.get()); }}
五、总结
本文重点在于:
垃圾回收的理解
垃圾回收触发的时机
需要做回收的对象
做出最终判定
Java引用的详解