前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java虚拟机内存管理(四)—垃圾回收

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

作者头像
Wizey
发布2018-09-29 09:45:38
4070
发布2018-09-29 09:45:38
举报
文章被收录于专栏:编程心路

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.08.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 4、垃圾回收
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档