JVM(九):垃圾回收算法

在本文中,我们将从概念模型的角度探讨 JVM 是如何回收对象,包括 JVM 是如何判断一个对象已经死亡,什么时候在哪里进行了垃圾回收,垃圾回收有几种核心算法,每个算法优劣是什么等。

为何需要GC

Java 中的一个核心技术就是自动垃圾回收,该技术使得程序员可以不用像写 C++ 一样手动分配和释放内存,那么为何还需要我们去学习垃圾回收呢。这里就要说到两个概念了。

  • 内存泄露:有已经不再使用的对象仍然占用着内存;
  • 内存溢出:已经没有足够的空间可以让 JVM 分配内存给对象了。

大量的内存泄露会引发内存溢出,但内存溢出不一定是内存泄露引起的,也可能是因为总共的内存空间就不够大,而需要分配的对象太大导致。

学习垃圾回收的背后逻辑,可以让我们在程序发生内存溢出的时候,快速高效地排查出问题进行解决。并且学习了 GC 的细节,也有助于我们调节 JVM 的一些运行参数,让系统达到更高的并发量。

对象的死亡

如果要销毁一个对象,那么就需要确定该对象已经死亡,只有这样才能够将该对象所占的内存空间进行释放。那么 JVM 是如何判断一个对象已经死亡了呢。

引用计数法

引用计数法实现十分简单,就是给每一个对象增加一个计数器,每当有一个地方对其进行了引用就 +1,当引用失效时就 -1,如果计数器的值为 0,则代表该对象已经不再被使用了,可以对其回收了。

这种方式的最大优点就是实现简单,判定效率高。但其有一个致命的缺点就是 循环引用问题。当两个对象互相引用,但其实他们已经没有任何其他用处了。此时因为彼此间还存在引用,就会发生循环引用,使用引用计数法就无法对其进行回收。

可达性分析算法

正是因为引用计数法那个致命的缺点,因此主流的实现都是通过 可达性分析 来判断对象能否进行销毁。其核心思想是 通过一系列称为 "GC Roots" 的节点来作为起始点,从这些节点开始搜索,这个搜索的轨迹被称为 "引用链",如果一个对象没有包含在任何一个引用链中,那么就判断该对象是无效的。

概念中说到是通过 GC Roots 来作为起始点,那么哪些对象可以作为GC Roots呢。

  • 虚拟机栈中引用的对象;
  • 本地方法栈中引用的对象;
  • 方法区中静态属性引用的对象;
  • 方法区中常量引用的对象。

引用的区分

在判断对象能否被销毁的时候,都使用到了 引用 这个词语,说的是如果有被引用的,那么就不销毁,如果没有引用则将其进行销毁,这种分别方式非黑即白,太过强硬,因此 JDK1.2 之后对引用的含义进行了扩充,实现了多级回收的效果。即在内存不紧张的时候,有一些对象是可以进行保留的,但如果内存紧张的时候,就需要对其进行回收。

  • 强引用:我们平常在编程使用的引用都是这种,普遍存在的引用,只要是这个,就不会被回收;
  • 软引用:有用但非必需。在内存很紧张快要溢出的时候,就会回收这些对象,如果回收后还没有空余空间才会报内存溢出。这种引用通常用来实现内存敏感的缓存;
  • 弱引用:比软引用更弱一些,只能活到下一次垃圾回收前。其实主要回收的就是这些内存;
  • 虚引用:虚引用不对对象生存时间造成影响,也无法通过虚引用获得对象实例。其存在的价值就是在对象收到回收的时候,能够让系统做一些事。应用场景为跟踪对象被 GC 的活动,因为其被回收的时候系统会受到一条系统通知。

方法区的回收

前面说的都是对象的回收,即对堆内存的回收,但其实在方法区内也是有垃圾回收的。在方法区内回收的内容主要是 废弃常量无用的类

其中废弃常量很好理解。就是常量池中的一个常量已经没有任何对象引用它了,即其已经没有价值了,那么就会将其移出常量池,回收其空间。

而对无用的类进行回收又是怎么理解的呢。首先我们需要判断什么是无用的类。一个类是无用的,需要满足以下3点:

  • 该类的实例都已经被回收了,即堆中没有该类的实例对象
  • 加载该类的 ClassLoader 已经被回收
  • 无法通过反射访问该类,即该类对应的 java.lang.Class 对象没有被调用

只有满足以上3点的类才可以被回收,但其是否回收取决于 JVM 启动时的参数控制。JVM 可以在启动时设置不对类进行回收。

回收算法

