GC
一、垃圾回收
释放垃圾占用的空间,防止内存溢出或内存泄露。为了有效的使用内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收
二、垃圾判定
垃圾:没有任何引用指向的对象,称为垃圾
2.1 垃圾判定算法
- 引用计算算法
- 可达性分析算法
2.1.1 引用计数算法
通过在对象头中分配一个空间保存该对象被引用的次数(Reference Count)。如果该对象被其他对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。
- 优点
- 引用计数收集器可以很快的执行,交织在程序运行中
- 对程序需要不被长时间打断的实时环境比较有利
- 缺点
- 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不能为0
2.1.2 可达性分析算法
通过一系列的成为“GC Rootts”的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。此算法解决了循环引用的问题
在Java语言中,可作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 字符串常量池里的引用
- 本地方法栈中JNI(Native方法)引用的对象
- synchronized锁对象
- Class对象
2.2 Java对象引用
Java中对象引用
- 强引用(Strong Reference)
- 垃圾收集器永远不会回收存活的强引用对象。类似"Object obj = new Object()"这类的引用
- 软引用(Soft Reference)
- 还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收
- 弱引用(Weak Reference)
- 当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用(Phantom Reference)
- 无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
这四种引用强度一次逐渐减弱
三、Java垃圾回收
Java中垃圾回收主要时针对堆中内存进行回收
从次数上来说
- 频发回收年轻代
- 较少回收老年代
- 基本不动永久代(元空间)
3.1 垃圾回收算法
常见垃圾回收算法
- 标记-清除算法(Mark-Sweep)
- 标记-整理算法(Mark-Compact)
- 复制算法(Copying)
- 分代手机算法(Generation Collection)
3.1.1 标记-清除算法(Mark-Sweep)
第一步(标记),利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。
第二部(清除),我们再遍历一遍,把所有"垃圾"对象所占的空间直接清空即可
特点
- 实现简单
- 容易产生碎片,需要记录可用空间
3.1.2 标记-整理算法(Mark-Compact)
第一步(标记):利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记
第二部(整理):把所有存活对象堆到同一个地方,这样就没有内存碎片了
特点
- 适合存活对象多,垃圾少的情况
- 需要整理的过程,下次直接使用指针碰撞分配空间
3.1.3 复制算法(Copying)
将内存按照容量划分为大小相等的两块,每次只使用其中一块。当一块用完了,就将还活着的对象复制到另一块上,然后再把使用过的内存空间一次性清理掉
特点:
- 简单
- 不会产生碎片
- 内存利用率太低,只用了一半
3.1.4 分代收集算法
当前商业虚拟机的垃圾收集都采用”分代收集“(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不同的垃圾收集算法
Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
- 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用”标记-清除“或者”标记-整理“算法来进行回收
Minor GC
- 新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂
Major GC/Full GC
- 指发生在老年代和永久代的GC
3.2 垃圾收集器
衡量垃圾收集器的三项指标
-
内存占用(Footprint)
- 垃圾收集器回收垃圾时的内存占用
-
吞吐量(Throughput)
- 用户代码运行时间/(用户代码运行时间+GC时间)
-
延迟、响应时间(Latency)
- 垃圾回收时每次暂停时间(Stop The World)
3.2.1 Serial 收集器(串行收集器)
Serial收集器是最基本,发展历史最悠久的收集器,曾经是虚拟机新生代收集的唯一选择。这是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
”Stop The World“这项工作实际上是由虚拟机在后台自动发起和自动完成的,用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的
3.2.2 ParNew收集器
ParNew 收集器其实就是Serial收集器的多线程版本
- 并行(Parallel)
- 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
- 并发(Concurrent)
- 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
3.2.3 Parallel Scavenge收集器
- Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
- 所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%
3.2.4 Serial Old收集器
- Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法
3.2.5 Parallel Old收集器
- Parallel Old是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一致处于比较尴尬的状态
- 原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)
3.2.6 CMS收集器
- CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
- 尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求
- CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面集中收集器来说更复杂一些,整个过程分为4个步骤,包括
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
3.2.7 G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。
它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留由新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合
3.2.8 其他收集器
3.2.9 ZGC、Epsilon、Shenandoah
- Java11引入了ZGC,宣称暂停时间不超过10ms,支持4TB,JDK13到了16TB,即使是TB级内存也只停顿1-10ms
- Java11还引入一个什么都不做的垃圾收集器(A NoOp Garbage Collector)Epsilon收集器。开发一个处理内存粉喷但不实现任何实际内存回收机制的GC,一旦可用堆内存用完,JVM就会退出。主要用途性能测试,内存压力测试
- JDK12新增的一个名为Shenandoah的GC算法,它的evacuation阶段工作能通过与正在运行中Java工作线程同时进行(即并发,concurrent),从而减少GC的停顿时间