—-慢慢来比较快,虚心学技术—-

什么是JVM垃圾回收?

垃圾回收(Garbage Collection,GC),顾名思义,就是回收被无用垃圾所占用的内存,防止内存泄露,有效地利用可以使用的内存。

回收的是什么?

如上所述,GC所回收的是被无用垃圾占用的内存,我们知道jvm垃圾回收的目标是运行时数据区,那么在jvm的运行时数据区中,什么才是无用的垃圾呢?

首先,jvm的运行时数据区中分线程共享区和线程独享区两大区

线程独享:程序计数器,本地方法栈,虚拟机栈

线程共享:堆,方法区 **

很明显,线程独享的内存区域生命周期随线程创建而创建,随线程销毁而销毁,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题

所以,jvm的GC回收的对象是堆和方法区中的内存。我们知道,java程序中几乎所有的对象实例都存放在堆内存中,而所有的类元信息及常量池等信息都存放在方法区内存,换一种说法就是:
GC回收的是堆内无用对象实例占用的内存以及方法区中无用常量及类元信息所占用的内存。(主要针对堆内存进行回收)

怎么判断哪些是垃圾?

垃圾回收是由垃圾回收器(位于JVM的执行引擎中)执行的,那么在执行回收操作之前,很自然的需要首先判断哪些对象是可回收的,哪些对象暂时不可回收。换句话说,如何判断对象是否“存活”

这就需要用到判断对象是否存活的算法

一、引用计数法算法(Reference Counting)

算法**这种算法是垃圾收集器的早期策略,这种算法中,每个对象实例都一个引用计数**。当一个对象被创建时就被赋予给了一个变量,该对象实例(假设为A)被分配一个计数变量,此时值为1(被一个变量所引用)。每当有其他变量被赋值为A的引用时,A的引用计数+1;当被赋值的其他变量超过了生命周期或被赋予新值,则A的引用计数-1。任何对象实例的引用计数为0时,都可以被当作垃圾回收。

  1. public class ReferenceCounting {
  2. public static void main(String[] args) {
  3. HeapTest heapTest = new HeapTest(); //对象实例Anew HeapTest())的引用计数:1
  4. HeapTest heapTest1 = heapTest; //对象实例A的引用计数+12
  5. heapTest1 = null; //对象实例A的引用计数-11
  6. heapTest = null; //对象实例A的引用计数-10
  7. }
  8. }

优点

实用性 无需等到堆内存不足才开始回收,判断对象引用计数是否为0即可

区域性 更新对象引用计数时,不会影响到别的对象

缺点
1、浪费CPU,即使内存足够也会进行引用计数的统计

2、每次对象被引用时都需要更新引用计数,增加开销。

3、无法解决循环引用问题:

  1. class HeapTest {
  2. private HeapTest heapTest;
  3. public void setHeapTest(HeapTest heapTest){
  4. this.heapTest = heapTest;
  5. }
  6. }
  7. public class ReferenceCounting {
  8. public static void main(String[] args) {
  9. HeapTest heapTest = new HeapTest(); //对象实例Anew HeapTest())的引用计数:1
  10. HeapTest heapTest2 = new HeapTest(); //对象实例Bnew HeapTest())的引用计数:1
  11. heapTest2.setHeapTest(heapTest); //对象实例A的引用计数+12
  12. heapTest.setHeapTest(heapTest2); //对象实例B的引用计数+12
  13. heapTest = null; //对象实例A的引用计数-11
  14. heapTest2 = null; //对象实例B的引用计数-11
  15. }
  16. }

显然上述代码中,最后两句将heapTest及heapTest2变量赋值为null,则他们引用的对象实例A和B理论上无法再被访问,可是由于heapTest及heapTest2都互相引用了对方,导致他们的引用计数器不为0,则垃圾回收器将永远无法回收A和B的内存

二、可达性分析算法(Reachability Analysis)【目前主流语言使用的算法(python使用引用计数法)】

算法
有一系列被称为“GC Roots”的起点,从这些起点根据引用关系开始向下搜索,走过的路称为“引用链”。若一个对象没有任何引用链可以到达”GC Roots”起点,那么该对象就是不可用的,无论该对象是否还与其他对象相关联

那么,什么才是“GC Roots”起点呢?由上述我们知道,GC Roots和对象之间是引用关系,而对象基本都是存在我们的JVM堆区,也就是说,我们可以认为那些引用了堆内对象的东西,就是GC Roots起点,如:

虚拟机栈(或本地方法栈)内局部变量表中的局部变量(引用类型)

方法区内的静态变量(引用类型)

