专栏首页编程心路Java虚拟机内存管理(四)—垃圾回收

Java虚拟机内存管理(四)—垃圾回收

Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的 “高墙”,墙外面的人想进去,墙里面的人却想出来。——《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》周志明

Java 虚拟机作为运行 Java 程序抽象出来的计算机,具有内存管理的能力,像内存分配、垃圾回收等这些相关的内存管理问题,Java 虚拟机都会帮我们解决,所以作为一个 Java 程序员要比 C++ 程序员幸福,但是内存方面一旦出现问题,如果对虚拟机怎样使用内存不了解,就很难排查错误。

这段时间看周志明先生的《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》,下面就对 Java 虚拟机对内存的管理做一个系统的整理,本篇文章是该专题的第四篇。

4、垃圾回收

在前面我们模拟了内存异常,其实 Java 虚拟机的垃圾回收机制为避免内存异常已经做出了最大努力,但还是无法避免上面情况的发生。垃圾收集简称为 “GC”,垃圾回收主要解决下面三个问题:

  • 哪些内存区域需要回收?
  • 什么时候回收?
  • 如何回收?

在前面对内存的划分中,程序计数器、虚拟机栈和本地方法栈都是随线程生和死的,栈中的栈帧随着方法的调用有序的进栈和出栈,每个栈帧上分配的内存在类结构确定时就已知了,所以这些区域内存的分配和回收都是具有确定性的,很容易回收内存。当方法调用结束或者该线程结束,占用的内存就要回收。而 Java堆和方法区中,每个类需要的内存都可能不一样,一个方法中多个分支需要的内存也可能不一样,只有在运行期才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收也主要是对这部分的内存进行回收。

理论上来说,如果一个对象已死,那么此时它占用的内存就应该被回收。但是怎么判断一个对象的生死,并不是那么容易的。我们也常说一些人虽然还活着但就像死了一样,而一些人虽然死了但仍然活在我们心中,在程序中生死可不能这么模棱两可,所以必须要有判断对象生死的方法。常见判断算法有下面两种:

1、引用计数算法

给每个对象添加一个引用计数器,当有一个地方引用该对象时,就将引用计数器的值加1,当引用失效时,就将引用计数器的值减1,当计数器的值为0时,就说明不存在对该对象的引用了,这个对象就没什么存活的意义了,也即是可以说这个对象是死的,可被回收。

缺点:这种判断方法虽然看起来简单高效,但是不能解决对象之间相互循环引用的问题。例如 A 对象和 B对象都是同一个类的对象实例,A 中字段 instance 引用对象 B,B 中字段 instance 也引用对象 A。如果垃圾收集器想要回收 A 对象,那么 A 的引用计时器值要为 0,也就是要清除 B 中字段 instance 对 A 的引用,也就是要清除 B 对象,而 B 对象又被 A 中的字段 instance 引用着,也就是要清除 A 对象,想要回收 A 对象,A 的 引用计数器值要为 0 ......这样就形成了循环,A 和 B 都不能被回收。不知道你有没有懵逼,反正垃圾收集器已经懵逼了。

2、可达性分析算法

从一个根节点开始向下搜索对象,搜索所走的路径称为是引用链,当一个对象从根节点开始找不到一条引用链时,就说明这个对象无法使用,或者说是对象已死可被回收。这个根节点叫做 GC Roots,是一个特殊的对象,且绝对不能被其他对象引用,不然也会像上面引用计数算法那样有循环引用的问题,GC Roots 对象包括虚拟机栈(栈帧本地变量表)中引用的对象、方法区中静态属性引用的对象、方法区常量引用的对象、本地方法栈中(Native 方法)引用的对象。

可达性分析算法判定对象是否回收.jpg

上面这两种判断方法都是“引用”有关的,引用计数算法需要计算对象的引用数量,可达性分析算法需要判断对象是否有可达的引用链。引用也是一个很模糊的概念,为了更加清晰的描述 Java 中的对象引用,在 JDK1.2 后,Java 将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference) 4 种,并且除了强引用外都有与之对应的 Java 类,分别是基类 Reference、软引用类 SoftReference、弱引用类 WeakReference、虚引用类 PhantomReference。