上面我们已经明白了什么对象是可以回收的,那么我们该如何针对这些对象进行回收呢。回收前后内存空间又是如何布局的呢。下面就让我们来看一下几个主流的 GC 算法。

标记-清除算法

标记-清除算法是最简单,最基本的算法。其本质就如同其名字一样,分为2个步骤,首先标记出所有需要清除的对象,然后在回收阶段,统一清除即可。

但其拥有两个严重的缺点。一个是标记和清除阶段都不快,效率很低;另一个是其只是单纯的将无用的对象清除,很容易造成大量的内存碎片,如果内存碎片太多,那么在分配大对象的时候,就很容易造成内存不够的情况。因此针对这些情况,就出现了几个改进的优良版本。

标记-整理算法

标记-整理算法解决的是内存碎片的问题,在标记阶段还是采取一样的解决方式,但在下个阶段并不是直接清除掉无用对象,而是先将有用的对象移到内存的一边,然后直接回收掉分界线一边的对象,这样就可以腾出许多规整的空间。

复制算法

标记-整理算法只是解决了内存碎片的问题,但是效率问题还是一个痛点,因此就有人提出了复制算法。其将内存空间分为 2 部分,每一次只使用其中一块,当这一块的空间用完了,就将存活的对象复制到另一边去,然后将使用过的空间直接清理掉即可。这种算法十分的高效,也解决了内存碎片的问题。

但其将可用空间简单的划分为了50-50,代价十分的高昂。不过经过研究表明,新生代对象大部分都是朝生夕死的,因此不需要按照 1:1 的比例来划分空间。商用的虚拟机 HotSpot 就是默认将内存划分为 8:1:1,即一块Eden区,两个Survivor区,在进行分配时,将 Eden 和一个 Survivor 直接复制到另一个 Survivor 即可,这样解决了复制算法空闲空间太大的问题,又提高了 GC 的效率。

但是也正是因为这样的划分,Survivor 的内存空间是比较小的,因此需要有一个其他内存进行分配担保,确保大对象也能够进行内存分配,这就老年代存在的价值之一。当另一块 Survivor 没有足够空间放置对象时,将会直接将对象分配至老年代。而老年代采取的 GC 算法为标记-整理算法。

分代算法

经过上面 3 种算法的分析,想必大家也想到了,分代算法其实并不是一个新算法,其只是根据前面算法的优劣将内存空间进行了划分,对每个不同的空间采取不同的算法,以便根据各个不同的年代采取不同的,最适合的算法。

在 Java8 之前,方法区称为永久代,也如同堆空间一样被 GC 进行管理,但在 Java8 之后,这种实现方式被MetaSpace 取代,采用直接内存的方式来进行内存分配管理.

Hotspot 将内存划分为新生代和老年代。新生代因为大部分的对象都是快节奏的,因此采用复制算法来处理。而老年代因为对象存活率高,且已经没有额外空间对齐进行分配担保了,因此采用标记-清理或标记-整理算法进行处理。

总结

在本文中,我们介绍了什么是垃圾回收,如何判断对象应该进行回收了,以及回收逻辑的几个不同抽象模型。在后面的文章,我们将对算法的具体实现进行探讨,了解当前业内主流的虚拟机实现,看看在实际生产情况下,不同的 垃圾收集器 的具体实现方式.

文章在公众号 “iceWang" 第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

本系列文章主要借鉴自《深入分析 JavaWeb 技术内幕》和《深入理解 Java 虚拟机-JVM高级特性与最佳实践》。

