对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。
=================================================================
程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而对于 Java 堆和方法区,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。
引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引用的问题。
举个栗子 👉 对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是引用计数算法无法通知 GC 收集器回收它们。
可达性分析法
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
GC Roots 是指:
-
Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
-
本地方法栈中引用的对象
-
方法区中常量引用的对象
-
方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
对于可达性分析中不可达的对象,也并不是没有存活的可能。
判定 finalize() 是否有必要执行
JVM 会判断此对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。
如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
对象重生或死亡
如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。
任何一个对象的 finalize() 方法只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。
方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
-
废弃常量
-
无用的类
判定废弃常量
只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 “bingo” 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 “bingo” 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。
判定无用的类
判定一个类是否是“无用的类”,条件较为苛刻。
-
该类的所有对象都已经被清除
-
加载该类的 ClassLoader 已经被回收
-
该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。
学会了如何判定无效对象、无用类、废弃常量之后,剩余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个:
标记-清除算法
标记的过程是:遍历所有的 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 区。
标记-整理算法(老年代)
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots
,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
分代收集算法
根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。
-
新生代:复制算法
-
老年代:标记-清除算法、标记-整理算法
1、标记清除
优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;
缺点:
2次扫描 一次标记 一次清除
有内存碎片
2、复制算法
优点:
使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;
缺点:
空间浪费;效率随对象存活率升高而变低;
HotSpot虚拟机复制算法过程
-
将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
-
每次使用Eden和其中一块Survivor;
-
当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
-
而后清理掉Eden和使用过的Survivor空间;
-
后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
分配担保机制
如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;
3、标记压缩
优点:不会产生内存碎片;
缺点:增加了对存活对象需要整理的过程,效率更低;
4、分代收集算法
“分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。
新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;
老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;
优点:根据各个年代的特点采用最适当的收集算法;
缺点:仍然不能控制每次垃圾收集的时间;
https://github.com/doocs/jvm/blob/main/docs/04-hotspot-gc.md
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
CMS收集器
老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
CMS收集器3个明显的缺点:
对CPU资源非常敏感&#x