强引用很常见,类似 Object obj = new Object() 这种引用就是强引用。

软引用是用来描述一些有用但非必须的对象引用,当内存紧张的时候,会把这些对象列为回收目标,进行二次回收,如果回收之后还是没有足够的内存,那么就会出现内存异常。

弱引用也是用来描述一些非必须的对象引用,但是引用的强度要比软引用弱,被弱引用关联的对象将会在下一次垃圾收集时回收,而不管内存是否充足。

虚引用又称为是幽灵引用或是幻影引用,是最弱的引用关系。一个对象是否有虚引用,完全不会对该对象的生存造成影响,也无法用虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是,在这个对象被垃圾收集器回收时收到一个系统的死亡通知,通俗的说也就是死的明明白白吧。

生存还是死亡?这是个问题。

在可达性分析算法中,即使是不可达的对象,也并非是要立即执行 “死刑”,它们暂时处于 “死缓”。就像 C++ 中,对象死亡要调用析构函数一样,Java 中对象在死亡时也有一个类似的 finalize() 方法,不可达的对象会被第一次标记并进行一次筛选,筛选的条件就是这个 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法被虚拟机调用了而这个对象还没有会回收,finalize() 方法都不会被执行。如果判定需要执行 finaliz() 方法,这个对象就会被放到一个队列中,由低优先级的单独线程(刽子手)执行对象中的 finalize() 方法。如果在 finalize() 方法中,该对象被引用链上的其他对象关联了,那么这个对象就可以被移出这个 “即将回收” 队列,从而死里逃生。这个 finalize() 方法可以说是对象逃脱死亡命运的最后一次机会,如果没有逃脱,这个对象就真正要被垃圾回收器执行死刑了。但是这个机会每个对象只有一次,第一次是可以逃脱的,第二次再次进入这个队列无论如何也逃脱不了被回收的命运。

虽然 Java 虚拟机规范中没有要求对方法区进行垃圾回收,但是一些虚拟机(如 HotSpot 虚拟机)仍然实现了方法区的垃圾回收,在 HotSpot 虚拟机中称方法区为 “永久代”,其实都是一个意思,方法区的垃圾主要是废弃的常量和无用的类。我们知道方法区中有一些常量池,如字符串常量池,如果系统中不存在引用常量池中常量的引用,那么在内存紧张的时候,这些常量就应该被废弃回收,常量池中的其他类(接口)、方法、字段、符号引用也是如此。

判断常量是否应该被废弃的方法比较简单,而判断一个类是无用的类,则需要满足下面三个条件:

  • 该类的所有实例都已经被回收了,也即 Java堆内存中没有该类的对象实例。
  • 加载该类的 ClassLoader 已经被回收了。
  • 该类对应的 java.lang.Class 对象在任何地方都没有被引用,也即无法通过反射访问该类。

但满足了上述这些条件,也不是说这个类就要被非回收不可,我们可以通过设置虚拟机参数进行控制。

至此,哪些内存区域需要回收和什么时候回收就说完了,下面就是如何去回收了。垃圾回收是一个具体的过程,里面涉及到一些收集算法。几种常见的垃圾回收算法思想如下:

1、标记-清除算法

如同它的名字,算法分为 “标记” 和 “清除” 两个过程,首先标记出需要回收的对象,如下图中的灰色区域,然后再将标记出的区域内容清除。标记过程肯定需要遍历,这里面也涉及到广度优先搜索和深度优先搜索,这里就不多说了。

不足之处:一个是效率问题,搜索的效率;另外一个是空间问题,标记清除后会产生内存碎片,不利于给大对象分配内存空间。

标记-清除算法是最基础的收集算法,后续的收集算法都是对它的改进。

标记-清除算法.jpg

2、复制算法

为了解决标记-清除算法的效率问题,复制算法将内存容量划分为两个等量的部分,每次只使用一块,当一块使用完后,就将还存活的对象复制到另一块内存区域,并把刚才使用的那块内存区域一次性清除,这样每次都只需要对一半内存区域进行垃圾回收。