JVM(九):垃圾回收算法的更多相关文章

  1. jvm详情——3、JVM基本垃圾回收算法回收策略

    JVM基本垃圾回收算法回收策略 引用计数(Reference Counting):比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回收时,只用收集计数为0的 ...

  2. JVM G1垃圾回收算法简要介绍

    JVM G1垃圾回收算法简要介绍 G1的特点 能够像CMS垃圾回收算法一样并发操作应用线程(潜台词:多核) 无需太长时间即可压缩空闲内存空间(潜台词:不会引起太多的GC停顿时间) 尽可能地让GC时长可 ...

  3. JVM常见垃圾回收算法

    jdk1.7.0_79 众所周知,Java是一门不用程序员手动管理内存的语言,全靠JVM自动管理内存,既然是自动管理,那必然有一个垃圾内存的回收机制或者回收算法.本文将介绍几种常见的垃圾回收(下文简称 ...

  4. jvm的垃圾回收算法

    一.对象存活判断判断对象是否存活一般有两种方式:1.引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收.此方法简单,无法解决对象相互循环引用的问题.2 ...

  5. 深入理解JVM一垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. 一.stop the world 在介绍 ...

  6. jvm学习-垃圾回收算法(三)

     垃圾回收算法  引用计数法 比较古老的一种垃圾回收算法.在java的GC并没有采用 增加一个引用 引用+1 减少一个引用引用减一 每次清除引用为0的的对象 缺点:不能回收循环引用的垃圾对象 标记清除 ...

  7. 深入探究JVM之垃圾回收算法实现细节

    @ 目录 前言 垃圾回收算法实现细节 根节点枚举 安全点 安全区域 记忆集和卡表 写屏障 并发的可达性分析 低延迟GC Shenandoah ZGC 总结 前言 本篇紧接上文,主要讲解垃圾回收算法的实 ...

  8. JVM中垃圾回收算法

    GC 算法与种类 GC的概念 Garbage Collection 垃圾收集1960年 List 使用了GCJava中,GC的对象是堆空间和永久区 引用计数法 老牌垃圾回收算法通过引用计算来回收垃圾使 ...

  9. 深入理解JVM(五) -- 垃圾回收算法

    上篇文章我们了解到哪些内存区域和哪些对象可以被回收,这篇文章我们就来了解一下具体的垃圾回收算法的思路,不讨论具体的实现. 一 最基础算法 标记-清除(Mark-Swap) 为什么说他是最基础的算法,因 ...

随机推荐

  1. SQL Server 可更新订阅中有行筛选的同步复制移除项目而不重新初始化所有订阅!

    原文:SQL Server 可更新订阅中有行筛选的同步复制移除项目而不重新初始化所有订阅! 在可更新订阅的同步复制中,有行筛选的项目表,移除的时候会提示重新初始化所有的快照并且应用此快照,这将导致所有 ...

  2. SQLSERVER使用密码加密备份文件以防止未经授权还原数据库

    原文:SQLSERVER使用密码加密备份文件以防止未经授权还原数据库 SQLSERVER使用密码加密备份文件以防止未经授权还原数据库 在备份数据库的时候,用户可以为媒体集.备份集或两者指定密码 在ba ...

  3. 基于X.509证书和SSL协议的身份认证过程实现(OpenSSL可以自己产生证书,有TCP通过SSL进行实际安全通讯的实际编程代码)good

    上周帮一个童鞋做一个数字认证的实验,要求是编程实现一个基于X.509证书认证的过程,唉!可怜我那点薄弱的计算机网络安全的知识啊!只得恶补一下了. 首先来看看什么是X.509.所谓X.509其实是一种非 ...

  4. Android零基础入门第87节:Fragment添加、删除、替换

    前面一起学习了Fragment的创建和加载,以及其生命周期方法,那么接下来进一步来学习Fragment的具体使用,本期先来学习Fragment添加.删除.替换. 一.概述 在前面的学习中,特别是动态加 ...

  5. ShellExecute的跨平台实现OpenUrl

    OpenUrl 是 iOS 中 UIApplication 提供的一个函数,用于调用其它程序.实际上各个平台都有自己的实现,这里提供一个直接封装完的跨平台版本给大家.   uses {$IFDEF M ...

  6. Access Violation分成两大类:运行期和设计期(很全的解释)

    用Delphi开发程序时,我们可以把遇到的Access Violation分成两大类:运行期和设计期. 一.设计期的Access Violation 1.硬件原因  在启动或关闭Delphi IDE以 ...

  7. c# winform快捷键实现

    我们在软件中经常用到快捷键,这里整理备份一下. 首先我们要定义可以作为快捷键的按键,以下是整理的 一些,自己可以根据情况来修改 public static Dictionary<int, str ...

  8. 一、Linux常用命令

    1.ls 作用:列出文件信息,默认为当前目录下 语法: -a:列出所有的文件,包括以.开头的隐藏文件 -d:列出目录本身,并不包含目录中的文件 -h:和-l一起使用,文件大小人类易读 -l:长输出(“ ...

  9. Java开发桌面程序学习(十)——css样式表使用以及Button悬浮改变样式实现

    css样式表使用 javafx中的css样式,与html的有些不一样,javafx中的css,是以-fx-background-color这种样子的,具体可以参考文档JavaFx css官方文档 ja ...

  10. Linux常用实用命令

    Linux是我们开发人员必不可少的系统,也是经常接触到的.然而,Linux命令比较多,有些不常用也难记住.那么,我们如何更高效的使用Linux命令,而又不必全面地学习呢?今天就给大家分享一下我在开发过 ...