说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了
7 种作用于不同分代的收集器,其中用于回收新生代的收集器包括 Serial 、 PraNew 、 Parallel
Scavenge ,回收老年代的收集器包括 Serial Old 、 Parallel Old 、 CMS ,还有用于回收整个 Java 堆的
G1 收集器。不同收集器之间的连线表示它们可以搭配使用。 Serial 收集器(复制算法 ): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew 收集器 ( 复制算法 ): 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核
CPU 环境下有着比 Serial 更好的表现;
Parallel Scavenge 收集器 ( 复制算法 ): 新生代并行收集器,追求高吞吐量,高效利用 CPU 。吞
吐量 = 用户线程时间 /( 用户线程时间 +GC 线程时间 ) ,高吞吐量可以高效率的利用 CPU 时间,尽
快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old 收集器 ( 标记 - 整理算法 ): 老年代单线程收集器, Serial 收集器的老年代版本;
Parallel Old 收集器 ( 标记 - 整理算法 ) : 老年代并行收集器,吞吐量优先, Parallel Scavenge 收
集器的老年代版本;
CMS(Concurrent Mark Sweep) 收集器(标记 - 清除算法): 老年代并行收集器,以获取最短回
收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。
G1(Garbage First) 收集器 ( 标记 - 整理算法 ) : Java 堆并行收集器, G1 收集器是 JDK1.7 提供的一
个新收集器, G1 收集器基于 “ 标记 - 整理 ” 算法实现,也就是说不会产生内存碎片。此外, G1 收
集器不同于之前的收集器的一个重要特点是: G1 回收的范围是整个 Java 堆 ( 包括新生代,老年
代 ) ,而前六种收集器回收的范围仅限于新生代或老年代。
ZGC ( Z Garbage Collector )是一款由 Oracle 公司研发的,以低延迟为首要目标的一款垃圾收
集器。它是基于动态 Region 内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内
存多重映射等技术来实现可并发的标记 - 整理算法的收集器。在 JDK 11 新加入,还在实验阶
段,主要特点是:回收 TB 级内存(最大 4T ),停顿时间不超过 10ms 。 优点 :低停顿,高吞吐
量, ZGC 收集过程中额外耗费的内存小。 缺点 :浮动垃圾
目前使用的非常少,真正普及还是需要写时间的。
新生代收集器 : Serial 、 ParNew 、 Parallel Scavenge
老年代收集器 : CMS 、 Serial Old 、 Parallel Old
整堆收集器 : G1 , ZGC 。
如何选择垃圾收集器?
1. 如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。
参数: - XX:+UseSerialGC 。
2. 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合
适的,这时候启用一些并行收集器没有任何收益。
参数: - XX:+UseSerialGC 。
3. 如果你的应用是 “ 吞吐量 ” 优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集
器是比较好的。
参数: - XX:+UseParallelGC 。 4. 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失
败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它
需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
参数:
- XX:+UseConcMarkSweepGC 、
- XX:+UseG1GC 、
- XX:+UseZGC 等。
从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。选择性其实就集
中在 CMS 、 G1 、 ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。
notify() 和 notifyAll() 有什么区别?
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
使用 notifyall, 可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤
醒一个。
wait() 应配合 while 循环使用,不应使用 if ,务必在 wait() 调用前后都检查条件,如果不满足,必须调
用 notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。
notify() 是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致
死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事
项,如果唤醒的线程无法正确处理,务必确保继续 notify() 下一个线程,并且自身需要重新回到
WaitSet 中。