JVM之垃圾回收 I —— 垃圾回收策略、finalize()方法、四种引用

对比我们之前学过的c/c++,我们创建对象是通过new来创建这个对象并且分配好内存空间,在使用结束后需要通过free来释放掉这个对象的内存空间。这些都是我们程序员需要注意的问题,如果一味的创建对象开辟空间而不释放的话就会导致内存溢出。

那么,Java中的垃圾回收是如何做的呢?

我们知道Java中没有free来通过程序员释放对象的内存空间,Java的JVM里边就设计了垃圾回收,JVM就可以自己判断什么时候应该清理垃圾。 这就避免了程序员去考虑,省去了很多事。

我们在上一篇博客:JVM I——JVM理解及内存区域划分 中提到了JVM中的动态内存管理器,它主要为程序动态的分配/回收内存空间的。

而我们现在要说的垃圾回收(GC)的本质就是动态内存分配器。

1、垃圾回收回收的是什么?

垃圾回收主要回收的是JVM中已死的对象。对于JVM我们已经知道了它的内存划分,来看看它们对于垃圾回收的重要性:

  1. PC/栈:和唯一的一个线程强绑定。线程出现的时候,就需要这块内存。线程结束的时候,这块内存就可以回收了。这块内存的分配/回收时机,是非常明确的,所以,不需要GC来花大精力操心。

  2. 常量池+方法区: 这两块内存:第一,相对占比较小;第二,里面的数据,很少“失去作用”(因为是数据共享区域)。 回收这块内存的性价比不高,也不是GC重点考虑的点。

    • 但是由于Spring等框架的发展,经常使用反射机制会造成类的膨胀,所以在常量池和方法区现在也是垃圾回收的考虑点。
  3. 堆: 创建的对象大多数在堆上,并且垃圾回收主要回收的是JVM中已死的对象,所以堆是GC考虑的重点。

堆上的内存,是以“对象为单位进行管理的”,要分配,就分配一个完整的对象,要回收,也是回收一个完整的对象。

所谓,GC管理内存,重点放在管理堆上的内存,并且以对象为单位进行管理。所以,回收不再使用的内存的问题,就转换成了,回收死去对象的问题!!**

说到已经死去的对象,那么问题来了,如何判断一个对象已经死去呢?

2、垃圾回收策略——如何判断对象已死?

需要垃圾回收某一个对象时,需要判断这个对象是否可以回收,怎么判断一般有两种算法:

  • 引用计数算法(Reference Counting)
  • 可达性分析算法(Reachability Analysis)

引用计数算法(Reference Counting)

引用计数法最直白的理解就是:只要没有引用指向的对象,就是死对象。

引用计数算法基本思想:

  • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;
  • 当引用失效时,计数器值就减1;
  • 任何时刻计数器为0的对象就是不可能再被使用的。

优点:

  • 引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。
  • Object-C、Python、PHP等语言都是基于引用计数法。(脚本语言有其他的机制来解决循环引用的问题,所以适合脚本语言,而Java不适合

缺陷: 很难解决对象之间相互循环引用的问题。 GC使用引用计数法是无法独立解决循环引用的问题的,所以需要应用程序编写者配合。可以分为强弱引用来解决。把负担留给了应用程序开发者,所以不是一个特别好的GC。所以大部分JVM都没有采用。

可达性分析算法(Reachability Analysis)

可达性分析算法思想: 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为GC Roots引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

首先,堆中的对象,其实都是通过一个或多个“图形结构”进行组织的。(数据结构上的图形结构),如下图所示:
在这里插入图片描述
从红色出发,经过图的遍历,可以到达的对象,就必须活着。也就是称这个对象是可达的,对于最上面的红色就相当于GC Roots。

如下图,object5和object6虽然相互引用,但是由于他们到GC Roots都不可达,因此会被判定为可回收的对象。
在这里插入图片描述

如果让 object1.属性=null,即将object1和object2之间的指向断掉,那么object3和object4就断掉了引用链,就是对象不可达的,因此就可以被回收。如下图所示:
在这里插入图片描述
GC Roots作为一个变量指向某一个对象的。在理解了什么是GC Roots的角色之后,来看看Java中那些都是GC Roots:

java中的GC Roots:

  • 虚拟机栈(栈帧中)引用的对象(本地变量表)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象(即:Native对象)

优点:

  • 通过可达性算法,能够解决引用计数所无法解决的问题,即“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。
  • 大部分JVM都采用可达性分析算法,Java、C#等语言都是使用可达性分析算法进行垃圾回收。

3、Object对象中的finalize() 方法

调用finalize() 方法的时机: 当JVM 确定不再有指向该对象的引用时,垃圾收集器在对象上调用该方法。
finalize() 方法的作用: JVM 调用该方法,表示该对象即将“死亡”,之后JVM就可以回收该对象了。有点类似对象生命周期的临终方法。

通过使用finalize方法可以实现对象的自我拯救,但是只能拯救一次(实际也就啥用)

注意:

  • finalize方法不是判断哪些是已死的对象,而是标记这个对象即将死亡,可以被回收了。
  • 注意回收对象还是在JVM 中处理的,所以手动调用某个对象的 fifinalize() 方法,不会造成对象的“死亡”。
  • JVM承诺,一个对象被回收之前,一定会调用其finalize方法一次并且仅有一次。它说会调用但是没有说什么时候调用,可能你要做一些资源回收,但是它迟迟不调用(也许会在进程退出之前才调用),以至于导致相关资源都不能迟迟释放。 所以实际中不会使用这个方法。

综上:实际中,完全不建议使用这个方法。应用程序需要精确控制。

4、引用类型[扩展]

在Java中没有指针这个类型了,但是有和指针类型相似的引用,每个类型的引用都占4个字节,就相当于c语言中指针的,而引用类型的目的就是为了找到对象,同时也方便了程序员的开发。

随着GC的引入,引用更进一步就可以决定对象的生死,比如我们前面提到的可达性分析算法,引用不可达就代表该对象已死。

在这里插入图片描述
引用又可以分为下面四个类型:

  • 强引用: 如果一个对象具有强引用,那垃圾收器绝不会回收它
  • 软引用: 软引用是用来描述一些还有用但并非必须的对象。
  • 弱引用: 弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
  • 虚引用: 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

垃圾回收对4种引用的回收流程:

  • 第一次GC:JVM在分配空间时,若果Heap空间不足,就会进行相应的GC,但是这次GC并不会收集软引用关联的对象,回收的是不可达的强引用对象和所有弱引用对象
  • 第二次GC:在JVM发现计算进行了一次回收后内存还是不足(Allocation Failure),JVM会尝试第二次GC,回收软引用关联的对象
  • 如果还是内次不足:抛出OOM异常

在这里插入图片描述
虚引用和弱引用的区别:

  • 虚引用唯一目的:就是能在这个对象被收集器回收时收到一个系统通知。 对垃圾回收没有任何影响!
  • 弱引用:在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值