在第 6 讲中我们说到 Java 虚拟机的内存结构,提到了这部分的规范其实是由《Java 虚拟机规范》指定的,每个 Java 虚拟机可能都有不同的实现。其实涉及到 Java 虚拟机的内存,就不得不谈到 Java 虚拟机的垃圾回收机制。因为内存总是有限的,我们需要一个机制来不断地回收废弃的内存,从而实现内存的循环利用,这样程序才能正常地运转下去。

比起 Java 虚拟机的内存结构有《Java 虚拟机规范》规定,垃圾回收机制并没有具体的规范约束。所以很多时候不同的虚拟机有不同的实现方式,下面所说的垃圾回收都是以 HotSpot 虚拟机为例。

到底谁是垃圾?

要进行垃圾回收,最为重要的一个问题是:判断谁是垃圾?

联想其日常生活中,如果一个东西经常没被使用,那么这个对象可以说就是垃圾。在 Java 中也是如此,如果一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收。

根据这个思想,我们很容易想到使用引用计数的方法来判断垃圾。在一个对象被引用时加一,被去除引用时减一,这样我们就可以通过判断引用计数是否为零来判断一个对象是否为垃圾。这种方法我们一般称之为「引用计数法」。

上面的这种方法虽然简单,但是其存在一个致命的问题,那就是循环引用。

A 引用了 B,B 引用了 C,C 引用了 A,它们各自的引用计数都为 1。但是它们三个对象却从未被其他对象引用,只有它们自身互相引用。从垃圾的判断思想来看,它们三个确实是不被其他对象引用的,但是此时它们的引用计数却不为零。这就是引用计数法存在的循环引用问题。

而现今的 Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。其大概的过程是这样:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。

可以看到这里最重要的就是 GC Root 这个集合了,其实 GC Root 就是一组活跃引用的集合。但是这个集合又与一般的对象集合不太一样,这些集合是经过特意筛选出来的,通常包括:

  • 所有当前被加载的 Java 类
  • Java 类的引用类型静态变量
  • Java类的运行时常量池里的引用类型常量
  • VM的一些静态数据结构里指向GC堆里的对象的引用
  • 等等

简单地说,GC Root 就是经过精心挑选的一组活跃引用,这些引用是肯定存活的。那么通过这些引用延伸到的对象,自然也是存活的。

如何进行垃圾回收?

到这里,我们了解了什么是垃圾以及 JVM 是如何判断垃圾对象的。那么识别出垃圾对象之后,JVM 是如何进行垃圾回收的呢?这就是我们下面要讲的内容:如何进行垃圾回收?

垃圾回收算法简单地说有三种算法:标记清除算法、复制算法、标记压缩算法。

标记清除算法。从名字可以看到其分为两个阶段:标记阶段和清除阶段。一种可行的实现方式是,在标记阶段,标记所有由 GC Root 触发的可达对象。此时,所有未被标记的对象就是垃圾对象。之后在清除阶段,清除所有未被标记的对象。标记清除算法最大的问题就是空间碎片问题。如果空间碎片过多,则会导致内存空间的不连续。虽说大对象也可以分配在不连续的空间中,但是效率要低于连续的内存空间。

复制算法。复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收。该算法的缺点是要将内存空间折半,极大地浪费了内存空间。

标记压缩算法。标记压缩算法可以说是标记清除算法的优化版,其同样需要经历两个阶段,分别是:标记结算、压缩阶段。在标记阶段,从 GC Root 引用集合触发去标记所有对象。在压缩阶段,其则是将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。

对比一下这三种算法,可以发现他们都有各自的优点和缺点。

标记清除算法虽然会产生内存碎片,但是不需要移动太多对象,比较适合在存活对象比较多的情况。而复制算法虽然需要将内存空间折半,并且需要移动存活对象,但是其清理后不会有空间碎片,比较适合存活对象比较少的情况。而标记压缩算法,则是标记清除算法的优化版,减少了空间碎片。

分代思想

试想一下,如果我们单独采用任何一种算法,那么最终的垃圾回收效率都不会很好。其实 JVM 虚拟机的建造者们也是这么想的,因此在实际的垃圾回收算法中采用了分代算法。

所谓分代算法,就是根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。

试想一下,如果没有采用分代算法,而在老年代中使用复制算法。在极端情况下,老年代对象的存活率可以达到100%,那么我们就需要复制这么多个对象到另外一个内存区域,这个工作量是非常庞大的。

在这里我们再深入地聊一聊新生代里采取的垃圾回收算法。如我们上面所说,新生代的特点是存活对象少,适合采用复制算法。而复制算法的一种最简单实现便是折半内存使用,另一半备用。但实际上我们知道,在实际的 JVM 新生代划分中,却不是采用等分为两块内存的形式。而是分为:Eden 区域、from 区域、to 区域 这三个区域。那么为什么 JVM 最终要采用这种形式,而不用 50% 等分为两个内存块的方式?

要解答这个问题,我们就需要先深入了解新生代对象的特点。根据IBM公司的研究表明,在新生代中的对象 98% 是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间。所以在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。

通过这种方式,内存的空间利用率达到了90%,只有10%的空间是浪费掉了。而如果通过均分为两块内存,则其内存利用率只有 50%,两者利用率相差了将近一倍。

分区思想

分代思想按照对象的生命周期长短将其分为了两个部分(新生代、老年代),但 JVM 中其实还有一个分区思想,即将整个堆空间划分成连续的不同小区间。

每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制 GC 时间。

