JVM(九):垃圾回收算法
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(九):垃圾回收算法的更多相关文章
- jvm详情——3、JVM基本垃圾回收算法回收策略
JVM基本垃圾回收算法回收策略 引用计数(Reference Counting):比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回收时,只用收集计数为0的 ...
- JVM G1垃圾回收算法简要介绍
JVM G1垃圾回收算法简要介绍 G1的特点 能够像CMS垃圾回收算法一样并发操作应用线程(潜台词:多核) 无需太长时间即可压缩空闲内存空间(潜台词:不会引起太多的GC停顿时间) 尽可能地让GC时长可 ...
- JVM常见垃圾回收算法
jdk1.7.0_79 众所周知,Java是一门不用程序员手动管理内存的语言,全靠JVM自动管理内存,既然是自动管理,那必然有一个垃圾内存的回收机制或者回收算法.本文将介绍几种常见的垃圾回收(下文简称 ...
- jvm的垃圾回收算法
一.对象存活判断判断对象是否存活一般有两种方式:1.引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收.此方法简单,无法解决对象相互循环引用的问题.2 ...
- 深入理解JVM一垃圾回收算法
我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. 一.stop the world 在介绍 ...
- jvm学习-垃圾回收算法(三)
垃圾回收算法 引用计数法 比较古老的一种垃圾回收算法.在java的GC并没有采用 增加一个引用 引用+1 减少一个引用引用减一 每次清除引用为0的的对象 缺点:不能回收循环引用的垃圾对象 标记清除 ...
- 深入探究JVM之垃圾回收算法实现细节
@ 目录 前言 垃圾回收算法实现细节 根节点枚举 安全点 安全区域 记忆集和卡表 写屏障 并发的可达性分析 低延迟GC Shenandoah ZGC 总结 前言 本篇紧接上文,主要讲解垃圾回收算法的实 ...
- JVM中垃圾回收算法
GC 算法与种类 GC的概念 Garbage Collection 垃圾收集1960年 List 使用了GCJava中,GC的对象是堆空间和永久区 引用计数法 老牌垃圾回收算法通过引用计算来回收垃圾使 ...
- 深入理解JVM(五) -- 垃圾回收算法
上篇文章我们了解到哪些内存区域和哪些对象可以被回收,这篇文章我们就来了解一下具体的垃圾回收算法的思路,不讨论具体的实现. 一 最基础算法 标记-清除(Mark-Swap) 为什么说他是最基础的算法,因 ...
随机推荐
- 将多个文本文件内的数据导入到Datagridview
private BindingList listXSxxInfoList = new BindingList(); openFileDialog1.Multiselect = true;//允许选择多 ...
- 领域驱动设计(DDD)的实践经验分享之持久化透明
原文:领域驱动设计(DDD)的实践经验分享之持久化透明 前一篇文章中,我谈到了领域驱动设计中,关于ORM工具该如何使用的问题.谈了很多我心里的想法,大家也对我的观点做了一些回复,或多或少让我深深感觉到 ...
- UWP入门(十一)--使用选取器打开文件和文件夹
原文:UWP入门(十一)--使用选取器打开文件和文件夹 很漂亮的功能,很有趣 重要的 API FileOpenPicker FolderPicker StorageFile 通过让用户与选取器交互来访 ...
- css的双飞翼布局
双飞翼布局的大概意思就是左右两边的内容是固定的,大小是固定的, 而中间的布局的随着页面的大小变化而自动变化的. 通过代码来解析: 1.四个div,也可以使用section,其中main,left.ri ...
- 一定要在commit之前做RAR备份,这样在出问题的时候,可以排除别人代码的干扰
否则找错实在是太痛苦了,根本不知道来自哪里...而这样上面那样做,可以节省时间.
- python的内存分配
一.前言 大多数编译型语言,变量在使用前必须先声明,其中C语言更加苛刻:变量声明必须位于代码块最开始,且在任何其他语句之前.其他语言,想C++和java,允许“随时随地”声明变量,比如,变量声明可以在 ...
- Java基础(一) 八大基本数据类型
自从Java发布以来,基本数据类型就是Java语言的一部分,分别是byte, short, int, long, char, float, double, boolean. 其中: 整型:byte, ...
- 解析《Effective Java》之多个构造器、Javabeans模式和Builder模式
最近看<Effective Java>这本被很多同行称为神作的书,但是这本书很多地方缺少了举例不好懂,下面是关于我对书上知识的理解. 一.<Effective Java>中文版 ...
- Call调用webservice接口,使用命名空间和不使用命名空间的区别
生活中我们会遇到许许多多的奇葩问题,而这些问题又是我们不得不解决的. 我先用一段代码来引出我想要说的内容: import javax.jws.WebMethod; import javax.jws.W ...
- 12 jQuery的ajax
什么是ajax AJAX = 异步的javascript和XML(Asynchronous Javascript and XML) 简言之,在不重载整个网页的情况下,AJAX通过后台加载数据,并在网页 ...