Java GC(绝对干货)

范围:要回收哪些区域

在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。

前提:如何判断对象已死

所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面讲的不可达主要是指应用程序已经没有内存块的引用了, 在Java中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

引用计数算法

引用计数是最简单直接的一种方式,这种方式在每一个对象中增加一个引用的计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。
优点:简单,直接,不需要暂停整个应用
缺点:1.需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作;2.不能处理循环引用的问题
因此这种方法是垃圾收集的早期策略,现在很少使用。Sun的JVM并没有采用引用计数算法来进行垃圾回收,而是基于根搜索算法的。

可达性分析算法(根搜索算法)

通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。

在java语言中,可作为GCRoot的对象包括以下几种:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象。

四种引用

GC在收集一个对象的时候会判断是否有引用指向对象,在JAVA中的引用主要有四种:

强引用(Strong Reference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用(Soft Reference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了内存溢出的问题。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(Weak Reference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(Phantom Reference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用于检测对象是否已经从内存中删除,跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
虚引用的唯一目的是当对象被回收时收到一个系统通知。

finalize() 方法

通过可达性分析,那些不可达的对象并不是立即被销毁,他们还有被拯救的机会。
如果要回收一个不可达的对象,要经历两次标记过程。首先是第一次标记,并判断对象是否覆写了 finalize 方法,如果没有覆写,则直接进行第二次标记并被回收。
如果对象有覆写finalize 方法,则会将改对象加入一个叫“F-Queue”的队列中,虚拟机会建立一个低优先级的 Finalizer 线程去执行它,这里说的“执行”是指该线程会去触发 finalize 方法,但是并不会等待 finalize 方法执行完成。主要是因为 finalize 方法的不确定性,它可能要花很长时间才能执行完成,甚至死循环,永远不结束,这将导致整个 GC 工作的异常,甚至崩溃。
关于拯救,可以在 finalize 方法中将自己(this关键字)赋值给类变量或其他对象的成员变量,则第二次标记时它将被移出回收的集合,如果对象并未被拯救,则最终被回收。
finalize 方法只会被调用一次,如果一个在 finalize 被拯救的对象再次需要回收,则它的 finalize 将不会再被触发了。
不建议使用finalize 方法,它的运行代价高,不确定性大,GC 也不会等待它执行完成,它的功能完全可以被 try-finally 代替。

方法区的回收

方法区也会被回收,其被回收的内存有:废弃常量、无用的类。
在 HotSpot 虚拟机规范里,将永久带作为方法区的实现。
废弃常量:没有被引用的常量,如 String。
判断无用的类:
(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
(2).加载该类的类加载器已经被回收。
(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

各种垃圾收集算法

标记-清除算法

步骤:
1、标记:从根集合开始扫描,标记存活对象;
2、清除:再次扫描真个内存空间,回收未被标记的对象。
此算法一般没有虚拟机采用
优点1:解决了循环引用的问题
优点2:与复制算法相比,不需要对象移动,效率较高,而且还不需要额外的空间
不足1:每个活跃的对象都要进行扫描,而且要扫描两次,效率较低,收集暂停的时间比较长。
不足2:产生不连续的内存碎片

标记-整理(压缩)算法

对标记-清除算法的改进
标记过程与标记-清除算法一样,但是标记完成后,存活对象向一端移动,然后清理边界的内存
步骤:
1、标记:从根集合开始扫描,标记存活对象;
2、整理:再次扫描真个内存空间,并往内存一段移动存活对象,再清理掉边界的对象。
不会产生内存碎片,但是依旧移动对象的成本。
适合老年代
还有一种算法是标记-清除-整理(压缩),是在多次标记清除后,再进行一次整理,这样就减少了移动对象的成本。

复制算法

将内存分成两块容量大小相等的区域,每次只使用其中一块,当这一块内存用完了,就将所有存活对象复制到另一块内存空间,然后清除前一块内存空间。
此种方法实现简单、效率较高,优点:
1、不会产生内存碎;
2、没有了先标记再删除的步骤,而是通过Tracing从 From内存中找到存活对象,复制到另一块To内存区域,From只要移动堆顶指针便可再次使用。
缺点:
1、复制的代价较高,所有适合新生代,因为新生代的对象存活率较低,需要复制的对象较少;
2、需要双倍的内存空间,而且总是有一块内存空闲,浪费空间。

分代收集算法

所有商业虚拟机都采用这种方式,将堆分成新生代和老年代,新生代使用复制算法,老年代使用标记-整理算法

GC 类型

1.Minor GC 针对新生代的 GC
2.Major GC 针对老年代的 GC
3.Full GC 针对新生代、老年代、永久带的 GC

为什么要分不同的 GC 类型,主要是1、对象有不同的生命周期,经研究,98%的对象都是临时对象;2、根据各代的特点应用不同的 GC 算法,提高 GC 效率。

各种垃圾收集器

串行收集器(Serial Collector)

单线程,会发生停顿
适用场景:
1.单 CPU、新生代小、对停顿时间要求不高的应用
2.client 模式下或32位 Windows 上的默认收集器
新生代均采用复制算法,老年代用标记-整理算法(Serial Old Collector)
在单核 CPU 上面的运行效果较好,甚至可能超过并行垃圾收集器,因为并行垃圾收集器有线程的切换消耗。
当 Eden 空间分配不足时触发
原理:
1.拷贝 Eden 和 From 空间的存活对象到 To 空间
2.部分对象可能晋升到老年代(大对象、达到年龄的对象、To 空间不足时)
3.清空 Eden、From 空间,From 与 To 空间交换角色

ParNew(Serial 收集器的多线程版本)

新生代收集器,是 Serial 的多线程版,是 Server 模式下的虚拟机中首选的新生代收集器,不是默认收集器。
除了 Serial 外,是唯一能与 CMS 收集器配合工作的收集器。
多线程下,性能较好,单线程下,并不会比 Serial 好。

并行收集器(Parallel Scavenge)

特性:
1.并行、停顿
2.并行线程数:CPU <= 8 := 8,CPU > 8 := (3+ cpu * 5) / 8,也可强制指定 GC 线程数
3.自适应调节策略,如果把该策略打开,则虚拟机会自动调整新生代的大小比例和晋升老年代的对象大小、年龄等细节参数
4.吞吐量优先收集器,即可用设置一个 GC 时间,收集器将尽可能的在该时间内完成 GC

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),即吞吐量越高,则垃圾收集时间就要求越短

用户可以设置最大垃圾收集停顿时间或者吞吐量
但并不是把最大垃圾收集停顿时间设置得越短越好,因为它是以牺牲吞吐量和新生代空间的代价来换取的,比如收集300M 空间总会比收集500M 空间更快,再如收集频率加高,本来10秒收集一次,每次停顿100毫秒,但是现在改成了5秒收集一次,每次停顿70毫秒,停顿时间是小了,但是吞吐量确也降下来了。

适用场景:
1.多 CPU、对停顿时间要求高的应用
2.是 Server 端的默认新生代收集器

Serial Old

是 Serial 收集器的老年代版本,依旧是单线程收集器,采用标记-整理算法,

Parallel Old

CMS(并发-标记-清除)

CMS 是一种以获取最短回收停顿时间为目标的收集器。
步骤:
1.初始标记
此阶段仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快,但是会停顿

注意:这里不是 GC Roots Tracing 的过程

2.并发标记
GC Roots Tracing 的过程,这个阶段可以与用户线程一起工作,不会造成停顿,从而导致整个停顿时间大大降低
3.重新标记
是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4.并发清除
优点:停顿时间短,但是总的 GC 时间长
缺点:
1.并发程序都是 CPU 敏感的,并发标记和并发清除可能会抢占应用 CPU
2.总的 GC 时间长
3.无法处理浮动垃圾

浮动垃圾:在并发清除过程中,程序还在运行,可能产生新的垃圾,但是本次 GC 确不可能清除掉这些新产生的垃圾了,所以这些新产生垃圾就叫浮动垃圾,也就是说在一次 CMS 的 GC 后,用户获取不到一个完全干净的内存空间,还是或多或少存在浮动垃圾的。

4.由于在并发标记和并发清除阶段,用户程序依旧在运行,所以也就需要为用户程序的运行预留一定空间,而不能想其他收集器一样会暂停用户程序的运行。在此期间,就可能发生预留空间不足,导致程序异常的情况。
5.是基于标记-清除的收集器,所以会产生内存碎片

G1

这款开发了10多年的收集器还比较年轻,目前还很少听说有人在生产环境使用。
此款收集器可以独立管理整个 java heap 空间,而不需要其他收集器的配合。
步骤:
1. 初始标记
与CMS 一样,只是标记一下 GC Roots 能直接关联到的对象,速度很快,但是需要停顿
2. 并发标记
GC Roots Tracing 过程,并发执行
3. 最终标记
并行执行,需要停顿
4. 筛选回收
并行执行,需要停顿

G1收集器把 Heap 分为多个大小相等的 Region,G1可以有计划的避免进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先收集价值大的 Regin,保证 G1收集器在有限时间内获取最大的收集效率。

优点:
1. 存在并发与并行操作,最大化利用硬件资源,提升收集效率
2. 分代收集,虽然 G1可以独立管理整个 Heap,但是它还是保留了分代的概念,实际上,在分区时,这些区域(regions)被映射为逻辑上的 Eden, Survivor, 和 old generation(老年代)空间,使其有目的的收集特定区域的内存。
title
3. 空间整合,G1回收内存时,是将某个或多个区域的存活对象拷贝至其他空区域,同时释放被拷贝的内存区域,这种方式在整体上看是标记-整理,在局部看(两个 Region 之间)是复制算法,所以不会产生内存碎片
4. 可预测的停顿时间

内存分配策略

  1. 对象优先在 Eden 区分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判断。并不是新生代对象的年龄一定要达到某个值,才会进入老年代。Survivor空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,那么年龄等于或大于该年龄的对象就直接进入老年代,无须等待设置的年龄
  5. 空间分配担保
展开阅读全文

没有更多推荐了,返回首页