说一下
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
中。