先回顾啥是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高级特性与最佳实践》
最新评论