Java 世界里的垃圾回收规则你搞懂了吗?,springboot 输出视频流
1960 年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,而这时 Java 还没有出世呢!所以实际上 GC 并不是 Java 的专利,GC 的历史远远大于 Java 的历史!
怎么定义垃圾
既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。
String m = new String("jack");
先创建一个字符串,这时候"jack"有一个引用,就是 m。
然后将 m 设置为 null,这时候"jack"的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了。
m = null;
引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
看似很美好,但我们知道 JVM 的垃圾回收就是"Stop-The-World"的,那是什么原因导致我们最终放弃了引用计数算法呢?看下面的例子。
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name){}
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
1. 定义 2 个对象
2. 相互引用
3. 置空各自的声明引用
我们可以看到,最后这 2 个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为 0,通过引用计数算法,也就永远无法通知 GC 收集器回收它们。
可达性分析算法
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。
Java 内存区域
在 Java 语言中,可作为 GC Root 的对象包括以下 4 种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
此时的 s,即为 GC Root,当 s 置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。
public class StackLocalParameter {
public StackLocalParameter(String name){}
}
public static void testGC(){
StackLocalParameter s = new StackLocalParameter("localParameter");
s = null;
}
方法区中类静态属性引用的对象
s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 proper
ties 对象由于无法与 GC Root 建立关系被回收。
而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。
public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name){}
}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}
方法区中常量引用的对象
m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。
public class MethodAreaStaicProperties {
public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
public MethodAreaStaicProperties(String name){}
}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
s = null;
}
本地方法栈中引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
怎么回收垃圾
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于 Java 虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想。
===========================================================================================================================================================================================================
=======================================================================
=============================================================================================================================================================================================
标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为 2 部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。
=============================================================================================================================================================================
这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。
================================================================================================
上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M 的内存区域,其中有 2 个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。
=================================================================================================================================================================================================
==================================================================
=============================================================================================================================================================================================
复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。
上面的图很清楚,也很明显的暴露了另一个问题,合着我这 140 平的大三房,只能当 70 平米的小两房来使?代价实在太高。
标记整理算法
评论