到这里我们基本上把 JVM 的垃圾回收都将清除了,从一开始什么是垃圾,到之后如何判断垃圾,到如何回收垃圾,到垃圾回收的两个重要思想:分代思想、分区思想。通过这么一个脉络,我们了解了垃圾回收的整体概括。在下面的章节中,我们将深入介绍这其中的细节。

参考资料


如果只是看,其实无法真正学会知识的。为了帮助大家更好地学习,我建了一个虚拟机群,专门讨论学习 Java 虚拟机方面的内容,每周针对我所发文章进行讨论答疑。如果你有兴趣,关注「Java技术精选」公众号,通过右下角菜单「入群交流」加我好友,小助手会拉你入群。


JVM基础系列目录

JVM基础系列第8讲:JVM 垃圾回收机制的更多相关文章

  1. JVM基础系列第10讲:垃圾回收的几种类型

    我们经常会听到许多垃圾回收的术语,例如:Minor GC.Major GC.Young GC.Old GC.Full GC.Stop-The-World 等.但这些 GC 术语到底指的是什么,它们之间 ...

  2. JVM基础系列第9讲:JVM垃圾回收器

    前面文章中,我们介绍了 Java 虚拟机的内存结构,Java 虚拟机的垃圾回收机制,那么这篇文章我们说说具体执行垃圾回收的垃圾回收器. 总的来说,Java 虚拟机的垃圾回收器可以分为四大类别:串行回收 ...

  3. JVM基础系列第15讲:JDK性能监控命令

    查看虚拟机进程:jps 命令 jps 命令可以列出所有的 Java 进程.如果 jps 不加任何参数,可以列出 Java 程序的进程 ID 以及 Main 函数短名称,如下所示. $ jps 6540 ...

  4. JVM基础系列第14讲:JVM参数之GC日志配置

    说到 Java 虚拟机,不得不提的就是 Java 虚拟机的 GC(Garbage Collection)日志.而对于 GC 日志,我们不仅要学会看懂,而且要学会如何设置对应的 GC 日志参数.今天就让 ...

  5. JVM基础系列第13讲:JVM参数之追踪类信息

    我们都知道 JVM 在启动的时候会去加载类信息,那么我们怎么得知他加载了哪些类,又卸载了哪些类呢?我们这一节就来介绍四个 JVM 参数,使用它们我们就可以清晰地知道 JVM 的类加载信息. 为了方便演 ...

  6. JVM基础系列第11讲:JVM参数之堆栈空间配置

    JVM 中最重要的一部分就是堆空间了,基本上大多数的线上 JVM 问题都是因为堆空间造成的 OutOfMemoryError.因此掌握 JVM 关于堆空间的参数配置对于排查线上问题非常重要. tips ...

  7. JVM基础系列第7讲:JVM 类加载机制

    当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析.运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制.JVM 虚拟机执行 class 字节 ...

  8. JVM基础系列第6讲:Java 虚拟机内存结构

    看到这里,我相信大家对于一个 Java 源文件是如何变成字节码文件,以及字节码文件的含义已经非常清楚了.那么接下来就是让 Java 虚拟机运行字节码文件,从而得出我们最终想要的结果了.在这个过程中,J ...

  9. JVM基础系列第5讲:字节码文件结构

    温馨提示:此篇文章长达两万字,图片50多张,内容非常多,建议收藏后再看. 前面我们说到 Java 虚拟机使用字节码实现了跨平台的愿景,无论什么系统,我们都可以使用 Java 虚拟机解释执行字节码文件. ...

随机推荐

  1. Emsemble

    RM # -*- coding: utf-8 -*- """ RandomForestClassifier 예 """ import pan ...

  2. python re库的正则表达式学习笔记

    1. 安装 默认已经安装好了python环境了 re库是python3的核心库,不需要pip install,直接import就行 2. 最简单的模式 字符本身就是最简单的模式 比如:'A', 'I ...

  3. python入门编程之mysql编程

    python关于mysql方面的连接编程 前提:引入mysql模块MySQLdb,即:MySQL_python-1.2.5-cp27-none-win_amd64.whl 如果要用线程池,则要引用模块 ...

  4. jedis keys和scan操作

    关于redis的keys命令的性能问题 KEYS pattern 查找所有符合给定模式 pattern 的 key . KEYS * 匹配数据库中所有 key . KEYS h?llo 匹配 hell ...

  5. vue-cli3.0安装element-ui组件及按需引入element-ui组件

    在VUE-CLI 3下的第一个Element-ui项目(菜鸟专用) (https://www.cnblogs.com/xzqyun/p/10780659.html) 上面这个链接是vue-cli3.0 ...

  6. ECMAScript 6 之 let 和 const 命令

    1.let基本用法 1.1.声明变量 let声明的变量只在它所在的代码块有效. 1.2.不存在变量提升 var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined: let命令 ...

  7. [pyMongo]insert_many的Bulkwrite实现机制

    在SQL中,insert many的操作可能会出现插入数据量过大的问题. 印象中MySQL Driver对insert语句的buffer有一个大小限制.超过这个限制的数据可能会被丢弃? -- > ...

  8. SDN网络虚拟化中有效协调的映射算法

    来自论文An efficient and coordinated mapping algorithm in virtualized SDN networks,来自期刊<信息与电子工程前沿> ...

  9. Linux SVN服务器的搭建配置及分支的创建与合并

    第一步:通过yum命令安装svnserve,命令如下: >yum -y install subversion 若需查看svn安装位置,可以用以下命令: >rpm -ql subversio ...

  10. 批量删除Excel里面的换行符

    关于批量删除excel里面的换行符,应该说写程序的遇上excel大体都会有这么个问题,在解决这个问题前,我的解决办法是把excel 的数据全部复制到txt里面, 因为操作txt比操作excel更为简单 ...