方法区内的静态常量(引用类型)
**
以上面的代码为例说明:

  1. HeapTest heapTest = new HeapTest(); //对象实例Anew HeapTest())
  2. HeapTest heapTest2 = new HeapTest(); //对象实例Bnew HeapTest())
  3. heapTest2.setHeapTest(heapTest); //对象实例B关联对象实例A
  4. heapTest.setHeapTest(heapTest2); //对象实例A关联对象实例B

虚拟机栈的栈帧局部变量表中局部变量heapTest和heapTest2都是GC Roots起点,引用指向A和B两个对象实例,而A和B也互相关联,此时引用链是正常的

图片.png

  1. heapTest = null;
  2. heapTest2 = null;

当heapTest和heapTest2重新赋值为null后,两个变量的引用就失效了,所以引用链将会变成如下模样,即便A与B之间存在关联,但是A和B两个对象实例都没有引用链到达GC Roots起点,就被标记为可回收的对象

图片.png

最终判定

那么,是不是只要无法抵达GC Roots,就会立马被回收呢?并不是,在java中,即便被标记为可回收对象,也不是立马就被“枪毙”回收,真正要宣布一个对象是否死亡,至少经过再次标记的过程。

前提已经确认对象无法抵达GC Roots

第一次标记**:筛选一次对象,条件是对象有没有必要执行finalize()方法,如果对象没有重写Object的protect方法finalize()或者finalize()方法已经被虚拟机调用过了,虚拟机会将该对象标识为“没有必要执行”,对象会被回收

第二次标记**如果对象被标识为“有必要执行finalize()方法”,那么这个对象会被放置到一个叫做F-Queue**的队列,并在稍后由一条虚拟机自动建立的、低优先级的Finalize线程去执行(finalize()方法)。也就是说,finalize执行是对象逃脱死亡的最后机会,稍后GC将再次执行筛选标记,如果在finalize方法执行期间对象重新与引用链上任意变量/对像建立关联关系,则第二次标记不会回收该对象,如果没有,则会被回收。

如下例子所示:

  1. //可达性分析算法中重写finalize方法重生的测试
  2. public class ReachabilityAnalysis {
  3. public static ReachabilityAnalysis reachabilityAnalysis;
  4. //重写父类中protect修饰finalize()方法以供调用
  5. @Override
  6. public void finalize() throws Throwable {
  7. super.finalize();
  8. System.out.println("执行finalize方法");
  9. reachabilityAnalysis=this; //重新建立连接
  10. }
  11. public static void main(String[] args) throws InterruptedException {
  12. reachabilityAnalysis = new ReachabilityAnalysis();
  13. reachabilityAnalysis = null; //断开对象引用
  14. System.gc();//执行垃圾回收,首先判断是否重写了finalize方法而且gc没有执行过finalize方法,如果是,则先调用finalize方法
  15. Thread.sleep(50);
  16. if(null == reachabilityAnalysis){
  17. System.out.println("我挂了");
  18. }else{
  19. System.out.println("我还活着");
  20. }
  21. //实际上此时的reachabilityAnalysis对象尚可进行对象赋予,做最后的挣扎
  22. reachabilityAnalysis = null; //此处再次断开引用,放弃挣扎
  23. System.gc();//执行垃圾回收,此次不会执行finalize,因为gc已经执行过一次
  24. Thread.sleep(50);
  25. if(null == reachabilityAnalysis){
  26. System.out.println("我挂了");
  27. }else{
  28. System.out.println("我还活着");
  29. }
  30. }
  31. }
  32. 运行结果:
  33. 执行finalize方法
  34. 我还活着
  35. 我挂了

finalize()方法是一个本地方法,当垃圾回收器要回收对象所占内存之前被调用,即当虚拟机宣告一个对象死亡时会首先调用该方法。

图片.png

方法区垃圾判断

上述内容都是针对堆内对象回收的判断,针对方法区, 主要回收的是废弃常量和无用的类废弃常量可以使用可达性分析算法判断,而无用类则需要同时满足下面3个条件:
**
该类所有的实例都已经被回收,意即堆内不存在该类的实例

加载该类的ClassLoader已被回收

该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方使用反射访问到该类的方法

总结

1、垃圾回收(Garbage Collection,GC),就是回收被无用垃圾所占用的内存,防止内存泄露,有效地利用可以使用的内存。

2、判断对象是否存活的算法主要包括引用计数法算法和可达性分析算法

3、引用计数法算法主要通过为每个实例赋予引用计数来标识改实例是否被引用,当引用计数归零时则证明可回收,弊端是无法解决循环引用问题

4、可达性分析算法通过每个实例与GC Roots根的引用链关系确认实例是否被引用

5、实例对象被判决死亡至少经过再次标记过程,该过程需要经过二次标记

如有贻误,还请评论指正