尚硅谷JavaEE技术之JUC&&JVM
因为在之前的”JavaGuide自学记录3-并发编程”和”Java八股文-并发学习记录”中已经记载了常见的synchronized,ReentrantLock,线程池,CAS,AQS,volatile,ThreadLocal,concurrentHashMap等常见热点知识。所以本篇文章只根据视频和笔记做简单的边缘的补充。
线程间通信
虚假唤醒
对于下面的类ShareDataOne:
class ShareDataOne {
private Integer number = 0;
/**
* 增加1
*/
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
/**
* 减少1
*/
public synchronized void decrement() throws InterruptedException {
// 1. 判断
if (number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
}
如果是4个线程,两个加,两个减,就会出现不是0和1的情况。因为消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头
解决办法就是将if换为while
// 1. 判断
while (number != 0) {
this.wait();
}
用Condition通信
condition是ReentrantLock里的,所以就不用synchronized了,但是还是要用while替换掉if。具体的ShareDataOne类如下。
class ShareDataOne {
private Integer number = 0;
final Lock lock = new ReentrantLock(); // 初始化lock锁
final Condition condition = lock.newCondition(); // 初始化condition对象
/**
* 增加1
*/
public void increment() throws InterruptedException {
lock.lock(); // 加锁
try {
// 1. 判断
while (number != 0) {
// this.wait();
condition.await();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
// this.notifyAll();
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 减少1
*/
public void decrement() throws InterruptedException {
lock.lock();
try {
// 1. 判断
while (number != 1) {
// this.wait();
condition.await();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
//this.notifyAll();
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
定制化调用通信
案例:
多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
分析实现方式:
- 有一个锁Lock,3把钥匙Condition
- 有顺序通知(切换线程),需要有标识位
- 判断标志位
- 输出线程名 + 内容
- 修改标识符,通知下一个
class ShareDataTwo {
private Integer flag = 1; // 线程标识位,通过它区分线程切换
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
死锁问题排查
jps(JVM Process Status Tool):显示当前系统的 Java 进程情况
jstack:java虚拟机自带的一种堆栈跟踪工具,查看Java进程内的线程堆栈信息,使用jstack命令查看线程堆栈信息时可能会看到的线程的几种状态:
- NEW 未启动的。
- RUNNABLE 运行中。
- BLOCKED 受阻塞并等待监视器锁。
- WATING 等待另一个线程执行特定操作。
- TIMED_WATING 有时限的等待另一个线程的特定操作。
- TERMINATED 已退出的。
如果出现:Found one Java-level deadlock 代表死锁
并发容器类
ArrayList不是并发安全的,因为他的add方法没有加上synchronized也没有Lock。
Vector和SynchronizedList是线程安全的,但是他们也有一些缺点:
vector:内存消耗比较大,适合一次增量比较大的情况
SynchronizedList:迭代器涉及的代码没有加上线程同步代码
CopyOnWrite容器
CopyOnWrite容器(简称COW容器)即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite并发容器用于读多写少的并发场景。比如:白名单,黑名单。假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。
缺点:
- 内存占用问题。写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
- 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
并发容器和同步容器
同步容器可以简单地理解为通过synchronized来实现同步的容器。同步容器会导致多个线程中对容器方法调用的串行执行,降低并发性,因为它们都是以容器自身对象为锁。在并发下进行迭代的读和写时并不是线程安全的。如:Vector、Stack、HashTable、Collections类的静态工厂方法创建的类(如Collections.synchronizedList)
并发容器是针对多个线程并发访问而设计的,在jdk5.0引入了concurrent包,其中提供了很多并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等。
ConcurrentHashMap:内部采用Segment结构,进行两次Hash进行定位,写时只对Segment加锁
CopyOnWriteArrayList:CopyOnWrite写时复制一份新的,在新的上面修改,然后把引用指向新的。只能实现数据的最终一致性,非实时一致的;代替List,适用于读操作为主的情况
同步容器与并发容器都为多线程并发访问提供了合适的线程安全,不过并发容器的可扩展性更高。
JUC强大的辅助类
JUC的多线程辅助类非常多,这里我们介绍三个:
- CountDownLatch(倒计数器)
- CyclicBarrier(循环栅栏)
- Semaphore(信号量)
CountDownLatch
例如:在手机上安装一个应用程序,假如需要5个子进程检查服务授权,那么主进程会维护一个计数器,初始计数就是5。用户每同意一个授权该计数器减1,当计数减为0时,主进程才启动,否则就只有阻塞等待了。
常用的就下面几个方法:
new CountDownLatch(int count) //实例化一个倒计数器,count指定初始计数
countDown() // 每调用一次,计数减一
await() //等待,当计数减到0时,阻塞线程(可以是一个,也可以是多个)并行执行
案例:6个同学陆续离开教室后值班同学才可以关门。
public class CountDownLatchDemo {
/**
* main方法也是一个进程,在这里是主进程,即上锁的同学
*
* @param args
*/
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,初始计数为6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// 每个同学墨迹几秒钟
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + " 同学出门了");
// 调用countDown()计算减1
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
// 调用计算器的await方法,等待6位同学都出来
countDownLatch.await();
System.out.println("值班同学锁门了");
}
}
面试:CountDownLatch 与 join 方法的区别
调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕。而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runnable 到线程池这时候就没有办法在调用线程的 join 方法了(因为你没有这个内部线程的直接引用,解决方法是使用Future或countDownLatch),countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
CyclicBarrier
常用方法:
- CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,barrierAction一个可选的Runnable命令,该命令只在每个屏障点运行一次,可以在执行后续业务之前共享状态。该操作由最后一个进入屏障点的线程执行。
- CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
- await() 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,直到所有线程都到达屏障点,当前线程才会被唤醒。
案例:组队打boss过关卡游戏。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println(Thread.currentThread().getName() + " 过关了");
});
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + " 开始第一关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第二关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第三关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
输出:
1 开始第一关
2 开始第一关
0 开始第一关
0 开始打boss
2 开始打boss
1 开始打boss
1 过关了
0 开始第二关
2 开始第二关
1 开始第二关
1 开始打boss
0 开始打boss
2 开始打boss
2 过关了
1 开始第三关
2 开始第三关
0 开始第三关
1 开始打boss
0 开始打boss
2 开始打boss
2 过关了
注意:所有的”过关了”都是由最后到达await方法的线程执行打印的
面试:CyclicBarrier和CountDownLatch的区别?
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
Semaphore
信号量,主要是占有资源操作,和,释放资源操作。
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的:
- 多个共享资源的互斥使用。
- 用于并发线程数的控制。保护一个关键部分不要一次输入超过N个线程。
案例:6辆车抢占3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
// 初始化信号量,3个车位
Semaphore semaphore = new Semaphore(3);
// 6个线程,模拟6辆车
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// 抢占一个停车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 抢到了一个停车位!!");
// 停一会儿车
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println(Thread.currentThread().getName() + " 离开停车位!!");
// 开走,释放一个停车位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
打印结果:
0 抢到了一个停车位!!
1 抢到了一个停车位!!
2 抢到了一个停车位!!
1 离开停车位!!
3 抢到了一个停车位!!
2 离开停车位!!
4 抢到了一个停车位!!
0 离开停车位!!
5 抢到了一个停车位!!
5 离开停车位!!
3 离开停车位!!
4 离开停车位!!
Callable接口
Thread类、Runnable接口使得多线程编程简单直接。
但Thread类和Runnable接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。不能声明抛出检查型异常则更麻烦一些。
**public void run()**方法规范意味着你必须捕获并处理检查型异常。即使你小心捕获异常,也不能保证这个类(Runnable对象)的所有使用者都读取异常信息。
以上两个问题现在都得到了解决。从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
使用步骤:
创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
创建Callable的实例。
实例化FutureTask类,参数为Callable接口实现类的对象,FutureTask封装了Callable对象call()方法的返回值
创建多线程Thread对象来启动线程,参数为FutureTask对象。
通过FutureTask类的对象的get()方法来获取线程结束后的返回值
/**
* 1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
*/
class MyCallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "执行了!");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 2. 创建Callable的实例,并用FutureTask类来包装Callable对象
// 3. 创建FutureTask对象,需要一个Callable类型的参数
FutureTask task = new FutureTask<Integer>(new MyCallableThread());
// 4. 创建多线程,由于FutureTask的本质是Runnable的实现类,所以第一个参数可以直接使用task
new Thread(task, "threadName").start();
//new Thread(task, "threadName2").start();
/*while (!task.isDone()) {
System.out.println("wait...");
}*/
System.out.println(task.get());
System.out.println(Thread.currentThread().getName() + " over!");
}
}
FutureTask:未来的任务,用它就干一件事,异步调用。通常用它解决耗时任务,挂起堵塞问题。
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask仅在call方法完成时才能get结果;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
注意:
为了防止主线程阻塞,建议get方法放到最后
只计算一次,FutureTask会复用之前计算过得结果
创建多个线程,会怎样?
运行结果:依然只有一个就是threadName。
如果想打印threadName2的结果,即不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可。
callable接口和runnable接口的区别
相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程
不同点:
- 具体方法不同:一个是run,一个是call
- Runnable没有返回值;Callable可以返回执行结果,是个泛型
- Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛
- 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。 Runnable只提供了一个run方法,太简单了。
获得多线程的方法
传统的是继承thread类和实现runnable接口
java5以后又有实现callable接口和java的线程池获得
阻塞队列-BlockingQueue
被阻塞的情况主要有如下两种:
当队列满了的时候进行入队列操作
当队列空了的时候进行出队列操作
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
认识BlockingQueue
java.util.concurrent 包里的 BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection。
BlockingQueue接口主要有以下7个实现类:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
BlockingQueue接口有以下几个方法:
它的方法可以分成以下4类:
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
抛出异常
add正常执行返回true,element(不删除)和remove返回阻塞队列中的第一个元素
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特定值
插入方法,成功ture失败false
移除方法,成功返回出队列的元素,队列里没有就返回null
检查方法,成功返回队列中的元素,没有返回null
一直阻塞
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
返回一个特定值以告知该操作是否成功(典型的是 true / false)。
垃圾回收器比较
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
整堆收集器: G1
垃圾回收器选择策略 :
客户端程序 : Serial + Serial Old;
吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;
响应时间优先的服务端程序 :ParNew + CMS。
G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。
ZGC收集器(-XX:+UseZGC)
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC是Azul System公司开发的 C4(Concurrent Continuously Compacting Collector)收集器
ZGC的目标主要有4个:
①支持TB量级的堆。
②最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右, Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能 做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
③奠定未来GC特性的基础。
④最糟糕的情况下吞吐量会降低15%。Oracle官方提到了它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是 10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。
不分代(暂时)
单代,即ZGC「没有分代」。我们知道以前的垃圾回收器之所以分代,是因为源于「大部分对象朝生夕死」的假 设,事实上大部分系统的对象分配行为也确实符合这个假设。
ZGC内存布局
ZGC收集器是一款基于Region内存布局的,暂时不设分代的,使用了可并发的标记-整 理算法, 以低延迟为首要目标的一款垃圾收集器。 ZGC的Region可以具有如下图所示的大、 中、 小三类容量:
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或 以上的大对象。 每个大型Region中 只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型 Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂。
ZGC运作过程
ZGC的运作过程大致可划分为以下四个大的阶段:
①并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记 (Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象 上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
②并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收 集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
③并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存 活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象 到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并 发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指 针的“自愈”(Self-Healing)能力。
④并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节 省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
指定使用垃圾回收器(了解)
-XX:+UseSerialGC 年轻代和老年代都用串行收集器
-XX:+UseParNewGC 年轻代使用ParNew,老年代使用 Serial Old
-XX:+UseParallelGC 年轻代使用Paraller Scavenge,老年代使用Serial Old
-XX:+UseParallelOldGC 新生代Paraller Scavenge,老年代使用Paraller Old
-XX:+UseConcMarkSweepGC,表示年轻代使用ParNew,老年代的用CMS + Serial Old
-XX:+UseG1GC 使用G1垃圾回收器
-XX:+UseZGC 使用ZGC垃圾回收器(jdk11以后支持)
JDK 1.8默认使用 Parallel(年轻代和老年代都是)
JDK 1.9默认使用 G1
代码中查看使用的垃圾收集器:
List<GarbageCollectorMXBean> l = ManagementFactory.getGarbageCollectorMXBeans();
for(GarbageCollectorMXBean b : l) {
System.out.println(b.getName());
}