不足之处:这种做法看似简单除暴,但实现简单,运行高效,确实可以解决产生内存碎片的问题,而牺牲了一半可以使用的内存空间的代价未免太大。另外在对象存活率较高的时候,就需要进行大量的复制操作,效率将会变低,所以对于存活时间长的对象一般不使用这种收集算法。

复制算法.jpg

3、标记-整理算法

标记-整理算法和标记-清除算法,标记过程相同,但不同的是,标记-整理算法是将存活对象向同一端移动,然后再清除这之外的内存区域,而不是对可回收的内存直接清除。

标记-整理算法.jpg

4、分代收集算法

分代收集算法中没有新的思想,只是根据对象存活的周期长短又对 Java堆内存进行了划分,一般是把 Java堆分为新生代(Young)和老年代(Old)。新生代和老年代的默认内存大小比例是 1 : 2,也即是新生代占据 1/3 的堆内存空间,老生代占据 2/3 的堆内存空间,这个比例值是可以通过 -XX:NewRatio 参数来动态设置的。而新生代又被划分为 Eden、From Survivor、To Survivor 三个区域,Eden 占据 8/10 的新生代内存空间,并且Java 虚拟机每次只会使用 Eden 区和一个 Survivor(From Survivor 和 To Survivor中的一个) 区,总有一个 Survivor 区是空闲着的。新生代区域用来存放那些朝生夕死的 Java 对象,这些对象存活时间很短,很容易就会被垃圾收集器回收,所以新生代使用复制算法会比较好,而老生代区域用来存放大对象(如对象数组),这些对象不是很容易被回收,存活时间比较长,使用标记-清除算法和标记-清理算法是比较好的。针对不同区域内对象存活时间的长短,使用合适的收集算法,可以最大的发挥出算法优势。

分代收集算法.jpg

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 这些面试必备的Java内存管理知识你需要吗?

    Java虚拟机规范将物理内存(主内存和CPU中的缓存、寄存器)划分为 程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区五个区域,但并没有规定这些...

    Wizey
  • 算法妙应用-算法的复杂度

    对于任何一个程序来说,都可以从三个方面进行分析,分别是 输入、处理、输出,也即 IPO(Input、Process、Output),这种分析方法对硬件和软件程序...

    Wizey
  • Linux目录及文件相关知识整理

    Linux 文件管理是学习 Linux 的基础,看了鸟叔的 Linux 书籍,把 Linux 目录及文件相关的知识整理如下。

    Wizey
  • 面试题:讲讲Java垃圾回收机制

    本文内容是基于 JDK 1.6 的,不同版本虚拟机之间也许会有些许差异,但不影响我们对JVM垃圾回收机制的整体把握和了解。

    用户1263954
  • JVM学习笔记——垃圾收集器与内存分配策略(1)

    上一篇文章介绍了java运行时内存的各个区域,其中虚拟机栈,程序计数器,本地方法栈三个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出有条不紊的执行着...

    用户1665735
  • 垃圾收集算法

    垃圾收集器是Java虚拟机中自带的功能,它的目的是帮助我们管理内存,正是因为有它的存在所以,我们在开发时,基本不用考虑内存溢出等问题。基本不用考虑不代表,一定不...

    吉林乌拉
  • 深入了解JVM垃圾收集器

    程序计数器、JVM栈、本地方法栈这三个内存区域和线程是一一对应的,并且每一个线程的这三个区域相互独立互不干扰。他们都随着线程的产生而产生,线程的灭亡而灭亡。JV...

    大闲人柴毛毛
  • 深入理解Java虚拟机(JVM) --- 垃圾收集算法(中)

    由于方法区中存放生命周期较长的类信息、常量、静态变量. 因此方法区就像堆的老年代,每次GC只有少量垃圾被清除.

    JavaEdge
  • 持续3分钟 - Java -05

    在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收...

    子乾建建-Jeff
  • 疯狂Java笔记之Java的内存与回收

    对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用改对象?只要有引用变量引用对象,垃圾回收机制就不会回收它。

    HelloJack

扫码关注云+社区

领取腾讯云代金券