先回顾啥是JVM:
引用:
强引用(Strong Reference)
•默认的赋值语句可以生成一个强引用
•GC时不会被释放
软引用(Soft Reference)
•仅被java.lang.ref.SoftReference引用
•JVM内存不足时,会被释放(FGC)
弱引用(Weak Reference)
•仅被java.lang.ref.WeakReference引用
•GC时被释放
虚引用(Phantom Reference)
•仅被java.lang.ref.PhantomReference引用
•目的不是为了释放内存,而是为了跟踪内存释放,可用于替代finalize函数
工具:
1、什么是GC
GC,全称是 Garbage Collection
(垃圾收集)或者 Garbage Collector
(垃圾收集器)。
研究 GC 的主要原因是 GC 的过程会有 Stop The World
(STW)的情况发生,即此时用户线程会停止工作,如果 STW 的时间过长,则应用的可用性、实时性等就下降的很厉害。
GC
主要解决如下3个问题:
- 如何找到垃圾?
- 如何回收垃圾?
- 何时回收垃圾?
1.1如何找到垃圾?
所谓垃圾,指的是不再被使用(引用)的对象。Java 的对象都是在堆(Heap)上创建的,我们这里默认也只讨论堆。那么现在问题就变为如何判定一个对象是否还有被引用,思路主要有如下两种:
- 引用计数法,即在对象被引用时加1,去除引用时减1,如果引用值为0,即表明该对象可回收了。
- 可达性分析法,即通过遍历已知的存活对象(GC Roots)的引用链来标记出所有存活对象
方法1简单粗暴效率高,但准确度不行,尤其是面对互相引用的垃圾对象时无能为力。
方法2是目前常用的方法,这里有一个关键是 GC Roots
,它是判定的源头,感兴趣的同学可以自己去研究下,这里就不展开讲了。
1.2 如何回收垃圾?
垃圾找到了,该怎么回收呢?看起来似乎是个很傻的问题。直接收起来扔掉不就好了?!对应到程序的操作,就是直接将这些对象占用的空间标记为空闲不就好了吗?那我们就来看一下这个基础的回收算法:标记-清除(Mark-Sweep)算法。
1.2.1 标记-清除 算法(Mark Sweep)
该算法很简单,使用通过可达性分析分析方法标记出垃圾,然后直接回收掉垃圾区域。它的一个显著问题是一段时间后,内存会出现大量碎片,导致虽然碎片总和很大,但无法满足一个大对象的内存申请,从而导致 OOM,而过多的内存碎片(需要类似链表的数据结构维护),也会导致标记和清除的操作成本高,效率低下,如下图所示:
1.2.2 复制算法(Copying)
为了解决上面算法的效率问题,有人提出了复制算法。它将可用内存一分为二,每次只用一块,当这一块内存不够用时,便触发 GC,将当前存活对象复制(Copy)到另一块上,以此往复。这种算法高效的原因在于分配内存时只需要将指针后移,不需要维护链表等。但它最大的问题是对内存的浪费,使用率只有 50%。
但这种算法在一种情况下会很高效:Java 对象的存活时间极短。据 IBM 研究,Java 对象高达 98% 是朝生夕死的,这也意味着每次 GC 可以回收大部分的内存,需要复制的数据量也很小,这样它的执行效率就会很高。
1.2.3 标记-整理算法(Mark Compact)
该算法解决了第1中算法的内存碎片问题,它会在回收阶段将所有内存做整理。但它的问题也在于增加了整理阶段,也就增加了 GC 的时间。
如下图所示:
1.2.4 分代收集算法(Generation Collection)
既然大部分 Java 对象是朝生夕死的,那么我们将内存按照 Java 生存时间分为 新生代(Young)
和 老年代(Old)
,前者存放短命僧,后者存放长寿佛,当然长寿佛也是由短命僧升级上来的。然后针对两者可以采用不同的回收算法,比如对于新生代
采用复制算法会比较高效,而对老年代
可以采用标记-清除或者标记-整理算法。这种算法也是最常用的。JVM Heap 分代后的划分一般如下所示,新生代一般会分为 Eden、Survivor0、Survivor1区,便于使用复制算法。
将内存分代后的 GC 过程一般类似下图所示:
- 对象一般都是先在
Eden
区创建 - 当
Eden
区满,触发 Young GC,此时将Eden
中还存活的对象复制到S0
中,并清空Eden
区后继续为新的对象分配内存 - 当
Eden
区再次满后,触发又一次的 Young GC,此时会将Eden
和S0
中存活的对象复制到S1
中,然后清空Eden
和S0
后继续为新的对象分配内存 - 每经过一次 Young GC,存活下来的对象都会将自己存活次数加1,当达到一定次数后,会随着一次 Young GC 晋升到
Old
区 -
Old
区也会在合适的时机进行自己的 GC
1.2.5 常见的垃圾收集器
前面我们讲了众多的垃圾收集算法,那么其具体的实现就是垃圾收集器,也是我们实际使用中会具体用到的。现代的垃圾收集机制基本都是分代收集算法,而 Young
与 Old
区分别有不同的垃圾收集器,简单总结如下图:
从上图我们可以看到 Young
与 Old
区有不同的垃圾收集器,实际使用时会搭配使用,也就是上图中两两连线的收集器是可以搭配使用的。这些垃圾收集器按照运行原理大概可以分为如下几类:
- Serial GC,串行,单线程的收集器,运行 GC 时需要停止所有的用户线程,且只有一个 GC 线程
- Parallel GC,并行,多线程的收集器,是 Serial 的多线程版,运行时也需要停止所有用户线程,但同时运行多个 GC 线程,所以效率高一些
- Concurrent GC,并发,多线程收集器,GC 分多阶段执行,部分阶段允许用户线程与 GC 线程同时运行,这也就是并发的意思,大家要和并行做一个区分。
- 其他
我们下面简单看一下他们的运行机制。
1.2.5.1 Serial GC
该类 Young区
的为 Serial GC
,Old区
的为Serial Old GC
。执行大致如下所示:
1.2.5.2 Parallel GC
该类Young 区
的有 ParNew
和 Parallel Scavenge
,Old 区
的有Parallel Old
。其运行机制如下,相比 Serial GC ,其最大特点在于 GC 线程是并行的,效率高很多:
1.2.5.3 Concurrent Mark-Sweep GC
该类目前只是针对 Old 区
,最常见就是CMS GC
,它的执行分为多个阶段,只有部分阶段需要停止用户进程,这里不详细介绍了,感兴趣可以去找相关文章来看,大体执行如下:
1.2.5.4 其他
目前最新的 GC 有G1GC
和ZGC
,其运行机制与上述均不相同,虽然他们也是分代收集算法,但会把 Heap 分成多个 region 来做处理,这里不展开讲。
参考资料
- Java Hotspot G1 GC的一些关键技术(https://mp.weixin.qq.com/s/4ufdCXCwO56WAJnzng_-ow)
- Understanding Java Garbage Collection(https://www.cubrid.org/blog/understanding-java-garbage-collection)
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》