垃圾收集器
如果说收集算法是内存回收的方法论, 那垃圾收集器就是内存回收的实践者。《Java虚拟机规范》 中对垃圾收集器应该如何实现并没有做出任何规定, 因此不同的厂商、 不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别, 不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。
1.Serial收集器
Serial收集器是最基础、 历史最悠久的收集器, 曾经(在JDK 1.3.1之前) 是HotSpot虚拟机新生代收集器的唯一选择。大家只看名字就能够猜到, 这个收集器是一个单线程工作的收集器, 但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作, 更重要的是强调在它进行垃圾收集时, 必须暂停其他所有工作线程, 直到它收集结束。
“Stop The World”这个词语也许听起来很酷, 但这项工作是由虚拟机在后台自动发起和自动完成的, 在用户不可知、 不可控的情况下把用户的正常工作的线程全部停掉, 这对很多应用来说都是不能接受的。读者不妨试想一下, 要是你的电脑每运行一个小时就会暂停响应五分钟, 你会有什么样的心情?
下图体现了Serial/Serial Old收集器的运行过程 :
对于“Stop The World”带给用户的恶劣体验, 早期HotSpot虚拟机的设计者们表示完全理解, 但也同时表示非常委屈:“你妈妈在给你打扫房间的时候, 肯定也会让你老老实实地在椅子上或者房间外待着, 如果她一边打扫, 你一边乱扔纸屑, 这房间还能打扫完?”这确实是一个合情合理的矛盾, 虽然垃圾收集这项工作听起来和打扫房间属于一个工种, 但实际上肯定还要比打扫房间复杂得多!
从JDK 1.3开始, 一直到现在最新的JDK 13, HotSpot虚拟机开发团队为消除或者降低用户线程因垃圾收集而导致停顿的努力一直持续进行着, 从Serial收集器到Parallel收集器, 再到Concurrent Mark Sweep(CMS) 和Garbage First(G1) 收集器, 最终至现在垃圾收集器的最前沿成果Shenandoah和ZGC 等, 我们看到了一个个越来越构思精巧, 越来越优秀, 也越来越复杂的垃圾收集器不断涌现, 用户线程的停顿时间在持续缩短, 但是仍然没有办法彻底消除(这里不去讨论RTSJ中的收集器) , 探索更优秀垃圾收集器的工作仍在继续。
对于单核处理器或处理器核心数较少的环境来说, Serial收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中, 分配给虚拟机管理的内存一般来说并不会特别大, 收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存, 桌面应用甚少超过这个容量) , 垃圾收集的停顿时间完全可以控制在十几、 几十毫秒, 最多一百多毫秒以内, 只要不是频繁发生收集, 这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
2.ParNew收集器
在过去多年里,假设没有最新的G1垃圾回收器的话,通常线上系统都是ParNew垃圾回收器作为新生代的垃圾回收器,当然现在即使有了G1,其实很多线上系统还是用的ParNew。
我们运行在服务器上的Java系统,其实可以充分利用服务器的多核CPU资源的优势,比如通常服务器配置为4核CPU,如果我们使用Serial收集器单线程进行垃圾回收,是没有充分利用我们的CPU资源的。如下图:
根据上图我们也可以看出,当系统程序停止运行后,仅一个垃圾回收线程在工作,这个时候的多核CPU资源没有得到充分利用,因此我们的ParNew垃圾回收器主打的就是多线程垃圾回收机制,除了同时使用多条线程进行垃圾收集之外, 其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure等) 、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一致, 在实现上这两种收集器也共用了相当多的代码。
ParNew收集器的运行图如下:
ParNew收集器一旦在合适的时机执行Minor GC的时候,就会把系统程序的工作线程全部停掉,禁止程序继续运行创建新的对象,然后通过分配多个线程(理论上4核CPU可以支持4个垃圾回收线程并行执行)进行垃圾回收工作,提升性能,如下图:
当我们开发时想要系统指定使用ParNew垃圾回收器,通过直接指定JVM参数:“-XX:+UseParNewGC”即可。默认情况下,垃圾回收的线程数量跟我们的CPU的核数是一致的,比如我们线上机器是8核CPU,那么我们垃圾回收器对应的线程数量也可以达到8个。当然我们也可以手动修改线程数量:“-XX:ParallelGCThreads”参数即可。(建议一般不要修改,除非涉及到一些内存优化,后续我们再结合案例讲解)
小结
本篇文章先给大家介绍下我们的两个基础核心垃圾收集器,后续将继续为大家介绍CMS垃圾收集器以及G1收集器的工作原理。