1. JMM

1. volatile是什么

volatile是JVM提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排(保证有序性)

2. JMM内存模型之可见性

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
image.png

可见性

通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题

3. 可见性的代码验证说明

  1. import java.util.concurrent.TimeUnit;
  2. /**
  3. * 假设是主物理内存
  4. */
  5. class MyData {
  6. //volatile int number = 0;
  7. int number = 0;
  8. public void addTo60() {
  9. this.number = 60;
  10. }
  11. }
  12. /**
  13. * 验证volatile的可见性
  14. * 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
  15. */
  16. public class VolatileDemo {
  17. public static void main(String args []) {
  18. // 资源类
  19. MyData myData = new MyData();
  20. // AAA线程 实现了Runnable接口的,lambda表达式
  21. new Thread(() -> {
  22. System.out.println(Thread.currentThread().getName() + "\t come in");
  23. // 线程睡眠3秒,假设在进行运算
  24. try {
  25. TimeUnit.SECONDS.sleep(3);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. // 修改number的值
  30. myData.addTo60();
  31. // 输出修改后的值
  32. System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
  33. }, "AAA").start();
  34. // main线程就一直在这里等待循环,直到number的值不等于零
  35. while(myData.number == 0) {}
  36. // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
  37. // 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
  38. System.out.println(Thread.currentThread().getName() + "\t mission is over");
  39. }
  40. }

由于没有volatile修饰MyData类的成员变量number,main线程将会卡在while(myData.number == 0) {},不能正常结束。若想正确结束,用volatile修饰MyData类的成员变量number吧。

volatile类比

没有volatile修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,这就与老师的课件不同了。

有volatile修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,并且与老师分享,老师认可A同学修改后的课件,并用它来作下一届的课件。

4. volatile不保证原子性

原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
volatile不保证原子性案例演示:

  1. class MyData2 {
  2. /**
  3. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
  4. */
  5. volatile int number = 0;
  6. public void addPlusPlus() {
  7. number ++;
  8. }
  9. }
  10. public class VolatileAtomicityDemo {
  11. public static void main(String[] args) {
  12. MyData2 myData = new MyData2();
  13. // 创建10个线程,线程里面进行1000次循环
  14. for (int i = 0; i < 20; i++) {
  15. new Thread(() -> {
  16. // 里面
  17. for (int j = 0; j < 1000; j++) {
  18. myData.addPlusPlus();
  19. }
  20. }, String.valueOf(i)).start();
  21. }
  22. // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
  23. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  24. while(Thread.activeCount() > 2) {
  25. // yield表示不执行
  26. Thread.yield();
  27. }
  28. // 查看最终的值
  29. // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
  30. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
  31. }
  32. }

最后的结果总是小于20000。

5. volatile不保证原子性理论解释

number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
image.png
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。

6. volatile不保证原子性问题解决

可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。

  1. import java.util.concurrent.atomic.AtomicInteger;
  2. class MyData2 {
  3. /**
  4. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
  5. */
  6. volatile int number = 0;
  7. AtomicInteger number2 = new AtomicInteger();
  8. public void addPlusPlus() {
  9. number ++;
  10. }
  11. public void addPlusPlus2() {
  12. number2.getAndIncrement();
  13. }
  14. }
  15. public class VolatileAtomicityDemo {
  16. public static void main(String[] args) {
  17. MyData2 myData = new MyData2();
  18. // 创建10个线程,线程里面进行1000次循环
  19. for (int i = 0; i < 20; i++) {
  20. new Thread(() -> {
  21. // 里面
  22. for (int j = 0; j < 1000; j++) {
  23. myData.addPlusPlus();
  24. myData.addPlusPlus2();
  25. }
  26. }, String.valueOf(i)).start();
  27. }
  28. // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
  29. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  30. while(Thread.activeCount() > 2) {
  31. // yield表示不执行
  32. Thread.yield();
  33. }
  34. // 查看最终的值
  35. // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
  36. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
  37. System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
  38. }
  39. }

输出结果为:

  1. main finally number value: 18766
  2. main finally number2 value: 20000

7. volatile指令重排案例 1

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
image.png
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的 数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

重排案例1

  1. public void mySort{
  2. int x = 11;//语句1
  3. int y = 12;//语句2
  4. × = × + 5;//语句3
  5. y = x * x;//语句4
  6. }

可重排序列:

  • 1234
  • 2134
  • 1324

问题:请问语句4可以重排后变成第一个条吗?答:不能。

重排案例2

int a,b,x,y = 0

线程1 线程2
x = a; y = b;
b = 1; a = 2;


x = 0; y = 0

如果编译器对这段程序代码执行重排优化后,可能出现下列情况:

线程1 线程2
b = 1; a = 2;
x = a; y = b;


x = 2; y = 1

这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

8.volatile指令重排案例 2

观察以下程序:

  1. public class ReSortSeqDemo{
  2. int a = 0;
  3. boolean flag = false;
  4. public void method01(){
  5. a = 1;//语句1
  6. flag = true;//语句2
  7. }
  8. public void method02(){
  9. if(flag){
  10. a = a + 5; //语句3
  11. }
  12. System.out.println("retValue: " + a);//可能是6或1或5或0
  13. }
  14. }

多线程环境中线程交替执行method01()method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

禁止指令重排小总结

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的执行顺序,
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
image.png
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
image.png

线性安全性获得保证

  1. 工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
  2. 对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

9. 单例模式在多线程环境下可能存在安全问题

懒汉单例模式

  1. public class SingletonDemo {
  2. private static SingletonDemo instance = null;
  3. private SingletonDemo () {
  4. System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  5. }
  6. public static SingletonDemo getInstance() {
  7. if(instance == null) {
  8. instance = new SingletonDemo();
  9. }
  10. return instance;
  11. }
  12. public static void main(String[] args) {
  13. // 这里的 == 是比较内存地址
  14. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  15. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  16. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  17. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  18. }
  19. }

输出结果:

  1. main 我是构造方法singletonDemo
  2. true
  3. true
  4. true
  5. true

但是,在多线程环境运行上述代码,能保证单例吗?

  1. public class SingletonDemo {
  2. private static SingletonDemo instance = null;
  3. private SingletonDemo () {
  4. System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  5. }
  6. public static SingletonDemo getInstance() {
  7. if(instance == null) {
  8. instance = new SingletonDemo();
  9. }
  10. return instance;
  11. }
  12. public static void main(String[] args) {
  13. for (int i = 0; i < 10; i++) {
  14. new Thread(() -> {
  15. SingletonDemo.getInstance();
  16. }, String.valueOf(i)).start();
  17. }
  18. }
  19. }

输出结果:

  1. 4 我是构造方法SingletonDemo
  2. 2 我是构造方法SingletonDemo
  3. 5 我是构造方法SingletonDemo
  4. 6 我是构造方法SingletonDemo
  5. 0 我是构造方法SingletonDemo
  6. 3 我是构造方法SingletonDemo
  7. 1 我是构造方法SingletonDemo

显然不能保证单例。
解决方法之一:用synchronized修饰方法getInstance(),但它属重量级同步机制,使用时慎重。

  1. public synchronized static SingletonDemo getInstance() {
  2. if(instance == null) {
  3. instance = new SingletonDemo();
  4. }
  5. return instance;
  6. }

10. 单例模式volatile分析

解决方法之二:DCL(Double Check Lock双端检锁机制)

  1. public class SingletonDemo{
  2. private SingletonDemo(){}
  3. private volatile static SingletonDemo instance = null;
  4. public static SingletonDemo getInstance() {
  5. if(instance == null) {
  6. synchronized(SingletonDemo.class){
  7. if(instance == null){
  8. instance = new SingletonDemo();
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }

DCL中volatile解析

原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化instance = new SingletonDemo();可以分为以下3步完成(伪代码):

  1. memory = allocate(); //1.分配对象内存空间
  2. instance(memory); //2.初始化对象
  3. instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

  1. memory = allocate(); //1.分配对象内存空间
  2. instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
  3. instance(memory);//2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

2. CAS

1. CAS是什么

比较并交换
Compare And Set
示例程序

  1. public class CASDemo{
  2. public static void main(string[] args){
  3. AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
  4. System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
  5. System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get());
  6. }
  7. }

输出结果为

  1. true 2019
  2. false 2019

2. CAS底层原理-上

Cas底层原理?如果知道,谈谈你对UnSafe的理解
atomiclnteger.getAndIncrement();源码

  1. public class AtomicInteger extends Number implements java.io.Serializable {
  2. private static final long serialVersionUID = 6214790243416807050L;
  3. // setup to use Unsafe.compareAndSwapInt for updates
  4. private static final Unsafe unsafe = Unsafe.getUnsafe();
  5. private static final long valueOffset;
  6. static {
  7. try {
  8. valueOffset = unsafe.objectFieldOffset
  9. (AtomicInteger.class.getDeclaredField("value"));
  10. } catch (Exception ex) { throw new Error(ex); }
  11. }
  12. private volatile int value;
  13. /**
  14. * Creates a new AtomicInteger with the given initial value.
  15. *
  16. * @param initialValue the initial value
  17. */
  18. public AtomicInteger(int initialValue) {
  19. value = initialValue;
  20. }
  21. /**
  22. * Creates a new AtomicInteger with initial value {@code 0}.
  23. */
  24. public AtomicInteger() {
  25. }
  26. ...
  27. /**
  28. * Atomically increments by one the current value.
  29. *
  30. * @return the previous value
  31. */
  32. public final int getAndIncrement() {
  33. return unsafe.getAndAddInt(this, valueOffset, 1);
  34. }
  35. ...
  36. }

UnSafe

1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3 变量value用volatile修饰,保证了多线程之间的内存可见性。

CAS是什么

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
image.png

3. CAS底层原理-下

继续上一节

UnSafe.getAndAddInt()源码解释:

  • var1 AtomicInteger对象本身。
  • var2 该对象值得引用地址。
  • var4 需要变动的数量。
  • var5是用过var1,var2找出的主内存中真实的值。
  • 用该对象当前的值与var5比较:
    • 如果相同,更新var5+var4并且返回true,
    • 如果不同,继续取值然后再比较,直到更新完成。

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  1. Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

    底层汇编

    Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。 ```java UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv env, jobject unsafe, jobject obj, jlong offset, jint e, jint x) UnsafeWrapper(“Unsafe_CompareAndSwaplnt”); oop p = JNlHandles::resolve(obj); jint addr = (jint *)index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e))== e; UNSAFE_END //先想办法拿到变量value在内存中的地址。 //通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
  1. <a name="IFcB1"></a>
  2. ### 小结
  3. CAS指令<br />CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。<br />当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
  4. <a name="RXNgX"></a>
  5. ## 4. CAS缺点
  6. **循环时间长开销很大**
  7. ```java
  8. // ursafe.getAndAddInt
  9. public final int getAndAddInt(Object var1, long var2, int var4){
  10. int var5;
  11. do {
  12. var5 = this.getIntVolatile(var1, var2);
  13. }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
  14. return var5;
  15. }

我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
引出来ABA问题

5. ABA问题

ABA问题怎么产生的
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

6. AtomicReference原子引用

  1. import java.util.concurrent.atomic.AtomicReference;
  2. class User{
  3. String userName;
  4. int age;
  5. public User(String userName, int age) {
  6. this.userName = userName;
  7. this.age = age;
  8. }
  9. @Override
  10. public String toString() {
  11. return String.format("User [userName=%s, age=%s]", userName, age);
  12. }
  13. }
  14. public class AtomicReferenceDemo {
  15. public static void main(String[] args){
  16. User z3 = new User( "z3",22);
  17. User li4 = new User("li4" ,25);
  18. AtomicReference<User> atomicReference = new AtomicReference<>();
  19. atomicReference.set(z3);
  20. System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
  21. System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
  22. }
  23. }

输出结果

  1. true User [userName=li4, age=25]
  2. false User [userName=li4, age=25]

7. AtomicStampedReference版本号原子引用

原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。

8. ABA问题的解决

ABA问题程序演示及解决方法演示:

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicReference;
  3. import java.util.concurrent.atomic.AtomicStampedReference;
  4. public class ABADemo {
  5. /**
  6. * 普通的原子引用包装类
  7. */
  8. static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
  9. // 传递两个值,一个是初始值,一个是初始版本号
  10. static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
  11. public static void main(String[] args) {
  12. System.out.println("============以下是ABA问题的产生==========");
  13. new Thread(() -> {
  14. // 把100 改成 101 然后在改成100,也就是ABA
  15. atomicReference.compareAndSet(100, 101);
  16. atomicReference.compareAndSet(101, 100);
  17. }, "t1").start();
  18. new Thread(() -> {
  19. try {
  20. // 睡眠一秒,保证t1线程,完成了ABA操作
  21. TimeUnit.SECONDS.sleep(1);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. // 把100 改成 101 然后在改成100,也就是ABA
  26. System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
  27. }, "t2").start();
  28. /
  29. try {
  30. TimeUnit.SECONDS.sleep(2);
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. /
  35. System.out.println("============以下是ABA问题的解决==========");
  36. new Thread(() -> {
  37. // 获取版本号
  38. int stamp = atomicStampedReference.getStamp();
  39. System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
  40. // 暂停t3一秒钟
  41. try {
  42. TimeUnit.SECONDS.sleep(1);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. // 传入4个值,期望值,更新值,期望版本号,更新版本号
  47. atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
  48. atomicStampedReference.getStamp() + 1);
  49. System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
  50. atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
  51. atomicStampedReference.getStamp() + 1);
  52. System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
  53. }, "t3").start();
  54. new Thread(() -> {
  55. // 获取版本号
  56. int stamp = atomicStampedReference.getStamp();
  57. System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
  58. // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
  59. try {
  60. TimeUnit.SECONDS.sleep(3);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
  65. System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
  66. + atomicStampedReference.getStamp());
  67. System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
  68. }, "t4").start();
  69. }
  70. }

输出结果

  1. ============以下是ABA问题的产生==========
  2. true 2019
  3. ============以下是ABA问题的解决==========
  4. t3 第一次版本号1
  5. t4 第一次版本号1
  6. t3 第二次版本号2
  7. t3 第三次版本号3
  8. t4 修改成功否:false 当前最新实际版本号:3
  9. t4 当前实际最新值100

3. 集合

1. 集合类不安全之并发修改异常

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.List;
  4. import java.util.UUID;
  5. import java.util.Vector;
  6. public class ArrayListNotSafeDemo {
  7. public static void main(String[] args) {
  8. List<String> list = new ArrayList<>();
  9. //List<String> list = new Vector<>();
  10. //List<String> list = Collections.synchronizedList(new ArrayList<>());
  11. for (int i = 0; i < 30; i++) {
  12. new Thread(() -> {
  13. list.add(UUID.randomUUID().toString().substring(0, 8));
  14. System.out.println(list);
  15. }, String.valueOf(i)).start();
  16. }
  17. }
  18. }

上述程序会抛java.util.ConcurrentModificationException
解决方法之一:Vector
解决方法之二:Collections.synchronizedList()

2. 集合类不安全之写时复制

上一节程序导致抛java.util.ConcurrentModificationException的原因解析
先观察下抛错打印栈堆信息:

  1. java.util.ConcurrentModificationException
  2. at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  3. at java.util.ArrayList$Itr.next(ArrayList.java:859)
  4. at java.util.AbstractCollection.toString(AbstractCollection.java:461)
  5. at java.lang.String.valueOf(String.java:2994)
  6. at java.io.PrintStream.println(PrintStream.java:821)
  7. at com.lun.collection.ArrayListNotSafeDemo.lambda$0(ArrayListNotSafeDemo.java:20)
  8. at java.lang.Thread.run(Thread.java:748)

可看出toString(),Itr.next(),Itr.checkForComodification()后抛出异常,那么看看它们next(),checkForComodification()源码:

  1. public class ArrayList<E> extends AbstractList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
  3. ...
  4. private class Itr implements Iterator<E> {
  5. int cursor; // index of next element to return
  6. int lastRet = -1; // index of last element returned; -1 if no such
  7. int expectedModCount = modCount;//modCount在AbstractList类声明
  8. Itr() {}
  9. ...
  10. @SuppressWarnings("unchecked")
  11. public E next() {
  12. checkForComodification();
  13. ...
  14. }
  15. final void checkForComodification() {
  16. if (modCount != expectedModCount)
  17. throw new ConcurrentModificationException();//<---异常在此抛出
  18. }
  19. }
  20. public boolean add(E e) {
  21. ensureCapacityInternal(size + 1); // Increments modCount!!
  22. elementData[size++] = e;
  23. return true;
  24. }
  25. private void ensureCapacityInternal(int minCapacity) {
  26. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  27. }
  28. private void ensureExplicitCapacity(int minCapacity) {
  29. modCount++;//添加时,修改了modCount的值
  30. // overflow-conscious code
  31. if (minCapacity - elementData.length > 0)
  32. grow(minCapacity);
  33. }
  34. ...
  35. }
  1. public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
  2. ...
  3. protected transient int modCount = 0;
  4. ...
  5. }

modCount具体详细说明如下:

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results. This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods. If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations. This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration. link

综上所述,假设线程A将通过迭代器next()获取下一元素时,从而将其打印出来。但之前,其他某线程添加新元素至list,结构发生了改变,modCount自增。当线程A运行到checkForComodification(),expectedModCount是modCount之前自增的值,判定modCount != expectedModCount为真,继而抛出ConcurrentModificationException。

解决方法之三:CopyOnWriteArrayList(推荐)

  1. public class CopyOnWriteArrayList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  3. /** The array, accessed only via getArray/setArray. */
  4. private transient volatile Object[] array;
  5. final Object[] getArray() {
  6. return array;
  7. }
  8. final void setArray(Object[] a) {
  9. array = a;
  10. }
  11. ...
  12. public boolean add(E e) {
  13. final ReentrantLock lock = this.lock;
  14. lock.lock();
  15. try {
  16. Object[] elements = getArray();
  17. int len = elements.length;
  18. Object[] newElements = Arrays.copyOf(elements, len + 1);
  19. newElements[len] = e;
  20. setArray(newElements);
  21. return true;
  22. } finally {
  23. lock.unlock();
  24. }
  25. }
  26. ...
  27. public String toString() {
  28. return Arrays.toString(getArray());
  29. }
  30. ...
  31. }

CopyOnWrite容器即写时复制的容器。待一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。

这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

3. 集合类不安全之Set

HashSet也是非线性安全的。(HashSet内部是包装了一个HashMap的)

  1. import java.util.Collections;
  2. import java.util.HashSet;
  3. import java.util.Set;
  4. import java.util.UUID;
  5. import java.util.concurrent.CopyOnWriteArraySet;
  6. public class SetNotSafeDemo {
  7. public static void main(String[] args) {
  8. Set<String> set = new HashSet<>();
  9. //Set<String> set = Collections.synchronizedSet(new HashSet<>());
  10. //Set<String> set = new CopyOnWriteArraySet<String>();
  11. for (int i = 0; i < 30; i++) {
  12. new Thread(() -> {
  13. set.add(UUID.randomUUID().toString().substring(0, 8));
  14. System.out.println(set);
  15. }, String.valueOf(i)).start();
  16. }
  17. }
  18. }

解决方法:

  1. Collections.synchronizedSet(new HashSet<>())
  2. CopyOnWriteArraySet<>()(推荐)

CopyOnWriteArraySet源码一览:

  1. public class CopyOnWriteArraySet<E> extends AbstractSet<E>
  2. implements java.io.Serializable {
  3. private static final long serialVersionUID = 5457747651344034263L;
  4. private final CopyOnWriteArrayList<E> al;
  5. /**
  6. * Creates an empty set.
  7. */
  8. public CopyOnWriteArraySet() {
  9. al = new CopyOnWriteArrayList<E>();
  10. }
  11. public CopyOnWriteArraySet(Collection<? extends E> c) {
  12. if (c.getClass() == CopyOnWriteArraySet.class) {
  13. @SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
  14. (CopyOnWriteArraySet<E>)c;
  15. al = new CopyOnWriteArrayList<E>(cc.al);
  16. }
  17. else {
  18. al = new CopyOnWriteArrayList<E>();
  19. al.addAllAbsent(c);
  20. }
  21. }
  22. //可看出CopyOnWriteArraySet包装了一个CopyOnWriteArrayList
  23. ...
  24. public boolean add(E e) {
  25. return al.addIfAbsent(e);
  26. }
  27. public boolean addIfAbsent(E e) {
  28. Object[] snapshot = getArray();
  29. return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
  30. addIfAbsent(e, snapshot);
  31. }
  32. //暴力查找
  33. private static int indexOf(Object o, Object[] elements,
  34. int index, int fence) {
  35. if (o == null) {
  36. for (int i = index; i < fence; i++)
  37. if (elements[i] == null)
  38. return i;
  39. } else {
  40. for (int i = index; i < fence; i++)
  41. if (o.equals(elements[i]))
  42. return i;
  43. }
  44. return -1;
  45. }
  46. private boolean addIfAbsent(E e, Object[] snapshot) {
  47. final ReentrantLock lock = this.lock;
  48. lock.lock();
  49. try {
  50. Object[] current = getArray();
  51. int len = current.length;
  52. if (snapshot != current) {//还要检查多一次元素存在性,生怕别的线程已经插入了
  53. // Optimize for lost race to another addXXX operation
  54. int common = Math.min(snapshot.length, len);
  55. for (int i = 0; i < common; i++)
  56. if (current[i] != snapshot[i] && eq(e, current[i]))
  57. return false;
  58. if (indexOf(e, current, common, len) >= 0)
  59. return false;
  60. }
  61. Object[] newElements = Arrays.copyOf(current, len + 1);
  62. newElements[len] = e;
  63. setArray(newElements);
  64. return true;
  65. } finally {
  66. lock.unlock();
  67. }
  68. }
  69. ...
  70. }

4. 集合类不安全之Map

  1. import java.util.Collections;
  2. import java.util.HashMap;
  3. import java.util.Hashtable;
  4. import java.util.Map;
  5. import java.util.UUID;
  6. import java.util.concurrent.ConcurrentHashMap;
  7. public class MapNotSafeDemo {
  8. public static void main(String[] args) {
  9. Map<String, String> map = new HashMap<>();
  10. // Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
  11. // Map<String, String> map = new ConcurrentHashMap<>();
  12. // Map<String, String> map = new Hashtable<>();
  13. for (int i = 0; i < 30; i++) {
  14. new Thread(() -> {
  15. map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
  16. System.out.println(map);
  17. }, String.valueOf(i)).start();
  18. }
  19. }
  20. }

解决方法:

  1. HashTable
  2. Collections.synchronizedMap(new HashMap<>())
  3. ConcurrencyMap<>()(推荐)

    5. TransferValue醒脑小练习

    Java的参数传递是值传递,不是引用传递。
    下面程序体验下上一句的含义: ```java class Person { private Integer id; private String personName;

    public Person(String personName) {

    1. this.personName = personName;

    }

    public Integer getId() {

    1. return id;

    }

    public void setId(Integer id) {

    1. this.id = id;

    }

    public String getPersonName() {

    1. return personName;

    }

    public void setPersonName(String personName) {

    1. this.personName = personName;

    } }

public class TransferValueDemo { public void changeValue1(int age) { age = 30; }

  1. public void changeValue2(Person person) {
  2. person.setPersonName("XXXX");
  3. }
  4. public void changeValue3(String str) {
  5. str = "XXX";
  6. }
  7. public static void main(String[] args) {
  8. TransferValueDemo test = new TransferValueDemo();
  9. // 定义基本数据类型
  10. int age = 20;
  11. test.changeValue1(age);
  12. System.out.println("age ----" + age);
  13. // 实例化person类
  14. Person person = new Person("abc");
  15. test.changeValue2(person);
  16. System.out.println("personName-----" + person.getPersonName());
  17. // String
  18. String str = "abc";
  19. test.changeValue3(str);
  20. System.out.println("string-----" + str);
  21. }

}

  1. 输出结果:
  2. ```java
  3. age ----20
  4. personName-----XXXX
  5. string-----abc

4. 锁

1. java锁之公平和非公平锁

是什么

  • 公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting. 此类的构造函数接受可选的公平性参数。当设置为true时,在争用下,锁有利于向等待时间最长的线程授予访问权限。否则,此锁不保证任何特定的访问顺序。与使用默认设置的程序相比,使用由许多线程访问的公平锁的程序可能显示出较低的总体吞吐量(即,较慢;通常要慢得多),但是在获得锁和保证没有饥饿的时间上差异较小。 但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个线程可以连续多次获得公平锁,而其他活动线程则没有进行并且当前没有持有该锁。还要注意,不计时的 tryLock()方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。 link

reentrant
英 [riːˈɛntrənt] 美 [ˌriˈɛntrənt]
a. 可重入;可重入的;重入;可再入的;重进入

两者区别

关于两者区别:

  • 公平锁
    • Threads acquire a fair lock in the order in which they requested it.
    • 公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
  • 非公平锁
    • a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
    • 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

题外话

Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁

2. java锁之可重入锁和递归锁理论知识

可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock/synchronized就是一个典型的可重入锁。
可重入锁最大的作用是避免死锁。

3. java锁之可重入锁和递归锁代码验证

Synchronized可入锁演示程序

  1. class Phone {
  2. public synchronized void sendSMS() throws Exception{
  3. System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
  4. // 在同步方法中,调用另外一个同步方法
  5. sendEmail();
  6. }
  7. public synchronized void sendEmail() throws Exception{
  8. System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
  9. }
  10. }
  11. public class SynchronizedReentrantLockDemo {
  12. public static void main(String[] args) {
  13. Phone phone = new Phone();
  14. // 两个线程操作资源列
  15. new Thread(() -> {
  16. try {
  17. phone.sendSMS();
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }, "t1").start();
  22. new Thread(() -> {
  23. try {
  24. phone.sendSMS();
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. }, "t2").start();
  29. }
  30. }

输出结果:

  1. t1 invoked sendSMS()
  2. 11 invoked sendEmail()
  3. t2 invoked sendSMS()
  4. 12 invoked sendEmail()

ReentrantLock可重入锁演示程序

  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. class Phone2 implements Runnable{
  4. Lock lock = new ReentrantLock();
  5. /**
  6. * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
  7. */
  8. public void getLock() {
  9. lock.lock();
  10. try {
  11. System.out.println(Thread.currentThread().getName() + "\t get Lock");
  12. setLock();
  13. } finally {
  14. lock.unlock();
  15. }
  16. }
  17. public void setLock() {
  18. lock.lock();
  19. try {
  20. System.out.println(Thread.currentThread().getName() + "\t set Lock");
  21. } finally {
  22. lock.unlock();
  23. }
  24. }
  25. @Override
  26. public void run() {
  27. getLock();
  28. }
  29. }
  30. public class ReentrantLockDemo {
  31. public static void main(String[] args) {
  32. Phone2 phone = new Phone2();
  33. /**
  34. * 因为Phone实现了Runnable接口
  35. */
  36. Thread t3 = new Thread(phone, "t3");
  37. Thread t4 = new Thread(phone, "t4");
  38. t3.start();
  39. t4.start();
  40. }
  41. }

输出结果

  1. t3 get Lock
  2. t3 set Lock
  3. t4 get Lock
  4. t4 set Lock

4. java锁之自旋锁理论知识

自旋锁(Spin Lock)

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。《深入理解JVM.2nd》Page 398

5. java锁之自旋锁代码验证

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicReference;
  3. public class SpinLockDemo {
  4. // 现在的泛型装的是Thread,原子引用线程
  5. AtomicReference<Thread> atomicReference = new AtomicReference<>();
  6. public void myLock() {
  7. // 获取当前进来的线程
  8. Thread thread = Thread.currentThread();
  9. System.out.println(Thread.currentThread().getName() + "\t come in ");
  10. // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
  11. while(!atomicReference.compareAndSet(null, thread)) {
  12. //摸鱼
  13. }
  14. }
  15. public void myUnLock() {
  16. // 获取当前进来的线程
  17. Thread thread = Thread.currentThread();
  18. // 自己用完了后,把atomicReference变成null
  19. atomicReference.compareAndSet(thread, null);
  20. System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
  21. }
  22. public static void main(String[] args) {
  23. SpinLockDemo spinLockDemo = new SpinLockDemo();
  24. // 启动t1线程,开始操作
  25. new Thread(() -> {
  26. // 开始占有锁
  27. spinLockDemo.myLock();
  28. try {
  29. TimeUnit.SECONDS.sleep(5);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. // 开始释放锁
  34. spinLockDemo.myUnLock();
  35. }, "t1").start();
  36. // 让main线程暂停1秒,使得t1线程,先执行
  37. try {
  38. TimeUnit.SECONDS.sleep(1);
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. // 1秒后,启动t2线程,开始占用这个锁
  43. new Thread(() -> {
  44. // 开始占有锁
  45. spinLockDemo.myLock();
  46. // 开始释放锁
  47. spinLockDemo.myUnLock();
  48. }, "t2").start();
  49. }
  50. }

输出结果

  1. t1 come in
  2. t2 come in
  3. t1 invoked myUnlock()
  4. t2 invoked myUnlock()

6. java锁之读写锁理论知识

独占锁

指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁

指该锁可被多个线程所持有。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

7. java锁之读写锁代码验证

实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.TimeUnit;
  4. class MyCache {
  5. private volatile Map<String, Object> map = new HashMap<>();
  6. public void put(String key, Object value) {
  7. System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
  8. try {
  9. // 模拟网络拥堵,延迟0.3秒
  10. TimeUnit.MILLISECONDS.sleep(300);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. map.put(key, value);
  15. System.out.println(Thread.currentThread().getName() + "\t 写入完成");
  16. }
  17. public void get(String key) {
  18. System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
  19. try {
  20. // 模拟网络拥堵,延迟0.3秒
  21. TimeUnit.MILLISECONDS.sleep(300);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. Object value = map.get(key);
  26. System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
  27. }
  28. }
  29. public class ReadWriteWithoutLockDemo {
  30. public static void main(String[] args) {
  31. MyCache myCache = new MyCache();
  32. // 线程操作资源类,5个线程写
  33. for (int i = 0; i < 5; i++) {
  34. final int tempInt = i;
  35. new Thread(() -> {
  36. myCache.put(tempInt + "", tempInt + "");
  37. }, String.valueOf(i)).start();
  38. }
  39. // 线程操作资源类, 5个线程读
  40. for (int i = 0; i < 5; i++) {
  41. final int tempInt = i;
  42. new Thread(() -> {
  43. myCache.get(tempInt + "");
  44. }, String.valueOf(i)).start();
  45. }
  46. }
  47. }

输出结果:

  1. 0 正在写入:0
  2. 1 正在写入:1
  3. 3 正在写入:3
  4. 2 正在写入:2
  5. 4 正在写入:4
  6. 0 正在读取:
  7. 1 正在读取:
  8. 2 正在读取:
  9. 4 正在读取:
  10. 3 正在读取:
  11. 1 写入完成
  12. 4 写入完成
  13. 0 写入完成
  14. 2 写入完成
  15. 3 写入完成
  16. 3 读取完成:3
  17. 0 读取完成:0
  18. 2 读取完成:2
  19. 1 读取完成:null
  20. 4 读取完成:null

看到有些线程读取到null,可用ReentrantReadWriteLock解决

  1. package com.lun.concurrency;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. import java.util.concurrent.TimeUnit;
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;
  6. class MyCache2 {
  7. private volatile Map<String, Object> map = new HashMap<>();
  8. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
  9. public void put(String key, Object value) {
  10. // 创建一个写锁
  11. rwLock.writeLock().lock();
  12. try {
  13. System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
  14. try {
  15. // 模拟网络拥堵,延迟0.3秒
  16. TimeUnit.MILLISECONDS.sleep(300);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. map.put(key, value);
  21. System.out.println(Thread.currentThread().getName() + "\t 写入完成");
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. } finally {
  25. // 写锁 释放
  26. rwLock.writeLock().unlock();
  27. }
  28. }
  29. public void get(String key) {
  30. // 读锁
  31. rwLock.readLock().lock();
  32. try {
  33. System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
  34. try {
  35. // 模拟网络拥堵,延迟0.3秒
  36. TimeUnit.MILLISECONDS.sleep(300);
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. Object value = map.get(key);
  41. System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. } finally {
  45. // 读锁释放
  46. rwLock.readLock().unlock();
  47. }
  48. }
  49. public void clean() {
  50. map.clear();
  51. }
  52. }
  53. public class ReadWriteWithLockDemo {
  54. public static void main(String[] args) {
  55. MyCache2 myCache = new MyCache2();
  56. // 线程操作资源类,5个线程写
  57. for (int i = 1; i <= 5; i++) {
  58. // lambda表达式内部必须是final
  59. final int tempInt = i;
  60. new Thread(() -> {
  61. myCache.put(tempInt + "", tempInt + "");
  62. }, String.valueOf(i)).start();
  63. }
  64. // 线程操作资源类, 5个线程读
  65. for (int i = 1; i <= 5; i++) {
  66. // lambda表达式内部必须是final
  67. final int tempInt = i;
  68. new Thread(() -> {
  69. myCache.get(tempInt + "");
  70. }, String.valueOf(i)).start();
  71. }
  72. }
  73. }

输出结果:

  1. 1 正在写入:1
  2. 1 写入完成
  3. 2 正在写入:2
  4. 2 写入完成
  5. 3 正在写入:3
  6. 3 写入完成
  7. 5 正在写入:5
  8. 5 写入完成
  9. 4 正在写入:4
  10. 4 写入完成
  11. 2 正在读取:
  12. 3 正在读取:
  13. 1 正在读取:
  14. 5 正在读取:
  15. 4 正在读取:
  16. 3 读取完成:3
  17. 2 读取完成:2
  18. 1 读取完成:1
  19. 5 读取完成:5
  20. 4 读取完成:4

5. 阻塞工具类

1. CountDownLatch

让一线程阻塞直到另一些线程完成一系列操作才被唤醒。
CountDownLatch主要有两个方法(await(),countDown())。
当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。

latch
英 [lætʃ] 美 [lætʃ]
n. 门闩;插销;碰锁;弹簧锁
v. 用插销插上;用碰锁锁上

案例1 countDownLatch

假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的

  1. import java.util.concurrent.CountDownLatch;
  2. public class CountDownLatchDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. // 计数器
  5. CountDownLatch countDownLatch = new CountDownLatch(6);
  6. for (int i = 0; i <= 6; i++) {
  7. new Thread(() -> {
  8. System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
  9. countDownLatch.countDown();
  10. }, String.valueOf(i)).start();
  11. }
  12. countDownLatch.await();
  13. System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
  14. }
  15. }

输出结果:

  1. 0 上完自习,离开教室
  2. 6 上完自习,离开教室
  3. 4 上完自习,离开教室
  4. 5 上完自习,离开教室
  5. 3 上完自习,离开教室
  6. 1 上完自习,离开教室
  7. 2 上完自习,离开教室
  8. main 班长最后关门

案例2 枚举 + CountDownLatch

温习枚举
程序演示秦国统一六国

  1. import java.util.Objects;
  2. public enum CountryEnum {
  3. ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");
  4. private Integer retcode;
  5. private String retMessage;
  6. CountryEnum(Integer retcode, String retMessage) {
  7. this.retcode = retcode;
  8. this.retMessage = retMessage;
  9. }
  10. public static CountryEnum forEach_countryEnum(int index) {
  11. CountryEnum[] myArray = CountryEnum.values();
  12. for(CountryEnum ce : myArray) {
  13. if(Objects.equals(index, ce.getRetcode())) {
  14. return ce;
  15. }
  16. }
  17. return null;
  18. }
  19. public Integer getRetcode() {
  20. return retcode;
  21. }
  22. public void setRetcode(Integer retcode) {
  23. this.retcode = retcode;
  24. }
  25. public String getRetMessage() {
  26. return retMessage;
  27. }
  28. public void setRetMessage(String retMessage) {
  29. this.retMessage = retMessage;
  30. }
  31. }
  1. import java.util.concurrent.CountDownLatch;
  2. public class UnifySixCountriesDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. // 计数器
  5. CountDownLatch countDownLatch = new CountDownLatch(6);
  6. for (int i = 1; i <= 6; i++) {
  7. new Thread(() -> {
  8. System.out.println(Thread.currentThread().getName() + "国被灭了!");
  9. countDownLatch.countDown();
  10. }, CountryEnum.forEach_countryEnum(i).getRetMessage()).start();
  11. }
  12. countDownLatch.await();
  13. System.out.println(Thread.currentThread().getName() + " 秦国统一中原。");
  14. }
  15. }

输出结果:

  1. 齐国被灭了!
  2. 燕国被灭了!
  3. 楚国被灭了!
  4. 魏国被灭了!
  5. 韩国被灭了!
  6. 赵国被灭了!
  7. main 秦国统一中原。

2. CyclicBarrier

CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法。

CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。

案例1 CyclicBarrier

程序演示集齐7个龙珠,召唤神龙

  1. import java.util.concurrent.BrokenBarrierException;
  2. import java.util.concurrent.CyclicBarrier;
  3. public class SummonTheDragonDemo {
  4. public static void main(String[] args) {
  5. /**
  6. * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
  7. */
  8. CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
  9. System.out.println("召唤神龙");
  10. });
  11. for (int i = 1; i <= 7; i++) {
  12. final Integer tempInt = i;
  13. new Thread(() -> {
  14. System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
  15. try {
  16. // 先到的被阻塞,等全部线程完成后,才能执行方法
  17. cyclicBarrier.await();
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. } catch (BrokenBarrierException e) {
  21. e.printStackTrace();
  22. }
  23. }, String.valueOf(i)).start();
  24. }
  25. }
  26. }

输出结果:

  1. 2 收集到 2颗龙珠
  2. 6 收集到 6颗龙珠
  3. 1 收集到 1颗龙珠
  4. 7 收集到 7颗龙珠
  5. 5 收集到 5颗龙珠
  6. 4 收集到 4颗龙珠
  7. 3 收集到 3颗龙珠
  8. 召唤神龙

案例2 来自《Java编程思想》的例子

来自《Java编程思想》的例子,展现CyclicBarrier的可循环性:

  1. import java.util.concurrent.*;
  2. import java.util.*;
  3. class Horse implements Runnable {
  4. private static int counter = 0;
  5. private final int id = counter++;
  6. private int strides = 0;
  7. private static Random rand = new Random(47);
  8. private static CyclicBarrier barrier;
  9. public Horse(CyclicBarrier b) {
  10. barrier = b;
  11. }
  12. public synchronized int getStrides() {
  13. return strides;
  14. }
  15. public void run() {
  16. try {
  17. while (!Thread.interrupted()) {//没有中断,就不断循环
  18. synchronized (this) {
  19. //模拟马单位时间的移动距离
  20. strides += rand.nextInt(3); // Produces 0, 1 or 2
  21. }
  22. barrier.await();//<---等待其他马到齐到循环屏障
  23. }
  24. } catch (InterruptedException e) {
  25. // A legitimate way to exit
  26. } catch (BrokenBarrierException e) {
  27. // This one we want to know about
  28. throw new RuntimeException(e);
  29. }
  30. }
  31. public String toString() {
  32. return "Horse " + id + " ";
  33. }
  34. public String tracks() {
  35. StringBuilder s = new StringBuilder();
  36. for (int i = 0; i < getStrides(); i++)
  37. s.append("*");
  38. s.append(id);
  39. return s.toString();
  40. }
  41. }
  42. public class HorseRace {
  43. static final int FINISH_LINE = 75;
  44. private List<Horse> horses = new ArrayList<Horse>();
  45. private ExecutorService exec = Executors.newCachedThreadPool();
  46. private CyclicBarrier barrier;
  47. public HorseRace(int nHorses, final int pause) {
  48. //初始化循环屏障
  49. barrier = new CyclicBarrier(nHorses, new Runnable() {
  50. // 循环多次执行的任务
  51. public void run() {
  52. // The fence on the racetrack
  53. StringBuilder s = new StringBuilder();
  54. for (int i = 0; i < FINISH_LINE; i++)
  55. s.append("=");
  56. System.out.println(s);
  57. //打印马移动距离
  58. for (Horse horse : horses)
  59. System.out.println(horse.tracks());
  60. //判断有没有马到终点了
  61. for (Horse horse : horses)
  62. if (horse.getStrides() >= FINISH_LINE) {
  63. System.out.println(horse + "won!");
  64. exec.shutdownNow();// 有只马跑赢了,所有任务都结束了
  65. return;
  66. }
  67. try {
  68. TimeUnit.MILLISECONDS.sleep(pause);
  69. } catch (InterruptedException e) {
  70. System.out.println("barrier-action sleep interrupted");
  71. }
  72. }
  73. });
  74. // 开跑!
  75. for (int i = 0; i < nHorses; i++) {
  76. Horse horse = new Horse(barrier);
  77. horses.add(horse);
  78. exec.execute(horse);
  79. }
  80. }
  81. public static void main(String[] args) {
  82. int nHorses = 7;
  83. int pause = 200;
  84. new HorseRace(nHorses, pause);
  85. }
  86. }

输出结果:

  1. ...省略一些...
  2. ===========================================================================
  3. **********************************************************0
  4. ************************************************************1
  5. ******************************************************2
  6. ***********************************************************************3
  7. *************************************************************************4
  8. *****************************************************************5
  9. *****************************************************************6
  10. ===========================================================================
  11. **********************************************************0
  12. ************************************************************1
  13. *******************************************************2
  14. ***********************************************************************3
  15. **************************************************************************4
  16. *****************************************************************5
  17. *******************************************************************6
  18. ===========================================================================
  19. ***********************************************************0
  20. *************************************************************1
  21. *******************************************************2
  22. ***********************************************************************3
  23. ****************************************************************************4
  24. *******************************************************************5
  25. ********************************************************************6
  26. Horse 4 won!

3. Semaphore

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

semaphore
英 [ˈseməfɔː®] 美 [ˈseməfɔːr]
n. 信号标;旗语
v. 打旗语;(用其他类似的信号系统)发信号

正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源。

案例1

模拟一个抢车位的场景,假设一共有6个车,3个停车位

  1. import java.util.concurrent.Semaphore;
  2. import java.util.concurrent.TimeUnit;
  3. public class SemaphoreDemo {
  4. public static void main(String[] args) {
  5. /**
  6. * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
  7. */
  8. Semaphore semaphore = new Semaphore(3, false);
  9. // 模拟6部车
  10. for (int i = 0; i < 6; i++) {
  11. new Thread(() -> {
  12. try {
  13. // 代表一辆车,已经占用了该车位
  14. semaphore.acquire(); // 抢占
  15. System.out.println(Thread.currentThread().getName() + "\t 抢到车位");
  16. // 每个车停3秒
  17. try {
  18. TimeUnit.SECONDS.sleep(3);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. System.out.println(Thread.currentThread().getName() + "\t 离开车位");
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. } finally {
  26. // 释放停车位
  27. semaphore.release();
  28. }
  29. }, String.valueOf(i)).start();
  30. }
  31. }
  32. }

输出结果:

  1. 1 抢到车位
  2. 2 抢到车位
  3. 0 抢到车位
  4. 0 离开车位
  5. 2 离开车位
  6. 1 离开车位
  7. 5 抢到车位
  8. 4 抢到车位
  9. 3 抢到车位
  10. 5 离开车位
  11. 4 离开车位
  12. 3 离开车位

6. 阻塞理论和实践

1. 阻塞队列理论

  • 阻塞队列有没有好的一面
  • 不得不阻塞,你如何管理

    2. 阻塞队列接口结构和实现类

    阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
    image.png
    线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。
    当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
    当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
    试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
    同样试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增

为什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下余挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

在Concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
架构介绍
image.png
种类分析:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现妁延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列。
  • LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

    BlockingQueue的核心方法

    | 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | | —- | —- | —- | —- | —- | | 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) | | 移除 | remove() | poll() | take() | poll(time,unit) | | 检查 | element() | peek() | 不可用 | 不可用 |
性质 说明
抛出异常 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full
当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException
特殊性 插入方法,成功true,失败false
移除方法:成功返回出队列元素,队列没有就返回空
一直阻塞 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。
超时退出 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出

3. 阻塞队列api之抛出异常组

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. public class BlockingQueueExceptionDemo {
  4. public static void main(String[] args) {
  5. BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
  6. System.out.println(blockingQueue.add("a"));
  7. System.out.println(blockingQueue.add("b"));
  8. System.out.println(blockingQueue.add("c"));
  9. try {
  10. //抛出 java.lang.IllegalStateException: Queue full
  11. System.out.println(blockingQueue.add("XXX"));
  12. } catch (Exception e) {
  13. System.err.println(e);
  14. }
  15. System.out.println(blockingQueue.element());
  16. ///
  17. System.out.println(blockingQueue.remove());
  18. System.out.println(blockingQueue.remove());
  19. System.out.println(blockingQueue.remove());
  20. try {
  21. //抛出 java.util.NoSuchElementException
  22. System.out.println(blockingQueue.remove());
  23. } catch (Exception e) {
  24. System.err.println(e);
  25. }
  26. try {
  27. //element()相当于peek(),但element()会抛NoSuchElementException
  28. System.out.println(blockingQueue.element());
  29. } catch (Exception e) {
  30. System.err.println(e);
  31. }
  32. }
  33. }

输出结果:

  1. true
  2. true
  3. true
  4. a
  5. java.lang.IllegalStateException: Queue full
  6. a
  7. b
  8. c
  9. java.util.NoSuchElementException
  10. java.util.NoSuchElementException

4. 阻塞队列api之返回布尔值组

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. public class BlockingQueueBooleanDemo {
  4. public static void main(String[] args) {
  5. BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
  6. System.out.println(blockingQueue.offer("a"));
  7. System.out.println(blockingQueue.offer("b"));
  8. System.out.println(blockingQueue.offer("c"));
  9. System.out.println(blockingQueue.offer("d"));
  10. System.out.println(blockingQueue.poll());
  11. System.out.println(blockingQueue.poll());
  12. System.out.println(blockingQueue.poll());
  13. System.out.println(blockingQueue.poll());
  14. }
  15. }

输出结果:

  1. true
  2. true
  3. true
  4. false
  5. a
  6. b
  7. c
  8. null

5. 阻塞队列api之阻塞和超时控制

队列阻塞演示

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. import java.util.concurrent.TimeUnit;
  4. public class BlockingQueueBlockedDemo {
  5. public static void main(String[] args) throws InterruptedException {
  6. BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
  7. new Thread(()->{
  8. try {
  9. blockingQueue.put("a");
  10. blockingQueue.put("b");
  11. blockingQueue.put("c");
  12. blockingQueue.put("c");//将会阻塞,直到主线程take()
  13. System.out.println("it was blocked.");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }).start();
  18. TimeUnit.SECONDS.sleep(2);
  19. try {
  20. blockingQueue.take();
  21. blockingQueue.take();
  22. blockingQueue.take();
  23. blockingQueue.take();
  24. System.out.println("Blocking...");
  25. blockingQueue.take();//将会阻塞
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }

阻塞超时放弃演示

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. import java.util.concurrent.TimeUnit;
  4. public class BlockingQueueTimeoutDemo {
  5. public static void main(String[] args) throws InterruptedException {
  6. BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
  7. System.out.println("Offer.");
  8. System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
  9. System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
  10. System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
  11. System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
  12. System.out.println("Poll.");
  13. System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
  14. System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
  15. System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
  16. System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
  17. }
  18. }

输出结果:

  1. Offer.
  2. true
  3. true
  4. true
  5. false
  6. Poll.
  7. a
  8. b
  9. c
  10. null

6. 阻塞队列之同步SynchronousQueue队列

SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

A blocking queue in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa. A synchronous queue does not have any internal capacity, not even a capacity of one.

  • You cannot peek at a synchronous queue because an element is only present when you try to remove it;
  • You cannot insert an element (using any method) unless another thread is trying to remove it;
  • You cannot iterate as there is nothing to iterate.

The head of the queue is the element that the first queued inserting thread is trying to add to the queue; if there is no such queued thread then no element is available for removal and poll() will return null. For purposes of other Collection methods (for example contains), a SynchronousQueue acts as an empty collection. This queue does not permit null elements. link

  1. import java.util.concurrent.BlockingQueue;
  2. import java.util.concurrent.SynchronousQueue;
  3. import java.util.concurrent.TimeUnit;
  4. public class SynchronousQueueDemo {
  5. public static void main(String[] args) {
  6. BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
  7. new Thread(() -> {
  8. try {
  9. System.out.println(Thread.currentThread().getName() + "\t put A ");
  10. blockingQueue.put("A");
  11. System.out.println(Thread.currentThread().getName() + "\t put B ");
  12. blockingQueue.put("B");
  13. System.out.println(Thread.currentThread().getName() + "\t put C ");
  14. blockingQueue.put("C");
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }, "t1").start();
  19. new Thread(() -> {
  20. try {
  21. try {
  22. TimeUnit.SECONDS.sleep(5);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. blockingQueue.take();
  27. System.out.println(Thread.currentThread().getName() + "\t take A ");
  28. try {
  29. TimeUnit.SECONDS.sleep(5);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. blockingQueue.take();
  34. System.out.println(Thread.currentThread().getName() + "\t take B ");
  35. try {
  36. TimeUnit.SECONDS.sleep(5);
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. blockingQueue.take();
  41. System.out.println(Thread.currentThread().getName() + "\t take C ");
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }, "t2").start();
  46. }
  47. }

7. 线程通信之生产者消费者传统版

阻塞队列用在哪里?

  • 生产者消费者模式
    • 传统版(synchronized, wait, notify)
    • 阻塞队列版(lock, await, signal)
  • 线程池
  • 消息中间件

    实现一个简单的生产者消费者模式

    ```java import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;

class ShareData {

  1. private int number = 0;
  2. private Lock lock = new ReentrantLock();
  3. private Condition condition = lock.newCondition();
  4. public void increment() throws Exception{
  5. // 同步代码块,加锁
  6. lock.lock();
  7. try {
  8. // 判断
  9. while(number != 0) {
  10. // 等待不能生产
  11. condition.await();
  12. }
  13. // 干活
  14. number++;
  15. System.out.println(Thread.currentThread().getName() + "\t " + number);
  16. // 通知 唤醒
  17. condition.signalAll();
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. } finally {
  21. lock.unlock();
  22. }
  23. }
  24. public void decrement() throws Exception{
  25. // 同步代码块,加锁
  26. lock.lock();
  27. try {
  28. // 判断
  29. while(number == 0) {
  30. // 等待不能消费
  31. condition.await();
  32. }
  33. // 干活
  34. number--;
  35. System.out.println(Thread.currentThread().getName() + "\t " + number);
  36. // 通知 唤醒
  37. condition.signalAll();
  38. } catch (Exception e) {
  39. e.printStackTrace();
  40. } finally {
  41. lock.unlock();
  42. }
  43. }

}

public class TraditionalProducerConsumerDemo {

  1. public static void main(String[] args) {
  2. ShareData shareData = new ShareData();
  3. // t1线程,生产
  4. new Thread(() -> {
  5. for (int i = 0; i < 5; i++) {
  6. try {
  7. shareData.increment();
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }, "t1").start();
  13. // t2线程,消费
  14. new Thread(() -> {
  15. for (int i = 0; i < 5; i++) {
  16. try {
  17. shareData.decrement();
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }, "t2").start();
  23. }

}

  1. 输出结果:
  2. ```java
  3. t1 1
  4. t2 0
  5. t1 1
  6. t2 0
  7. t1 1
  8. t2 0
  9. t1 1
  10. t2 0
  11. t1 1
  12. t2 0
  13. t1 1
  14. t2 0
  15. t1 1
  16. t2 0
  17. t1 1
  18. t2 0
  19. t1 1
  20. t2 0
  21. t1 1
  22. t2 0

注意,increment()和decrement()内的

  1. // 判断
  2. while(number != 0) {
  3. // 等待不能生产
  4. condition.await();
  5. }

不能用

  1. // 判断
  2. if(number != 0) {
  3. // 等待不能生产
  4. condition.await();
  5. }

否则会出现虚假唤醒,出现异常状况。

8. Synchronized和Lock有什么区别

  1. synchronized属于JVM层面,属于java的关键字
    1. monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
    2. Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
  2. 使用方法:
    1. synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
    2. ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成
  3. 等待是否中断
    1. synchronized:不可中断,除非抛出异常或者正常运行完成。
    2. ReentrantLock:可中断,可以设置超时方法
      1. 设置超时方法,trylock(long timeout, TimeUnit unit)
      2. lockInterrupible() 放代码块中,调用interrupt() 方法可以中断
  4. 加锁是否公平
    1. synchronized:非公平锁
    2. ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
  5. 锁绑定多个条件Condition
    1. synchronized:没有,要么随机,要么全部唤醒
    2. ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒

9. 锁绑定多个条件Condition

实现场景
多线程之间按顺序调用,实现 A-> B -> C 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次

来10轮

  1. import java.util.concurrent.locks.Condition;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. class ShareResource {
  5. // A 1 B 2 c 3
  6. private int number = 1;
  7. // 创建一个重入锁
  8. private Lock lock = new ReentrantLock();
  9. // 这三个相当于备用钥匙
  10. private Condition condition1 = lock.newCondition();
  11. private Condition condition2 = lock.newCondition();
  12. private Condition condition3 = lock.newCondition();
  13. public void print5() {
  14. lock.lock();
  15. try {
  16. // 判断
  17. while(number != 1) {
  18. // 不等于1,需要等待
  19. condition1.await();
  20. }
  21. // 干活
  22. for (int i = 0; i < 5; i++) {
  23. System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
  24. }
  25. // 唤醒 (干完活后,需要通知B线程执行)
  26. number = 2;
  27. // 通知2号去干活了
  28. condition2.signal();
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. } finally {
  32. lock.unlock();
  33. }
  34. }
  35. public void print10() {
  36. lock.lock();
  37. try {
  38. // 判断
  39. while(number != 2) {
  40. // 不等于1,需要等待
  41. condition2.await();
  42. }
  43. // 干活
  44. for (int i = 0; i < 10; i++) {
  45. System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
  46. }
  47. // 唤醒 (干完活后,需要通知C线程执行)
  48. number = 3;
  49. // 通知2号去干活了
  50. condition3.signal();
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. } finally {
  54. lock.unlock();
  55. }
  56. }
  57. public void print15() {
  58. lock.lock();
  59. try {
  60. // 判断
  61. while(number != 3) {
  62. // 不等于1,需要等待
  63. condition3.await();
  64. }
  65. // 干活
  66. for (int i = 0; i < 15; i++) {
  67. System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
  68. }
  69. // 唤醒 (干完活后,需要通知C线程执行)
  70. number = 1;
  71. // 通知1号去干活了
  72. condition1.signal();
  73. } catch (Exception e) {
  74. e.printStackTrace();
  75. } finally {
  76. lock.unlock();
  77. }
  78. }
  79. }
  80. public class SynchronizedAndReentrantLockDemo {
  81. public static void main(String[] args) {
  82. ShareResource shareResource = new ShareResource();
  83. int num = 10;
  84. new Thread(() -> {
  85. for (int i = 0; i < num; i++) {
  86. shareResource.print5();
  87. }
  88. }, "A").start();
  89. new Thread(() -> {
  90. for (int i = 0; i < num; i++) {
  91. shareResource.print10();
  92. }
  93. }, "B").start();
  94. new Thread(() -> {
  95. for (int i = 0; i < num; i++) {
  96. shareResource.print15();
  97. }
  98. }, "C").start();
  99. }
  100. }

输出结果:

  1. ...
  2. A 1 0
  3. A 1 1
  4. A 1 2
  5. A 1 3
  6. A 1 4
  7. B 2 0
  8. B 2 1
  9. B 2 2
  10. B 2 3
  11. B 2 4
  12. B 2 5
  13. B 2 6
  14. B 2 7
  15. B 2 8
  16. B 2 9
  17. C 3 0
  18. C 3 1
  19. C 3 2
  20. C 3 3
  21. C 3 4
  22. C 3 5
  23. C 3 6
  24. C 3 7
  25. C 3 8
  26. C 3 9
  27. C 3 10
  28. C 3 11
  29. C 3 12
  30. C 3 13
  31. C 3 14
  32. A 1 0
  33. A 1 1
  34. A 1 2
  35. A 1 3
  36. A 1 4
  37. B 2 0
  38. B 2 1
  39. B 2 2
  40. B 2 3
  41. B 2 4
  42. B 2 5
  43. B 2 6
  44. B 2 7
  45. B 2 8
  46. B 2 9
  47. C 3 0
  48. C 3 1
  49. C 3 2
  50. C 3 3
  51. C 3 4
  52. C 3 5
  53. C 3 6
  54. C 3 7
  55. C 3 8
  56. C 3 9
  57. C 3 10
  58. C 3 11
  59. C 3 12
  60. C 3 13
  61. C 3 14

10. 线程通信之生产者消费者阻塞队列版

  1. import java.util.concurrent.ArrayBlockingQueue;
  2. import java.util.concurrent.BlockingQueue;
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.atomic.AtomicInteger;
  5. class MyResource {
  6. // 默认开启,进行生产消费
  7. // 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
  8. private volatile boolean FLAG = true;
  9. // 使用原子包装类,而不用number++
  10. private AtomicInteger atomicInteger = new AtomicInteger();
  11. // 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
  12. BlockingQueue<String> blockingQueue = null;
  13. // 而应该采用依赖注入里面的,构造注入方法传入
  14. public MyResource(BlockingQueue<String> blockingQueue) {
  15. this.blockingQueue = blockingQueue;
  16. // 查询出传入的class是什么
  17. System.out.println(blockingQueue.getClass().getName());
  18. }
  19. public void myProducer() throws Exception{
  20. String data = null;
  21. boolean retValue;
  22. // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
  23. // 当FLAG为true的时候,开始生产
  24. while(FLAG) {
  25. data = atomicInteger.incrementAndGet() + "";
  26. // 2秒存入1个data
  27. retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
  28. if(retValue) {
  29. System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "成功" );
  30. } else {
  31. System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "失败" );
  32. }
  33. try {
  34. TimeUnit.SECONDS.sleep(1);
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产介绍");
  40. }
  41. public void myConsumer() throws Exception{
  42. String retValue;
  43. // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
  44. // 当FLAG为true的时候,开始生产
  45. while(FLAG) {
  46. // 2秒存入1个data
  47. retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
  48. if(retValue != null && retValue != "") {
  49. System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue + "成功" );
  50. } else {
  51. FLAG = false;
  52. System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出" );
  53. // 退出消费队列
  54. return;
  55. }
  56. }
  57. }
  58. /**
  59. * 停止生产的判断
  60. */
  61. public void stop() {
  62. this.FLAG = false;
  63. }
  64. }
  65. public class ProducerConsumerWithBlockingQueueDemo {
  66. public static void main(String[] args) {
  67. // 传入具体的实现类, ArrayBlockingQueue
  68. MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(10));
  69. new Thread(() -> {
  70. System.out.println(Thread.currentThread().getName() + "\t 生产线程启动\n\n");
  71. try {
  72. myResource.myProducer();
  73. System.out.println("\n");
  74. } catch (Exception e) {
  75. e.printStackTrace();
  76. }
  77. }, "producer").start();
  78. new Thread(() -> {
  79. System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
  80. try {
  81. myResource.myConsumer();
  82. } catch (Exception e) {
  83. e.printStackTrace();
  84. }
  85. }, "consumer").start();
  86. // 5秒后,停止生产和消费
  87. try {
  88. TimeUnit.SECONDS.sleep(5);
  89. } catch (InterruptedException e) {
  90. e.printStackTrace();
  91. }
  92. System.out.println("\n\n5秒中后,生产和消费线程停止,线程结束");
  93. myResource.stop();
  94. }
  95. }

输出结果:

  1. java.util.concurrent.ArrayBlockingQueue
  2. producer 生产线程启动
  3. consumer 消费线程启动
  4. producer 插入队列:1成功
  5. consumer 消费队列:1成功
  6. producer 插入队列:2成功
  7. consumer 消费队列:2成功
  8. producer 插入队列:3成功
  9. consumer 消费队列:3成功
  10. producer 插入队列:4成功
  11. consumer 消费队列:4成功
  12. producer 插入队列:5成功
  13. consumer 消费队列:5成功
  14. 5秒中后,生产和消费线程停止,线程结束
  15. producer 停止生产,表示FLAG=false,生产介绍
  16. consumer 消费失败,队列中已为空,退出

11. Callable接口

Callable接口,是一种让线程执行完成后,能够返回结果的。

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. import java.util.concurrent.TimeUnit;
  5. class MyThread implements Callable<Integer> {
  6. @Override
  7. public Integer call() throws Exception {
  8. System.out.println(Thread.currentThread().getName() + " come in Callable");
  9. TimeUnit.SECONDS.sleep(2);
  10. return 1024;
  11. }
  12. }
  13. public class CallableDemo {
  14. public static void main(String[] args) throws InterruptedException, ExecutionException {
  15. FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
  16. new Thread(futureTask, "A").start();
  17. new Thread(futureTask, "B").start();//多个线程执行 一个FutureTask的时候,只会计算一次
  18. // 输出FutureTask的返回值
  19. System.out.println("result FutureTask " + futureTask.get());
  20. }
  21. }

7. 线程池

1. 线程池使用及优势

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
优点:

  1. 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    2. 线程池3个常用方式

    Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
    image.png

    了解

  • Executors.newScheduledThreadPool()
  • Executors.newWorkStealingPool(int) - Java8新增,使用目前机器上可用的处理器作为它的并行级别

    重点

    Executors.newSingleThreadExecutor()

    1. public static ExecutorService newSingleThreadExecutor() {
    2. return new FinalizableDelegatedExecutorService
    3. (new ThreadPoolExecutor(1, 1,
    4. 0L, TimeUnit.MILLISECONDS,
    5. new LinkedBlockingQueue<Runnable>()));
    6. }
    主要特点如下
  1. 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  2. newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。

Executors.newFixedThreadPool(int)

  1. public static ExecutorService newFixedThreadPool(int nThreads) {
  2. return new ThreadPoolExecutor(nThreads, nThreads,
  3. 0L, TimeUnit.MILLISECONDS,
  4. new LinkedBlockingQueue<Runnable>());
  5. }

主要特点如下:

  1. 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  2. newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。

Executors.newCachedThreadPool()

  1. public static ExecutorService newCachedThreadPool() {
  2. return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
  3. 60L, TimeUnit.SECONDS,
  4. new SynchronousQueue<Runnable>());
  5. }

主要特点如下:

  1. 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。 ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

public class ThreadPoolDemo { public static void main(String[] args) {

  1. // 一池5个处理线程(用池化技术,一定要记得关闭)

// ExecutorService threadPool = Executors.newFixedThreadPool(5);

  1. // 创建一个只有一个线程的线程池

// ExecutorService threadPool = Executors.newSingleThreadExecutor();

  1. // 创建一个拥有N个线程的线程池,根据调度创建合适的线程
  2. ExecutorService threadPool = Executors.newCachedThreadPool();
  3. // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
  4. try {
  5. // 循环十次,模拟业务办理,让5个线程处理这10个请求
  6. for (int i = 0; i < 10; i++) {
  7. final int tempInt = i;
  8. threadPool.execute(() -> {
  9. System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
  10. });
  11. }
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. } finally {
  15. threadPool.shutdown();
  16. }
  17. }

}

  1. 输出结果:
  2. ```java
  3. pool-1-thread-1 给用户:0 办理业务
  4. pool-1-thread-6 给用户:5 办理业务
  5. pool-1-thread-5 给用户:4 办理业务
  6. pool-1-thread-2 给用户:1 办理业务
  7. pool-1-thread-4 给用户:3 办理业务
  8. pool-1-thread-3 给用户:2 办理业务
  9. pool-1-thread-10 给用户:9 办理业务
  10. pool-1-thread-9 给用户:8 办理业务
  11. pool-1-thread-8 给用户:7 办理业务
  12. pool-1-thread-7 给用户:6 办理业务

3. 线程池7大参数入门简介

  1. public class ThreadPoolExecutor extends AbstractExecutorService {
  2. ...
  3. public ThreadPoolExecutor(int corePoolSize,
  4. int maximumPoolSize,
  5. long keepAliveTime,
  6. TimeUnit unit,
  7. BlockingQueue<Runnable> workQueue,
  8. ThreadFactory threadFactory,
  9. RejectedExecutionHandler handler) {
  10. if (corePoolSize < 0 ||
  11. maximumPoolSize <= 0 ||
  12. maximumPoolSize < corePoolSize ||
  13. keepAliveTime < 0)
  14. throw new IllegalArgumentException();
  15. if (workQueue == null || threadFactory == null || handler == null)
  16. throw new NullPointerException();
  17. this.acc = System.getSecurityManager() == null ?
  18. null :
  19. AccessController.getContext();
  20. this.corePoolSize = corePoolSize;
  21. this.maximumPoolSize = maximumPoolSize;
  22. this.workQueue = workQueue;
  23. this.keepAliveTime = unit.toNanos(keepAliveTime);
  24. this.threadFactory = threadFactory;
  25. this.handler = handler;
  26. }
  27. ...
  28. }

4. 线程池7大参数深入介绍

  1. corePoolSize:线程池中的常驻核心线程数
    1. 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
    2. 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间。
    1. 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
  4. unit:keepAliveTime的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
  7. handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)。

5. 线程池底层工作原理

image.png

  1. 在创建了线程池后,等待提交过来的任务请求。
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    3. 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。

6. 线程池的4种拒绝策略理论简介

等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。
这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK拒绝策略:

  • AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
  • CallerRunsPolicy:”调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了RejectedExecutionHandler接口。

7. 线程池实际中使用哪一个

超级大坑警告)你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?
答案是一个都不用,我们生产上只能使用自定义的
Executors 中JDK已经给你提供了,为什么不用?

3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 阿里巴巴《Java 开发手册》

8. 线程池的手写改造和拒绝策略

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. import java.util.concurrent.LinkedBlockingQueue;
  4. import java.util.concurrent.RejectedExecutionHandler;
  5. import java.util.concurrent.ThreadPoolExecutor;
  6. import java.util.concurrent.TimeUnit;
  7. public class MyThreadPoolExecutorDemo {
  8. public static void doSomething(ExecutorService executorService, int numOfRequest) {
  9. try {
  10. System.out.println(((ThreadPoolExecutor)executorService).getRejectedExecutionHandler().getClass() + ":");
  11. TimeUnit.SECONDS.sleep(1);
  12. for (int i = 0; i < numOfRequest; i++) {
  13. final int tempInt = i;
  14. executorService.execute(() -> {
  15. System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
  16. });
  17. }
  18. TimeUnit.SECONDS.sleep(1);
  19. System.out.println("\n\n");
  20. } catch (Exception e) {
  21. System.err.println(e);
  22. } finally {
  23. executorService.shutdown();
  24. }
  25. }
  26. public static ExecutorService newMyThreadPoolExecutor(int corePoolSize,
  27. int maximumPoolSize, int blockingQueueSize, RejectedExecutionHandler handler){
  28. return new ThreadPoolExecutor(
  29. corePoolSize,
  30. maximumPoolSize,
  31. 1,//keepAliveTime
  32. TimeUnit.SECONDS,
  33. new LinkedBlockingQueue<>(blockingQueueSize),
  34. Executors.defaultThreadFactory(),
  35. handler);
  36. }
  37. public static void main(String[] args) {
  38. doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.AbortPolicy()), 10);
  39. doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.CallerRunsPolicy()), 20);
  40. doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardOldestPolicy()), 10);
  41. doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardPolicy()), 10);
  42. }
  43. }

输出结果:

  1. class java.util.concurrent.ThreadPoolExecutor$AbortPolicy:
  2. pool-1-thread-1 给用户:0 办理业务
  3. pool-1-thread-3 给用户:5 办理业务java.util.concurrent.RejectedExecutionException: Task com.lun.concurrency.MyThreadPoolExecutorDemo$$Lambda$1/303563356@eed1f14 rejected from java.util.concurrent.ThreadPoolExecutor@7229724f[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 8]
  4. pool-1-thread-2 给用户:1 办理业务
  5. pool-1-thread-5 给用户:7 办理业务
  6. pool-1-thread-3 给用户:3 办理业务
  7. pool-1-thread-4 给用户:6 办理业务
  8. pool-1-thread-1 给用户:2 办理业务
  9. pool-1-thread-2 给用户:4 办理业务
  10. class java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy:
  11. pool-2-thread-1 给用户:0 办理业务
  12. pool-2-thread-2 给用户:1 办理业务
  13. pool-2-thread-1 给用户:2 办理业务
  14. pool-2-thread-3 给用户:5 办理业务
  15. pool-2-thread-3 给用户:7 办理业务
  16. pool-2-thread-3 给用户:9 办理业务
  17. pool-2-thread-4 给用户:6 办理业务
  18. pool-2-thread-2 给用户:3 办理业务
  19. pool-2-thread-5 给用户:8 办理业务
  20. main 给用户:10 办理业务
  21. pool-2-thread-1 给用户:4 办理业务
  22. pool-2-thread-3 给用户:11 办理业务
  23. pool-2-thread-4 给用户:13 办理业务
  24. main 给用户:14 办理业务
  25. pool-2-thread-1 给用户:12 办理业务
  26. pool-2-thread-5 给用户:15 办理业务
  27. pool-2-thread-2 给用户:17 办理业务
  28. main 给用户:18 办理业务
  29. pool-2-thread-3 给用户:16 办理业务
  30. pool-2-thread-4 给用户:19 办理业务
  31. class java.util.concurrent.ThreadPoolExecutor$DiscardOldestPolicy:
  32. pool-3-thread-1 给用户:0 办理业务
  33. pool-3-thread-2 给用户:1 办理业务
  34. pool-3-thread-1 给用户:2 办理业务
  35. pool-3-thread-2 给用户:3 办理业务
  36. pool-3-thread-3 给用户:5 办理业务
  37. pool-3-thread-5 给用户:8 办理业务
  38. pool-3-thread-2 给用户:7 办理业务
  39. pool-3-thread-4 给用户:6 办理业务
  40. pool-3-thread-1 给用户:4 办理业务
  41. pool-3-thread-3 给用户:9 办理业务
  42. class java.util.concurrent.ThreadPoolExecutor$DiscardPolicy:
  43. pool-4-thread-1 给用户:0 办理业务
  44. pool-4-thread-2 给用户:1 办理业务
  45. pool-4-thread-1 给用户:2 办理业务
  46. pool-4-thread-2 给用户:3 办理业务
  47. pool-4-thread-3 给用户:5 办理业务
  48. pool-4-thread-3 给用户:9 办理业务
  49. pool-4-thread-1 给用户:4 办理业务
  50. pool-4-thread-5 给用户:8 办理业务
  51. pool-4-thread-4 给用户:6 办理业务
  52. pool-4-thread-2 给用户:7 办理业务

9. 线程池配置合理线程数

合理配置线程池你是如何考虑的?

CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:(CPU核数+1)个线程的线程池

lO密集型

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/ (1-阻塞系数)
阻塞系数在0.8~0.9之间
比如8核CPU:8/(1-0.9)=80个线程数

8.JVM

1. 死锁编码及定位分析

是什么

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够碍到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
image.png

产生死锁主要原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

    发生死锁的四个条件

  1. 互斥条件,线程使用的资源至少有一个不能共享的。
  2. 至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
  3. 资源不能被抢占。
  4. 循环等待。

    如何解决死锁问题

    破坏发生死锁的四个条件其中之一即可。
    产生死锁的代码(根据发生死锁的四个条件): ```java package com.lun.concurrency;

import java.util.concurrent.TimeUnit;

class MyTask implements Runnable{

  1. private Object resourceA, resourceB;
  2. public MyTask(Object resourceA, Object resourceB) {
  3. this.resourceA = resourceA;
  4. this.resourceB = resourceB;
  5. }
  6. @Override
  7. public void run() {
  8. synchronized (resourceA) {
  9. System.out.println(String.format("%s 自己持有%s,尝试持有%s",//
  10. Thread.currentThread().getName(), resourceA, resourceB));
  11. try {
  12. TimeUnit.SECONDS.sleep(2);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. synchronized (resourceB) {
  17. System.out.println(String.format("%s 同时持有%s,%s",//
  18. Thread.currentThread().getName(), resourceA, resourceB));
  19. }
  20. }
  21. }

}

public class DeadLockDemo { public static void main(String[] args) { Object resourceA = new Object(); Object resourceB = new Object();

  1. new Thread(new MyTask(resourceA, resourceB),"Thread A").start();
  2. new Thread(new MyTask(resourceB, resourceA),"Thread B").start();
  3. }

}

  1. 输出结果:
  2. ```java
  3. Thread A 自己持有java.lang.Object@59d8d77,尝试持有java.lang.Object@7a15e6e6
  4. Thread B 自己持有java.lang.Object@7a15e6e6,尝试持有java.lang.Object@59d8d77

程序卡死,未出现同时持有的字样。

查看是否死锁工具

  1. jps命令定位进程号
  2. jstack找到死锁查看 ```java C:\Users\abc>jps -l 11968 com.lun.concurrency.DeadLockDemo 6100 jdk.jcmd/sun.tools.jps.Jps 6204 Eclipse

C:\Users\abc>jstack 11968 2021-03-09 02:42:46 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.251-b08 mixed mode):

“DestroyJavaVM” #13 prio=5 os_prio=0 tid=0x00000000004de800 nid=0x2524 waiting on condition [0 x0000000000000000] java.lang.Thread.State: RUNNABLE

“Thread B” #12 prio=5 os_prio=0 tid=0x000000001e0a5800 nid=0x6bc waiting for monitor entry [0x 000000001efae000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lun.concurrency.MyTask.run(DeadLockDemo.java:27)

  1. - waiting to lock <0x000000076b431d80> (a java.lang.Object)
  2. - locked <0x000000076b431d90> (a java.lang.Object)
  3. at java.lang.Thread.run(Thread.java:748)

“Thread A” #11 prio=5 os_prio=0 tid=0x000000001e0a4800 nid=0x650 waiting for monitor entry [0x 000000001eeae000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lun.concurrency.MyTask.run(DeadLockDemo.java:27)

  1. - waiting to lock <0x000000076b431d90> (a java.lang.Object)
  2. - locked <0x000000076b431d80> (a java.lang.Object)
  3. at java.lang.Thread.run(Thread.java:748)

“Service Thread” #10 daemon prio=9 os_prio=0 tid=0x000000001e034000 nid=0x2fb8 runnable [0x000 0000000000000] java.lang.Thread.State: RUNNABLE

“C1 CompilerThread3” #9 daemon prio=9 os_prio=2 tid=0x000000001dffa000 nid=0x26e8 waiting on c ondition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

“C2 CompilerThread2” #8 daemon prio=9 os_prio=2 tid=0x000000001dff6000 nid=0x484 waiting on co ndition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

“C2 CompilerThread1” #7 daemon prio=9 os_prio=2 tid=0x000000001dfe0800 nid=0x35c8 waiting on c ondition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

“C2 CompilerThread0” #6 daemon prio=9 os_prio=2 tid=0x000000001dfde800 nid=0x3b7c waiting on c ondition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

“Attach Listener” #5 daemon prio=5 os_prio=2 tid=0x000000001dfdd000 nid=0x3834 waiting on cond ition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

“Signal Dispatcher” #4 daemon prio=9 os_prio=2 tid=0x000000001dfdb000 nid=0x214 runnable [0x00 00000000000000] java.lang.Thread.State: RUNNABLE

“Finalizer” #3 daemon prio=8 os_prio=1 tid=0x000000001df70800 nid=0x2650 in Object.wait() [0x0 00000001e54f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)

  1. - waiting on <0x000000076b388ee0> (a java.lang.ref.ReferenceQueue$Lock)
  2. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
  3. - locked <0x000000076b388ee0> (a java.lang.ref.ReferenceQueue$Lock)
  4. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
  5. at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

“Reference Handler” #2 daemon prio=10 os_prio=2 tid=0x000000001c17d000 nid=0x1680 in Object.wa it() [0x000000001e44f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)

  1. - waiting on <0x000000076b386c00> (a java.lang.ref.Reference$Lock)
  2. at java.lang.Object.wait(Object.java:502)
  3. at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
  4. - locked <0x000000076b386c00> (a java.lang.ref.Reference$Lock)
  5. at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

“VM Thread” os_prio=2 tid=0x000000001c178000 nid=0x3958 runnable

“GC task thread#0 (ParallelGC)” os_prio=0 tid=0x0000000002667800 nid=0xd3c runnable

“GC task thread#1 (ParallelGC)” os_prio=0 tid=0x0000000002669000 nid=0x297c runnable

“GC task thread#2 (ParallelGC)” os_prio=0 tid=0x000000000266a800 nid=0x2fd0 runnable

“GC task thread#3 (ParallelGC)” os_prio=0 tid=0x000000000266c000 nid=0x1c90 runnable

“GC task thread#4 (ParallelGC)” os_prio=0 tid=0x000000000266f800 nid=0x3614 runnable

“GC task thread#5 (ParallelGC)” os_prio=0 tid=0x0000000002670800 nid=0x298c runnable

“GC task thread#6 (ParallelGC)” os_prio=0 tid=0x0000000002674000 nid=0x2b40 runnable

“GC task thread#7 (ParallelGC)” os_prio=0 tid=0x0000000002675000 nid=0x25f4 runnable

“VM Periodic Task Thread” os_prio=2 tid=0x000000001e097000 nid=0xd54 waiting on condition

JNI global references: 5

Found one Java-level deadlock:

“Thread B”: waiting to lock monitor 0x000000001e105dc8 (object 0x000000076b431d80, a java.lang.Object), which is held by “Thread A” “Thread A”: waiting to lock monitor 0x000000001c181828 (object 0x000000076b431d90, a java.lang.Object), which is held by “Thread B”

Java stack information for the threads listed above:

“Thread B”: at com.lun.concurrency.MyTask.run(DeadLockDemo.java:27)

  1. - waiting to lock <0x000000076b431d80> (a java.lang.Object)
  2. - locked <0x000000076b431d90> (a java.lang.Object)
  3. at java.lang.Thread.run(Thread.java:748)

“Thread A”: at com.lun.concurrency.MyTask.run(DeadLockDemo.java:27)

  1. - waiting to lock <0x000000076b431d90> (a java.lang.Object)
  2. - locked <0x000000076b431d80> (a java.lang.Object)
  3. at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

C:\Users\abc>

  1. <a name="y2zyv"></a>
  2. ## 2. JVMGC下半场技术加强说明和前提知识要求(暂无)
  3. <a name="MzgNv"></a>
  4. ## 3. JVMGC快速回顾复习串讲
  5. <a name="wC3KQ"></a>
  6. ### JVM内存结构
  7. <a name="RzLZl"></a>
  8. ### JVM体系概述
  9. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587325982-112d7d06-9d23-4a80-a06d-5fee2d998fa1.png#clientId=ubf15ffb7-458f-4&from=paste&id=u03ed677e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=475&originWidth=595&originalType=url&ratio=1&size=183429&status=done&style=none&taskId=uc6e3df98-f162-4fac-97e1-ee745471f4f)
  10. <a name="RRCCV"></a>
  11. ### Java8以后的JVM
  12. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587325889-a3844471-d759-47e7-b3fe-285bf663fc67.png#clientId=ubf15ffb7-458f-4&from=paste&id=ub319be23&margin=%5Bobject%20Object%5D&name=image.png&originHeight=257&originWidth=742&originalType=url&ratio=1&size=53152&status=done&style=none&taskId=ucbfba196-ccdc-4384-b4e9-d053a6a3aa6)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587326053-902bf45e-a829-4e43-8363-a64459aef283.png#clientId=ubf15ffb7-458f-4&from=paste&id=u4b6da750&margin=%5Bobject%20Object%5D&name=image.png&originHeight=463&originWidth=641&originalType=url&ratio=1&size=264310&status=done&style=none&taskId=u5470c0a1-97f7-4b9a-8852-c9bc2c9e866)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587326014-f3401ea4-f0f3-4f57-9a2b-7d83576ae80e.png#clientId=ubf15ffb7-458f-4&from=paste&id=u5391636a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=559&originWidth=734&originalType=url&ratio=1&size=227063&status=done&style=none&taskId=u9e2452ff-6cec-4207-8c06-bfcbb8e4d84)
  13. <a name="K4LW5"></a>
  14. ### 常见的垃圾回收算法
  15. <a name="JBgJu"></a>
  16. #### 1. 引用计数
  17. ![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587417144-fea7d14c-1a62-4b36-9ec8-d93309281a76.png#clientId=ubf15ffb7-458f-4&from=paste&height=465&id=udaf7bec7&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=465&originWidth=979&originalType=binary&ratio=1&size=239019&status=done&style=none&taskId=udc5de67f-db72-44ee-a66d-c1834fed76c&width=979)
  18. <a name="QP2ge"></a>
  19. #### 2. 复制
  20. Java堆从GC的角度还可以细分为: 新生代(Eden 区、From Survivor 区和To Survivor 区)和老年代。<br />![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587436116-a119cf90-d158-4c08-bea0-1d0d67dfadcc.png#clientId=ubf15ffb7-458f-4&from=paste&height=142&id=u4e16bcd7&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=142&originWidth=691&originalType=binary&ratio=1&size=50441&status=done&style=none&taskId=u0d65e786-3a60-4f45-8e00-21b8eee5394&width=691)<br />MinorGC的过程(复制->清空->互换):
  21. 1. Eden、SurvivorFrom复制到SurvivorTo,年龄+1
  22. 1. 首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1。
  23. 2. 清空eden-SurvivorErom
  24. 1. 然后,清空Eden和Survivor From中的对象,也即复制之后有交换,谁空谁是To。
  25. 3. Survivor To和 Survivor From互换
  26. 1. 最后,Survivor To和Survivor From互换,原SurvivorTo成为下一次GC时的Survivor From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由ⅣM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
  27. <a name="gvK9X"></a>
  28. #### 3. 标记清除
  29. 算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。<br />![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587529177-fbe2d53b-2d52-4948-b066-57b64f5c1300.png#clientId=ubf15ffb7-458f-4&from=paste&height=455&id=ucb6ad3d7&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=455&originWidth=976&originalType=binary&ratio=1&size=82820&status=done&style=none&taskId=u1e5d6188-99a2-47c5-b3db-b707c08b560&width=976)
  30. <a name="gzB5N"></a>
  31. #### 4. 标记整理
  32. ![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587578847-1458dc6d-eacf-4ec6-9cbf-0c977a561d85.png#clientId=ubf15ffb7-458f-4&from=paste&height=605&id=ud607b8a6&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=605&originWidth=856&originalType=binary&ratio=1&size=269493&status=done&style=none&taskId=u0931a6f8-730e-4891-9525-ef770e44aa8&width=856)
  33. <a name="Fi7N8"></a>
  34. ## 4. 谈谈你对GCRoots的理解
  35. <a name="eauw9"></a>
  36. ### 什么是垃圾?
  37. 简单的说就是内存中已经不再被使用到的空间就是垃圾。<br />要进行垃圾回收,如何判断一个对象是否可以被回收?
  38. - 引用计数法
  39. - 枚举根节点做可达性分析(根搜索路径)
  40. <a name="AylTG"></a>
  41. ### 引用计数法
  42. Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。<br />因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,<br />每当有一个地方引用它,计数器值加1,<br />每当有一个引用失效时,计数器值减1。<br />任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。<br />那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。<br />该算法存在但目前无人用了,解决不掉循环引用的问题,了解即可。<br />![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587701176-0b6594ff-c7e3-45c7-902c-3e343b197064.png#clientId=ubf15ffb7-458f-4&from=paste&height=463&id=uc681a1c3&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=463&originWidth=963&originalType=binary&ratio=1&size=239947&status=done&style=none&taskId=u0aa7fd92-94a0-448c-adf1-dc5186704d1&width=963)
  43. <a name="GQq7Z"></a>
  44. ### 枚举根节点做可达性分析(根搜索路径)
  45. 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。<br />![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587736604-6e29657f-c786-4272-866a-cfaa949f926e.png#clientId=ubf15ffb7-458f-4&from=paste&height=377&id=uS8Vz&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=377&originWidth=698&originalType=binary&ratio=1&size=142514&status=done&style=none&taskId=u262fc6b0-5f92-4df4-a0c7-8bdbca4f4ab&width=698)<br />![图片.png](https://cdn.nlark.com/yuque/0/2021/png/12495364/1633587754767-45000575-bbfc-44f8-95ab-12f4cb6cfbe3.png#clientId=ubf15ffb7-458f-4&from=paste&height=516&id=uf8f94453&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=516&originWidth=720&originalType=binary&ratio=1&size=13551&status=done&style=none&taskId=ud42bae07-7392-4f01-86a3-aa7bca47b3f&width=720)<br />所谓“GC roots”或者说tracing GC的“根集合”就是一组必须活跃的引用。<br />基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。
  46. Java中可以作为GC Roots的对象
  47. - 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  48. - 方法区中的类静态属性引用的对象。
  49. - 方法区中常量引用的对象。
  50. - 本地方法栈中JNI(Native方法)引用的对象。
  51. <a name="lW4GQ"></a>
  52. ## 5. JVM的标配参数和X参数
  53. [官方文档](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html)<br />JVM的参数类型:
  54. - 标配参数
  55. - -version java -version
  56. - -help
  57. - X参数(了解)
  58. - -Xint:解释执行
  59. - -Xcomp:第一次使用就编译成本地代码
  60. - -Xmixed:混合模式
  61. - XX参数(下一节)
  62. <a name="EF53L"></a>
  63. ## 6. JVM的XX参数之布尔类型
  64. 公式:-XX:+ 或者 - 某个属性值(+表示开启,-表示关闭)<br />如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?
  65. 1. jps -l 查看一个正在运行中的java程序,得到Java程序号。
  66. 1. jinfo -flag PrintGCDetails (Java程序号 )查看它的某个jvm参数(如PrintGCDetails )是否开启。
  67. 1. jinfo -flags (Java程序号 )查看它的所有jvm参数
  68. Case<br />是否打印GC收集细节
  69. - -XX:-PrintGCDetails
  70. - -XX:+PrintGCDetails
  71. 是否使用串行垃圾回收器
  72. - -XX:-UseSerialGC
  73. - -XX:+UserSerialGC
  74. <a name="e4Ycj"></a>
  75. ## 7. JVM的XX参数之设值类型
  76. 公式:-XX:属性key=属性值value<br />Case
  77. - -XX:MetaspaceSize=128m
  78. - -XX:MaxTenuringThreshold=15
  79. <a name="vBP57"></a>
  80. ## 8. JVM的XX参数之XmsXmx坑题
  81. <a name="NANKL"></a>
  82. ### 两个经典参数
  83. - -Xms等价于-XX:InitialHeapSize,初始大小内存,默认物理内存1/64
  84. - -Xmx等价于-XX:MaxHeapSize,最大分配内存,默认为物理内存1/4
  85. <a name="N6YBt"></a>
  86. ## 9. JVM盘点家底查看初始默认值
  87. <a name="vizhp"></a>
  88. ### 查看初始默认参数值
  89. -XX:+PrintFlagsInitial<br />公式:java -XX:+PrintFlagsInitial
  90. ```java
  91. C:\Users\abc>java -XX:+PrintFlagsInitial
  92. [Global flags]
  93. int ActiveProcessorCount = -1 {product} {default}
  94. uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
  95. uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
  96. uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} {default}
  97. uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
  98. uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
  99. uintx AdaptiveSizePolicyWeight = 10 {product} {default}
  100. ...

查看修改更新参数值

-XX:+PrintFlagsFinal
公式:java -XX:+PrintFlagsFinal

  1. C:\Users\abc>java -XX:+PrintFlagsFinal
  2. ...
  3. size_t HeapBaseMinAddress = 2147483648 {pd product} {default}
  4. bool HeapDumpAfterFullGC = false {manageable} {default}
  5. bool HeapDumpBeforeFullGC = false {manageable} {default}
  6. bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
  7. ccstr HeapDumpPath = {manageable} {default}
  8. uintx HeapFirstMaximumCompactionCount = 3 {product} {default}
  9. uintx HeapMaximumCompactionInterval = 20 {product} {default}
  10. uintx HeapSearchSteps = 3 {product} {default}
  11. size_t HeapSizePerGCThread = 43620760 {product} {default}
  12. bool IgnoreEmptyClassPaths = false {product} {default}
  13. bool IgnoreUnrecognizedVMOptions = false {product} {default}
  14. uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default}
  15. bool IncrementalInline = true {C2 product} {default}
  16. size_t InitialBootClassLoaderMetaspaceSize = 4194304 {product} {default}
  17. uintx InitialCodeCacheSize = 2555904 {pd product} {default}
  18. size_t InitialHeapSize := 268435456 {product} {ergonomic}
  19. ...

=表示默认,:=表示修改过的。

10. JVM盘点家底查看修改变更值

PrintFlagsFinal举例,运行java命令的同时打印出参数
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m HelloWorld

  1. ...
  2. size_t MetaspaceSize := 536870912 {pd product} {default}
  3. ...

打印命令行参数
-XX:+PrintCommandLineFlags

  1. C:\Users\abc>java -XX:+PrintCommandLineFlags -version
  2. -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
  3. 194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
  4. mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
  5. openjdk version "15.0.1" 2020-10-20
  6. OpenJDK Runtime Environment (build 15.0.1+9-18)
  7. OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)

11. 堆内存初始大小快速复习

JDK 1.8之后将最初的永久代取消了,由元空间取代。
图片.png
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间(Java8)与永久代(Java7)之间最大的区别在于:永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

  1. public class JVMMemorySizeDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 返回Java虚拟机中内存的总量
  4. long totalMemory = Runtime.getRuntime().totalMemory();
  5. // 返回Java虚拟机中试图使用的最大内存量
  6. long maxMemory = Runtime.getRuntime().maxMemory();
  7. System.out.println(String.format("TOTAL_MEMORY(-Xms): %d B, %.2f MB.", totalMemory, totalMemory / 1024.0 / 1024));
  8. System.out.println(String.format("MAX_MEMORY(-Xmx): %d B, %.2f MB.", maxMemory, maxMemory / 1024.0 / 1024));
  9. }
  10. }

输出结果:

  1. TOTAL_MEMORY(-Xms): 257425408 B, 245.50 MB.
  2. MAX_MEMORY(-Xmx): 3793747968 B, 3618.00 MB.

12. 常用基础参数栈内存Xss讲解

设置单个线程栈的大小,一般默认为512k~1024K
等价于-XX:ThreadStackSize

-XX:ThreadStackSize=size Sets the thread stack size (in bytes). Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The default value depends on virtual memory. The following examples show how to set the thread stack size to 1024 KB in different units:

  1. -XX:ThreadStackSize=1m
  2. -XX:ThreadStackSize=1024k
  3. -XX:ThreadStackSize=1048576

This option is equivalent to -Xss. 官方文档

13. 常用基础参数元空间MetaspaceSize讲解

-Xmn:设置年轻代大小
-XX:MetaspaceSize 设置元空间大小

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

典型设置案例
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails-XX:+UseSerialGC
[

](https://blog.csdn.net/u011863024/article/details/114684428)

14. 常用基础参数PrintGCDetails回收前后对比讲解

-XX:+PrintGCDetails 输出详细GC收集日志信息
设置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails 运行以下程序

  1. import java.util.concurrent.TimeUnit;
  2. public class PrintGCDetailsDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. byte[] byteArray = new byte[10 * 1024 * 1024];
  5. TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
  6. }
  7. }

输出结果:

  1. [GC (Allocation Failure) [PSYoungGen: 778K->480K(2560K)] 778K->608K(9728K), 0.0029909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  2. [GC (Allocation Failure) [PSYoungGen: 480K->480K(2560K)] 608K->616K(9728K), 0.0007890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  3. [Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 136K->518K(7168K)] 616K->518K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0058272 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  4. [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 518K->518K(9728K), 0.0002924 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 518K->506K(7168K)] 518K->506K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0056906 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
  6. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  7. at com.lun.jvm.PrintGCDetailsDemo.main(PrintGCDetailsDemo.java:9)
  8. Heap
  9. PSYoungGen total 2560K, used 61K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  10. eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd0f748,0x00000000fff00000)
  11. from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  12. to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  13. ParOldGen total 7168K, used 506K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  14. object space 7168K, 7% used [0x00000000ff600000,0x00000000ff67ea58,0x00000000ffd00000)
  15. Metaspace used 2676K, capacity 4486K, committed 4864K, reserved 1056768K
  16. class space used 285K, capacity 386K, committed 512K, reserved 1048576K

图片.png

15. 常用基础参数SurvivorRatio讲解

图片.png
调节新生代中 eden 和 S0、S1的空间比例,默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1
假如设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1
SurvivorRatio值就是设置eden区的比例占多少,S0和S1相同。

ratio
英 [ˈreɪʃiəʊ] 美 [ˈreɪʃioʊ]
n. 比率;比例

16. 常用基础参数NewRatio讲解

配置年轻代new 和老年代old 在堆结构的占比
默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3
-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,
NewRadio值就是设置老年代的占比,剩下的1个新生代。
新生代特别小,会造成频繁的进行GC收集。
[

](https://blog.csdn.net/u011863024/article/details/114684428)

17. 常用基础参数MaxTenuringThreshold讲解

晋升到老年代的对象年龄。

SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代。

这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念。

tenure 英 [ˈtenjə®] 美 [ˈtenjər] n. 任期;占有 vt. 授予…终身职位

threshold
英 [ˈθreʃhəʊld] 美 [ˈθreʃhoʊld]
n. 门槛;门口;阈;界;起始点;开端;起点;入门

9. 引用

1. 强引用Reference

Reference类以及继承派生的类
java.lang.ref.Reference 为 软(soft)引用、弱(weak)引用、虚(phantom)引用的父类。
因为Reference对象和垃圾回收密切配合实现,该类可能不能被直接子类化。
可以理解为Reference的直接子类都是由jvm定制化处理的,因此在代码中直接继承于Reference类型没有任何作用。但可以继承jvm定制的Reference的子类。
例如:Cleaner 继承了 PhantomReference

  1. public class Cleaner extends PhantomReference<Object>


图片.png
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

  1. // 这样定义的默认就是强应用
  2. Object obj1 = new Object();

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

构造函数

其内部提供2个构造函数,一个带queue,一个不带queue。其中queue的意义在于,我们可以在外部对这个queue进行监控。即如果有对象即将被回收,那么相应的reference对象就会被放到这个queue里。我们拿到reference,就可以再作一些事务。
而如果不带的话,就只有不断地轮询reference对象,通过判断里面的get是否返回null( phantomReference对象不能这样作,其get始终返回null,因此它只有带queue的构造函数 )。这两种方法均有相应的使用场景,取决于实际的应用。如weakHashMap中就选择去查询queue的数据,来判定是否有对象将被回收。而ThreadLocalMap,则采用判断get()是否为null来作处理。

  1. /* -- Constructors -- */
  2. Reference(T referent) {
  3. this(referent, null);
  4. }
  5. Reference(T referent, ReferenceQueue<? super T> queue) {
  6. this.referent = referent;
  7. this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
  8. }

如果我们在创建一个引用对象时,指定了ReferenceQueue,那么当引用对象指向的对象达到合适的状态(根据引用类型不同而不同)时,GC 会把引用对象本身添加到这个队列中,方便我们处理它,因为“引用对象指向的对象 GC 会自动清理,但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。”

Reference链表结构内部主要的成员有

① pending 和 discovered

  1. /* List of References waiting to be enqueued. The collector adds
  2. * References to this list, while the Reference-handler thread removes
  3. * them. This list is protected by the above lock object. The
  4. * list uses the discovered field to link its elements.
  5. */
  6. private static Reference<Object> pending = null;
  7. /* When active: next element in a discovered reference list maintained by GC (or this if last)
  8. * pending: next element in the pending list (or null if last)
  9. * otherwise: NULL
  10. */
  11. transient private Reference<T> discovered; /* used by VM */

可以理解为jvm在gc时会将要处理的对象放到这个静态字段上面。同时,另一个字段discovered:表示要处理的对象的下一个对象。即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered不断地拿到下一个对象赋值给pending即可,直到取到了最有一个。因为这个pending对象,两个线程都可能访问,因此需要加锁处理。

  1. if (pending != null) {
  2. r = pending;
  3. // 'instanceof' might throw OutOfMemoryError sometimes
  4. // so do this before un-linking 'r' from the 'pending' chain...
  5. c = r instanceof Cleaner ? (Cleaner) r : null;
  6. // unlink 'r' from 'pending' chain
  7. pending = r.discovered;
  8. r.discovered = null;

图片.png
② referent

private T referent; / Treated specially by GC /
👆referent字段由GC特别处理
referent:表示其引用的对象,即我们在构造的时候需要被包装在其中的对象。对象即将被回收的定义:此对象除了被reference引用之外没有其它引用了( 并非确实没有被引用,而是gcRoot可达性不可达,以避免循环引用的问题 )。如果一旦被回收,则会直接置为null,而外部程序可通过引用对象本身( 而不是referent,这里是reference#get() )了解到回收行为的产生( PhntomReference除外 )。

③ next

  1. /* When active: NULL
  2. * pending: this
  3. * Enqueued: next reference in queue (or this if last)
  4. * Inactive: this
  5. */
  6. @SuppressWarnings("rawtypes")
  7. Reference next;

next:即描述当前引用节点所存储的下一个即将被处理的节点。但next仅在放到queue中才会有意义( 因为,只有在enqueue的时候,会将next设置为下一个要处理的Reference对象 )。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的ENQUEUED。因为已经放到队列当中,并且不会再次放到队列当中。

④ discovered

  1. /* When active: next element in a discovered reference list maintained by GC (or this if last)
  2. * pending: next element in the pending list (or null if last)
  3. * otherwise: NULL
  4. */
  5. transient private Reference<T> discovered; /* used by VM */

👆被VM使用
discovered:当处于active状态时:discoverd reference的下一个元素是由GC操纵的( 如果是最后一个了则为this );当处于pending状态:discovered为pending集合中的下一个元素( 如果是最后一个了则为null );其他状态:discovered为null

⑤ lock

  1. static private class Lock { }
  2. private static Lock lock = new Lock();


lock:在垃圾收集中用于同步的对象。收集器必须获取该锁在每次收集周期开始时。因此这是至关重要的:任何持有该锁的代码应该尽快完成,不分配新对象,并且避免调用用户代码。

⑥ pending

  1. /* List of References waiting to be enqueued. The collector adds
  2. * References to this list, while the Reference-handler thread removes
  3. * them. This list is protected by the above lock object. The
  4. * list uses the discovered field to link its elements.
  5. */
  6. private static Reference<Object> pending = null;

pending:等待被入队的引用列表。收集器会添加引用到这个列表,直到Reference-handler线程移除了它们。这个列表被上面的lock对象保护。这个列表使用discovered字段来连接它自己的元素( 即pending的下一个元素就是discovered对象 )。

⑦ queue

volatile ReferenceQueue<? super T> queue;
queue:是对象即将被回收时所要通知的队列。当对象即被回收时,整个reference对象( 而不是被回收的对象 )会被放到queue里面,然后外部程序即可通过监控这个queue拿到相应的数据了。
这里的queue( 即,ReferenceQueue对象 )名义上是一个队列,但实际内部并非有实际的存储结构,它的存储是依赖于内部节点之间的关系来表达。可以理解为queue是一个类似于链表的结构,这里的节点其实就是reference本身。可以理解为queue为一个链表的容器,其自己仅存储当前的head节点,而后面的节点由每个reference节点自己通过next来保持即可。

  • Reference 实例( 即Reference中的真是引用对象referent )的4中可能的内部状态值
    Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:
    • Active:新创建的引用对象都是这个状态,在 GC 检测到引用对象已经到达合适的reachability时,GC 会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。
    • Pending:pending-Reference列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。
    • Enqueued:相应的对象已经为待回收,并且相应的引用对象已经放到queue当中了。准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqueued方法后的Reference处于这个状态中。当Reference实例从它的ReferenceQueue移除后,它将成为Inactive。没有注册的实例不会进入这个状态。
    • Inactive:即此对象已经由外部从queue中获取到,并且已经处理掉了。即意味着此引用对象可以被回收,并且对内部封装的对象也可以被回收掉了( 实际的回收运行取决于clear动作是否被调用 )。可以理解为进入到此状态的肯定是应该被回收掉的。一旦一个Reference实例变为了Inactive,它的状态将不会再改变。


jvm并不需要定义状态值来判断相应引用的状态处于哪个状态,只需要通过计算next和queue即可进行判断。

  • Active:queue为创建一个Reference对象时传入的ReferenceQueue对象;如果ReferenceQueue对象为空或者没有传入ReferenceQueue对象,则为ReferenceQueue.NULL;next==null;
  • Pending:queue为初始化时传入ReferenceQueue对象;next==this(由jvm设置);
  • Enqueue:当queue!=null && queue != ENQUEUED 时;设置queue为ENQUEUED;next为下一个要处理的reference对象,或者若为最后一个了next==this;
  • Inactive:queue = ReferenceQueue.NULL; next = this.

    通过这个组合,收集器只需要检测next属性为了决定是否一个Reference实例需要特殊的处理:如果next==null,则实例是active;如果next!=null,为了确保并发收集器能够发现active的Reference对象,而不会影响可能将enqueue()方法应用于这些对象的应用程序线程,收集器应通过discovered字段链接发现的对象。discovered字段也用于链接pending列表中的引用对象。
    图片.png
    👆外部从queue中获取Reference

  • WeakReference对象进入到queue之后,相应的referent为null。

  • SoftReference对象,如果对象在内存足够时,不会进入到queue,自然相应的referent不会为null。如果需要被处理( 内存不够或其它策略 ),则置相应的referent为null,然后进入到queue。通过debug发现,SoftReference是pending状态时,referent就已经是null了,说明此事referent已经被GC回收了。
  • FinalReference对象,因为需要调用其finalize对象,因此其reference即使入queue,其referent也不会为null,即不会clear掉。
  • PhantomReference对象,因为本身get实现为返回null。因此clear的作用不是很大。因为不管enqueue还是没有,都不会清除掉。

Q:👆如果PhantomReference对象不管enqueue还是没有,都不会清除掉reference对象,那么怎么办?这个reference对象不就一直存在这了??而且JVM是会直接通过字段操作清除相应引用的,那么是不是JVM已经释放了系统底层资源,但java代码中该引用还未置null??
A:不会的,虽然PhantomReference有时候不会调用clear,如Cleaner对象 。但Cleaner的clean()方法只调用了remove(this),这样当clean()执行完后,Cleaner就是一个无引用指向的对象了,也就是可被GC回收的对象。

active ——> pending :Reference#tryHandlePending
pending ——> enqueue :ReferenceQueue#enqueue
enqueue ——> inactive :Reference#clear

图片.png

重要方法

① clear()

  1. /**
  2. * Clears this reference object. Invoking this method will not cause this
  3. * object to be enqueued.
  4. *
  5. * <p> This method is invoked only by Java code; when the garbage collector
  6. * clears references it does so directly, without invoking this method.
  7. */
  8. public void clear() {
  9. this.referent = null;
  10. }

调用此方法不会导致此对象入队。此方法仅由Java代码调用;当垃圾收集器清除引用时,它直接执行,而不调用此方法。
clear的语义就是将referent置null。
清除引用对象所引用的原对象,这样通过get()方法就不能再访问到原对象了( PhantomReference除外 )。从相应的设计思路来说,既然都进入到queue对象里面,就表示相应的对象需要被回收了,因为没有再访问原对象的必要。此方法不会由JVM调用,而JVM是直接通过字段操作清除相应的引用,其具体实现与当前方法相一致。

② ReferenceHandler线程

  1. static {
  2. ThreadGroup tg = Thread.currentThread().getThreadGroup();
  3. for (ThreadGroup tgn = tg;
  4. tgn != null;
  5. tg = tgn, tgn = tg.getParent());
  6. Thread handler = new ReferenceHandler(tg, "Reference Handler");
  7. /* If there were a special system-only priority greater than
  8. * MAX_PRIORITY, it would be used here
  9. */
  10. handler.setPriority(Thread.MAX_PRIORITY);
  11. handler.setDaemon(true);
  12. handler.start();
  13. // provide access in SharedSecrets
  14. SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
  15. @Override
  16. public boolean tryHandlePendingReference() {
  17. return tryHandlePending(false);
  18. }
  19. });
  20. }

其优先级最高,可以理解为需要不断地处理引用对象。

  1. private static class ReferenceHandler extends Thread {
  2. private static void ensureClassInitialized(Class<?> clazz) {
  3. try {
  4. Class.forName(clazz.getName(), true, clazz.getClassLoader());
  5. } catch (ClassNotFoundException e) {
  6. throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
  7. }
  8. }
  9. static {
  10. // pre-load and initialize InterruptedException and Cleaner classes
  11. // so that we don't get into trouble later in the run loop if there's
  12. // memory shortage while loading/initializing them lazily.
  13. ensureClassInitialized(InterruptedException.class);
  14. ensureClassInitialized(Cleaner.class);
  15. }
  16. ReferenceHandler(ThreadGroup g, String name) {
  17. super(g, name);
  18. }
  19. public void run() {
  20. while (true) {
  21. tryHandlePending(true);
  22. }
  23. }
  24. }

③ tryHandlePending()

  1. /**
  2. * Try handle pending {@link Reference} if there is one.<p>
  3. * Return {@code true} as a hint that there might be another
  4. * {@link Reference} pending or {@code false} when there are no more pending
  5. * {@link Reference}s at the moment and the program can do some other
  6. * useful work instead of looping.
  7. *
  8. * @param waitForNotify if {@code true} and there was no pending
  9. * {@link Reference}, wait until notified from VM
  10. * or interrupted; if {@code false}, return immediately
  11. * when there is no pending {@link Reference}.
  12. * @return {@code true} if there was a {@link Reference} pending and it
  13. * was processed, or we waited for notification and either got it
  14. * or thread was interrupted before being notified;
  15. * {@code false} otherwise.
  16. */
  17. static boolean tryHandlePending(boolean waitForNotify) {
  18. Reference<Object> r;
  19. Cleaner c;
  20. try {
  21. synchronized (lock) {
  22. if (pending != null) {
  23. r = pending;
  24. // 'instanceof' might throw OutOfMemoryError sometimes
  25. // so do this before un-linking 'r' from the 'pending' chain...
  26. c = r instanceof Cleaner ? (Cleaner) r : null;
  27. // unlink 'r' from 'pending' chain
  28. pending = r.discovered;
  29. r.discovered = null;
  30. } else {
  31. // The waiting on the lock may cause an OutOfMemoryError
  32. // because it may try to allocate exception objects.
  33. if (waitForNotify) {
  34. lock.wait();
  35. }
  36. // retry if waited
  37. return waitForNotify;
  38. }
  39. }
  40. } catch (OutOfMemoryError x) {
  41. // Give other threads CPU time so they hopefully drop some live references
  42. // and GC reclaims some space.
  43. // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
  44. // persistently throws OOME for some time...
  45. Thread.yield();
  46. // retry
  47. return true;
  48. } catch (InterruptedException x) {
  49. // retry
  50. return true;
  51. }
  52. // Fast path for cleaners
  53. if (c != null) {
  54. c.clean();
  55. return true;
  56. }
  57. ReferenceQueue<? super Object> q = r.queue;
  58. if (q != ReferenceQueue.NULL) q.enqueue(r);
  59. return true;
  60. }

这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。
由此可见,pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。并且这里enqueue的队列是我们在初始化( 构造函数 )Reference对象时传进来的queue,如果传入了null( 实际使用的是ReferenceQueue.NULL ),则ReferenceHandler则不进行enqueue操作,所以只有非RefernceQueue.NULL的queue才会将Reference进行enqueue。
ReferenceQueue是作为 JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理

2. 软引用SoftReference

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,

  • 当系统内存充足时它不会被回收,
  • 当系统内存不足时它会被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

当内存充足的时候,软引用不用回收

  1. public class SoftReferenceDemo {
  2. /**
  3. * 内存够用的时候
  4. * -XX:+PrintGCDetails
  5. */
  6. public static void softRefMemoryEnough() {
  7. // 创建一个强应用
  8. Object o1 = new Object();
  9. // 创建一个软引用
  10. SoftReference<Object> softReference = new SoftReference<>(o1);
  11. System.out.println(o1);
  12. System.out.println(softReference.get());
  13. o1 = null;
  14. // 手动GC
  15. System.gc();
  16. System.out.println(o1);
  17. System.out.println(softReference.get());
  18. }
  19. /**
  20. * JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
  21. * -Xms5m -Xmx5m -XX:+PrintGCDetails
  22. */
  23. public static void softRefMemoryNoEnough() {
  24. System.out.println("========================");
  25. // 创建一个强应用
  26. Object o1 = new Object();
  27. // 创建一个软引用
  28. SoftReference<Object> softReference = new SoftReference<>(o1);
  29. System.out.println(o1);
  30. System.out.println(softReference.get());
  31. o1 = null;
  32. // 模拟OOM自动GC
  33. try {
  34. // 创建30M的大对象
  35. byte[] bytes = new byte[30 * 1024 * 1024];
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. } finally {
  39. System.out.println(o1);
  40. System.out.println(softReference.get());
  41. }
  42. }
  43. public static void main(String[] args) {
  44. softRefMemoryEnough();
  45. //softRefMemoryNoEnough();
  46. }
  47. }

内存充足输出结果

  1. java.lang.Object@15db9742
  2. java.lang.Object@15db9742
  3. [GC (System.gc()) [PSYoungGen: 2621K->728K(76288K)] 2621K->736K(251392K), 0.0011732 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [Full GC (System.gc()) [PSYoungGen: 728K->0K(76288K)] [ParOldGen: 8K->519K(175104K)] 736K->519K(251392K), [Metaspace: 2646K->2646K(1056768K)], 0.0048782 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. null
  6. java.lang.Object@15db9742
  7. Heap
  8. PSYoungGen total 76288K, used 1966K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  9. eden space 65536K, 3% used [0x000000076b380000,0x000000076b56ba70,0x000000076f380000)
  10. from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  11. to space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
  12. ParOldGen total 175104K, used 519K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  13. object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1a81e88,0x00000006cc500000)
  14. Metaspace used 2653K, capacity 4486K, committed 4864K, reserved 1056768K
  15. class space used 282K, capacity 386K, committed 512K, reserved 1048576K

内存不充足,软引用关联对象会被回收

  1. ========================
  2. java.lang.Object@15db9742
  3. java.lang.Object@15db9742
  4. [GC (Allocation Failure) [PSYoungGen: 756K->496K(1536K)] 756K->600K(5632K), 0.0009017 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [GC (Allocation Failure) [PSYoungGen: 496K->480K(1536K)] 600K->624K(5632K), 0.0006772 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [Full GC (Allocation Failure) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 144K->519K(4096K)] 624K->519K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0055489 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  7. [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 519K->519K(5632K), 0.0002674 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 519K->507K(4096K)] 519K->507K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0052951 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
  9. null
  10. null
  11. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  12. at com.lun.jvm.SoftReferenceDemo.softRefMemoryNotEnough(SoftReferenceDemo.java:44)
  13. at com.lun.jvm.SoftReferenceDemo.main(SoftReferenceDemo.java:58)
  14. Heap
  15. PSYoungGen total 1536K, used 30K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  16. eden space 1024K, 2% used [0x00000000ffe00000,0x00000000ffe07ac8,0x00000000fff00000)
  17. from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  18. to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  19. ParOldGen total 4096K, used 507K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  20. object space 4096K, 12% used [0x00000000ffa00000,0x00000000ffa7edd0,0x00000000ffe00000)
  21. Metaspace used 2678K, capacity 4486K, committed 4864K, reserved 1056768K
  22. class space used 285K, capacity 386K, committed 512K, reserved 1048576K

回收后,内存依然不足的话,还是会抛异常。

3. 弱引用WeakReference

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,
对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存。

  1. import java.lang.ref.WeakReference;
  2. public class WeakReferenceDemo {
  3. public static void main(String[] args) {
  4. Object o1 = new Object();
  5. WeakReference<Object> weakReference = new WeakReference<>(o1);
  6. System.out.println(o1);
  7. System.out.println(weakReference.get());
  8. o1 = null;
  9. System.gc();
  10. System.out.println(o1);
  11. System.out.println(weakReference.get());
  12. }
  13. }

输出结果:

  1. java.lang.Object@15db9742
  2. java.lang.Object@15db9742
  3. null
  4. null

4. 软引用和弱引用的适用场景

场景:假如有一个应用需要读取大量的本地图片

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中,又可能造成内存溢出

此时使用软引用可以解决这个问题。
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题

  1. Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

5. WeakHashMap案例演示和解析

详细 https://www.yuque.com/alibas/zcgitz/atdx7w

Hash table based implementation of the Map interface, with weak keys. An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded its entry is effectively removed from the map, so this class behaves somewhat differently from other Map implementations. link

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.WeakHashMap;
  4. public class WeakHashMapDemo {
  5. public static void main(String[] args) {
  6. myHashMap();
  7. System.out.println("==========");
  8. myWeakHashMap();
  9. }
  10. private static void myHashMap() {
  11. Map<Integer, String> map = new HashMap<>();
  12. Integer key = new Integer(1);
  13. String value = "HashMap";
  14. map.put(key, value);
  15. System.out.println(map);
  16. key = null;
  17. System.gc();
  18. System.out.println(map);
  19. }
  20. private static void myWeakHashMap() {
  21. Map<Integer, String> map = new WeakHashMap<>();
  22. Integer key = new Integer(1);
  23. String value = "WeakHashMap";
  24. map.put(key, value);
  25. System.out.println(map);
  26. key = null;
  27. System.gc();
  28. System.out.println(map);
  29. }
  30. }

输出结果:

  1. {1=HashMap}
  2. {1=HashMap}
  3. ==========
  4. {1=WeakHashMap}
  5. {}

6. 虚引用简介

虚引用需要java.lang.ref.PhantomReference类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的gei方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比fihalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

7. ReferenceQueue引用队列介

详细 https://www.yuque.com/alibas/zcgitz/atdx7w

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected. link

回收前需要被引用的,用队列保存下。

  1. import java.lang.ref.ReferenceQueue;
  2. import java.lang.ref.WeakReference;
  3. import java.util.concurrent.TimeUnit;
  4. public class ReferenceQueueDemo {
  5. public static void main(String[] args) {
  6. Object o1 = new Object();
  7. // 创建引用队列
  8. ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
  9. // 创建一个弱引用
  10. WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
  11. System.out.println(o1);
  12. System.out.println(weakReference.get());
  13. // 取队列中的内容
  14. System.out.println(referenceQueue.poll());
  15. System.out.println("==================");
  16. o1 = null;
  17. System.gc();
  18. System.out.println("执行GC操作");
  19. try {
  20. TimeUnit.SECONDS.sleep(2);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. System.out.println(o1);
  25. System.out.println(weakReference.get());
  26. // 取队列中的内容
  27. System.out.println(referenceQueue.poll());
  28. }
  29. }

输出结果:

  1. java.lang.Object@15db9742
  2. java.lang.Object@15db9742
  3. null
  4. ==================
  5. 执行GC操作
  6. null
  7. null
  8. java.lang.ref.WeakReference@6d06d69c

8. 虚引用PhantomReference

Java提供了4种引用类型,在垃圾回收的时候,都有自己各自的特点。
ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。
创建引用的时候可以指定关联的队列,当Gc释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。

  1. import java.lang.ref.PhantomReference;
  2. import java.lang.ref.ReferenceQueue;
  3. public class PhantomReferenceDemo {
  4. public static void main(String[] args) throws InterruptedException {
  5. Object o1 = new Object();
  6. ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
  7. PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);
  8. System.out.println(o1);
  9. System.out.println(phantomReference.get());
  10. System.out.println(referenceQueue.poll());
  11. System.out.println("==================");
  12. o1 = null;
  13. System.gc();
  14. Thread.sleep(500) ;
  15. System.out.println(o1);
  16. System.out.println(phantomReference.get());
  17. System.out.println(referenceQueue.poll());
  18. }
  19. }

输出结果:

  1. java.lang.Object@15db9742
  2. null
  3. null
  4. ==================
  5. null
  6. null
  7. java.lang.ref.PhantomReference@6d06d69c

10. GCRoots和四大引用小总结

图片.png

11. JVM常见错误

1. SOFE之StackOverflowError

JVM中常见的两种错误

StackoverFlowError

  • java.lang.StackOverflowError

OutofMemoryError

  • java.lang.OutOfMemoryError:java heap space
  • java.lang.OutOfMemoryError:GC overhead limit exceeeded
  • java.lang.OutOfMemoryError:Direct buffer memory
  • java.lang.OutOfMemoryError:unable to create new native thread
  • java.lang.OutOfMemoryError:Metaspace

[

](https://blog.csdn.net/u011863024/article/details/114684428)
图片.png

StackOverflowError的展现

  1. public class StackOverflowErrorDemo {
  2. public static void main(String[] args) {
  3. main(args);
  4. }
  5. }

输出结果:

  1. Exception in thread "main" java.lang.StackOverflowError
  2. at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
  3. at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
  4. at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
  5. ...

2. OOM之Java heap space

  1. public class OOMEJavaHeapSpaceDemo {
  2. /**
  3. *
  4. * -Xms10m -Xmx10m
  5. *
  6. * @param args
  7. */
  8. public static void main(String[] args) {
  9. byte[] array = new byte[80 * 1024 * 1024];
  10. }
  11. }

输出结果:

  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  2. at com.lun.jvm.OOMEJavaHeapSpaceDemo.main(OOMEJavaHeapSpaceDemo.java:6)

3. OOM之GC overhead limit exceeded

GC overhead limit exceeded 超出GC开销限制

GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。

假如不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使cc再次执行。这样就形成恶性循环,CPU使用率一直是100%,而Gc却没有任何成果。
图片.png

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. public class OOMEGCOverheadLimitExceededDemo {
  4. /**
  5. *
  6. * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
  7. *
  8. * @param args
  9. */
  10. public static void main(String[] args) {
  11. int i = 0;
  12. List<String> list = new ArrayList<>();
  13. try {
  14. while(true) {
  15. list.add(String.valueOf(++i).intern());
  16. }
  17. } catch (Exception e) {
  18. System.out.println("***************i:" + i);
  19. e.printStackTrace();
  20. throw e;
  21. }
  22. }
  23. }

输出结果

  1. [GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1658K(9728K), 0.0033090 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  2. [GC (Allocation Failure) [PSYoungGen: 2323K->489K(2560K)] 3483K->3305K(9728K), 0.0020911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  3. [GC (Allocation Failure) [PSYoungGen: 2537K->496K(2560K)] 5353K->4864K(9728K), 0.0025591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [PSYoungGen: 2410K->512K(2560K)] 6779K->6872K(9728K), 0.0058689 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
  5. [Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6360K->6694K(7168K)] 6872K->6694K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0894928 secs] [Times: user=0.42 sys=0.00, real=0.09 secs]
  6. [Full GC (Ergonomics) [PSYoungGen: 2048K->1421K(2560K)] [ParOldGen: 6694K->6902K(7168K)] 8742K->8324K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0514932 secs] [Times: user=0.34 sys=0.00, real=0.05 secs]
  7. [Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6902K->6902K(7168K)] 8950K->8950K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0381615 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
  8. ...省略89行...
  9. [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7044K->7044K(7168K)] 9092K->9092K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360935 secs] [Times: user=0.25 sys=0.00, real=0.04 secs]
  10. [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7046K->7046K(7168K)] 9094K->9094K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360458 secs] [Times: user=0.38 sys=0.00, real=0.04 secs]
  11. [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7048K->7048K(7168K)] 9096K->9096K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0353033 secs] [Times: user=0.11 sys=0.00, real=0.04 secs]
  12. ***************i:147041
  13. [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7050K->7048K(7168K)] 9098K->9096K(9728K), [Metaspace: 2670K->2670K(1056768K)], 0.0371397 secs] [Times: user=0.22 sys=0.00, real=0.04 secs]
  14. java.lang.OutOfMemoryError: GC overhead limit exceeded
  15. [Full GC (Ergonomics) at java.lang.Integer.toString(Integer.java:401)
  16. [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7051K->7050K(7168K)] 9099K->9097K(9728K), [Metaspace: 2676K->2676K(1056768K)], 0.0434184 secs] [Times: user=0.38 sys=0.00, real=0.04 secs]
  17. at java.lang.String.valueOf(String.java:3099)
  18. at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
  19. Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
  20. [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7054K->513K(7168K)] 9102K->513K(9728K), [Metaspace: 2677K->2677K(1056768K)], 0.0056578 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
  21. at java.lang.Integer.toString(Integer.java:401)
  22. at java.lang.String.valueOf(String.java:3099)
  23. at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
  24. Heap
  25. PSYoungGen total 2560K, used 46K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  26. eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0bb90,0x00000000fff00000)
  27. from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  28. to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  29. ParOldGen total 7168K, used 513K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  30. object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6807f0,0x00000000ffd00000)
  31. Metaspace used 2683K, capacity 4486K, committed 4864K, reserved 1056768K
  32. class space used 285K, capacity 386K, committed 512K, reserved 1048576K

4. OOM之Direct buffer memory

导致原因

写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避兔了在Java堆和Native堆中来回复制数据。

  • ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
  • ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

但如果不断分配本地内存,堆内存很少使用,那么JV就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。

  1. import java.nio.ByteBuffer;
  2. import java.util.concurrent.TimeUnit;
  3. public class OOMEDirectBufferMemoryDemo {
  4. /**
  5. * -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
  6. *
  7. * @param args
  8. * @throws InterruptedException
  9. */
  10. public static void main(String[] args) throws InterruptedException {
  11. System.out.println(String.format("配置的maxDirectMemory: %.2f MB",//
  12. sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
  13. TimeUnit.SECONDS.sleep(3);
  14. ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
  15. }
  16. }

输出结果:

  1. [GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
  2. 配置的maxDirectMemory: 5.00 MB
  3. [GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  5. Exception in thread "main" Heap
  6. PSYoungGen total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  7. eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
  8. from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  9. to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  10. ParOldGen total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  11. object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
  12. Metaspace used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
  13. class space used 391K, capacity 394K, committed 512K, reserved 1048576K
  14. java.lang.OutOfMemoryError: Direct buffer memory
  15. at java.nio.Bits.reserveMemory(Bits.java:694)
  16. at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
  17. at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
  18. at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)

5. OOM之unable to create new native thread故障演示

不能够创建更多的新的线程了,也就是说创建线程的上限达到了
高并发请求服务器时,经常会出现异常java.lang.OutOfMemoryError:unable to create new native thread,准确说该native thread异常与对应的平台有关

导致原因

  • 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
  • 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

    解决方法

  • 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低

  • 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改Linux服务器配置,扩大linux默认限制
    1. public class OOMEUnableCreateNewThreadDemo {
    2. public static void main(String[] args) {
    3. for (int i = 0; ; i++) {
    4. System.out.println("************** i = " + i);
    5. new Thread(() -> {
    6. try {
    7. TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }, String.valueOf(i)).start();
    12. }
    13. }
    14. }
    上面程序在Linux OS(CentOS)运行,会出现下列的错误,线程数大概在900多个
    1. Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread

    6. OOM之unable to create new native thread上限调整

    非root用户登录Linux系统(CentOS)测试
    服务器级别调参调优
    查看系统线程限制数目
    1. ulimit -u
    修改系统线程限制数目
    1. vim /etc/security/limits.d/90-nproc.conf
    打开后发现除了root,其他账户都限制在1024个
    图片.png
    假如我们想要张三这个用卢运行,希望他生成的线程多一些,我们可以如下配置
    图片.png

    7. OOM之Metaspace

    使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(大约20.8M)

Java 8及之后的版本使用Metaspace来替代永久代。

Metaspace是方法区在Hotspot 中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在Java8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace native memory。
永久代(Java8后被原空向Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们借助CGLib直接操作字节码运行时不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。

首先添加CGLib依赖

  1. <!-- https://mvnrepository.com/artifact/cglib/cglib -->
  2. <dependency>
  3. <groupId>cglib</groupId>
  4. <artifactId>cglib</artifactId>
  5. <version>3.2.10</version>
  6. </dependency>
  1. import java.lang.reflect.Method;
  2. import net.sf.cglib.proxy.Enhancer;
  3. import net.sf.cglib.proxy.MethodInterceptor;
  4. import net.sf.cglib.proxy.MethodProxy;
  5. public class OOMEMetaspaceDemo {
  6. // 静态类
  7. static class OOMObject {}
  8. /**
  9. * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
  10. *
  11. * @param args
  12. */
  13. public static void main(final String[] args) {
  14. // 模拟计数多少次以后发生异常
  15. int i =0;
  16. try {
  17. while (true) {
  18. i++;
  19. // 使用Spring的动态字节码技术
  20. Enhancer enhancer = new Enhancer();
  21. enhancer.setSuperclass(OOMObject.class);
  22. enhancer.setUseCache(false);
  23. enhancer.setCallback(new MethodInterceptor() {
  24. @Override
  25. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  26. return methodProxy.invokeSuper(o, args);
  27. }
  28. });
  29. enhancer.create();
  30. }
  31. } catch (Throwable e) {
  32. System.out.println("发生异常的次数:" + i);
  33. e.printStackTrace();
  34. } finally {
  35. }
  36. }
  37. }

输出结果

  1. 发生异常的次数:569
  2. java.lang.OutOfMemoryError: Metaspace
  3. at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
  4. at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
  5. at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
  6. at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
  7. at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
  8. at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
  9. at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)

12. 垃圾收集器

1. 垃圾收集器回收种类

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。
因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集
4种主要垃圾收集器

  • Serial
  • Parallel
  • CMS
  • G1

图片.png

2. 串行并行并发G1四大垃圾回收方式

  • 串行垃级回收器(Serial) - 它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境。
  • 并行垃圾回收器(Parallel) - 多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短。
  • 并发垃圾回收器(CMS) - 用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。
  • G1垃圾回收器 - G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
  • ZGC(Java 11的,了解)

串行,并行,并发GC小总结(G1稍后)
图片.png

3. 如何查看默认的垃圾收集器

  1. java -XX:+PrintCommandLineFlags -version

输出结果

  1. C:\Users\abc>java -XX:+PrintCommandLineFlags -version
  2. -XX:InitialHeapSize=266613056 -XX:MaxHeapSize=4265808896 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
  3. java version "1.8.0_251"
  4. Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
  5. Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

从结果看到-XX:+UseParallelGC,也就是说默认的垃圾收集器是并行垃圾回收器。
或者

  1. jps -l

得出Java程序号

  1. jinfo -flags (Java程序号)

4. JVM默认的垃圾收集器有哪些

Java中一共有7大垃圾收集器

  • 年轻代GC
    • UserSerialGC:串行垃圾收集器
    • UserParallelGC:并行垃圾收集器
    • UseParNewGC:年轻代的并行垃圾回收器
  • 老年代GC
    • UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
    • UseParallelOldGC:老年代的并行垃圾回收器
    • UseConcMarkSweepGC:(CMS)并发标记清除
  • 老嫩通吃

    • UseG1GC:G1垃圾收集器

      5. GC之7大垃圾收集器概述

      垃圾收集器就来具体实现这些GC算法并实现内存回收。
      不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
      图片.png

      新生代

  • 串行GC(Serial)/(Serial Copying)

  • 并行GC(ParNew)
  • 并行回收GC(Parallel)/(Parallel Scavenge)

[

](https://blog.csdn.net/u011863024/article/details/114684428)

6. GC之约定参数说明

  • DefNew:Default New Generation
  • Tenured:Old
  • ParNew:Parallel New Generation
  • PSYoungGen:Parallel Scavenge
  • ParOldGen:Parallel Old Generation

Server/Client模式分别是什么意思?

  • 使用范围:一般使用Server模式,Client模式基本不会使用
  • 操作系统
    • 32位的Window操作系统,不论硬件如何都默认使用Client的JVM模式
    • 32位的其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
    • 64位只有Server模式
      1. C:\Users\abc>java -version
      2. java version "1.8.0_251"
      3. Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
      4. Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

      7. GC之Serial收集器

      serial
      英 [ˈsɪəriəl] 美 [ˈsɪriəl]
      n. 电视连续剧;广播连续剧;杂志连载小说
      adj. 顺序排列的;排成系列的;连续的;多次的;以连续剧形式播出的;连载的

一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。
图片.png
STW: Stop The World
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World”状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
对应JVM参数是:-XX:+UseSerialGC
开启后会使用:Serial(Young区用) + Serial Old(Old区用)的收集器组合
表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

  1. public class GCDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. Random rand = new Random(System.nanoTime());
  4. try {
  5. String str = "Hello, World";
  6. while(true) {
  7. str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
  8. }
  9. }catch (Throwable e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. }

VM参数:(启用UseSerialGC)

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
  2. [GC (Allocation Failure) [DefNew: 2346K->320K(3072K), 0.0012956 secs] 2346K->1030K(9920K), 0.0013536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  3. [GC (Allocation Failure) [DefNew: 2888K->0K(3072K), 0.0013692 secs] 3598K->2539K(9920K), 0.0014059 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [DefNew: 2065K->0K(3072K), 0.0011613 secs] 4604K->4550K(9920K), 0.0011946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [GC (Allocation Failure) [DefNew: 2056K->0K(3072K), 0.0010394 secs] 6606K->6562K(9920K), 0.0010808 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [GC (Allocation Failure) [DefNew: 2011K->2011K(3072K), 0.0000124 secs][Tenured: 6562K->2537K(6848K), 0.0021691 secs] 8574K->2537K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0024399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  7. [GC (Allocation Failure) [DefNew: 2059K->2059K(3072K), 0.0000291 secs][Tenured: 6561K->6561K(6848K), 0.0012330 secs] 8620K->6561K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0012888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [Full GC (Allocation Failure) [Tenured: 6561K->6547K(6848K), 0.0017784 secs] 6561K->6547K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0018111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  9. java.lang.OutOfMemoryError: Java heap space
  10. at java.util.Arrays.copyOfRange(Arrays.java:3664)
  11. at java.lang.String.<init>(String.java:207)
  12. at java.lang.StringBuilder.toString(StringBuilder.java:407)
  13. at com.lun.jvm.GCDemo.main(GCDemo.java:23)
  14. Heap
  15. def new generation total 3072K, used 105K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  16. eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a7c8, 0x00000000ff8b0000)
  17. from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  18. to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
  19. tenured generation total 6848K, used 6547K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
  20. the space 6848K, 95% used [0x00000000ff950000, 0x00000000fffb4c30, 0x00000000fffb4e00, 0x0000000100000000)
  21. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  22. class space used 286K, capacity 386K, committed 512K, reserved 1048576K
  • DefNew:Default New Generation
  • Tenured:Old

8. GC之ParNew收集器

一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World暂停其他所有的工作线程直到它收集结束。
图片.png
ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代。
开启上述参数后,会使用:ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
但是,ParNew+Tenured这样的搭配,Java8已经不再被推荐

Java HotSpot™64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release.

备注:-XX:ParallelGCThreads限制线程数量,默认开启和CPU数目相同的线程数。
复用上一节的GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
  2. [GC (Allocation Failure) [ParNew: 2702K->320K(3072K), 0.0007029 secs] 2702K->1272K(9920K), 0.0007396 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
  3. [GC (Allocation Failure) [ParNew: 2292K->37K(3072K), 0.0010829 secs] 3244K->2774K(9920K), 0.0011000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [ParNew: 2005K->9K(3072K), 0.0008401 secs] 4742K->5624K(9920K), 0.0008605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [GC (Allocation Failure) [ParNew: 1974K->1974K(3072K), 0.0000136 secs][Tenured: 5615K->3404K(6848K), 0.0021646 secs] 7589K->3404K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0022520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [GC (Allocation Failure) [ParNew: 1918K->2K(3072K), 0.0008094 secs] 5322K->5324K(9920K), 0.0008273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  7. [GC (Allocation Failure) [ParNew: 1970K->1970K(3072K), 0.0000282 secs][Tenured: 5322K->4363K(6848K), 0.0018652 secs] 7292K->4363K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0019205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [Full GC (Allocation Failure) [Tenured: 4363K->4348K(6848K), 0.0023131 secs] 4363K->4348K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0023358 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  9. java.lang.OutOfMemoryError: Java heap space
  10. at java.util.Arrays.copyOf(Arrays.java:3332)
  11. at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  12. at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
  13. at java.lang.StringBuilder.append(StringBuilder.java:136)
  14. at com.lun.jvm.GCDemo.main(GCDemo.java:22)
  15. Heap
  16. par new generation total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  17. eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a938, 0x00000000ff8b0000)
  18. from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  19. to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
  20. tenured generation total 6848K, used 4348K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
  21. the space 6848K, 63% used [0x00000000ff950000, 0x00000000ffd8f3a0, 0x00000000ffd8f400, 0x0000000100000000)
  22. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  23. class space used 286K, capacity 386K, committed 512K, reserved 1048576K
  24. Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

9. GC之Parallel收集器

Parallel / Parallel Scavenge
图片.png
Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化。

它重点关注的是:

可控制的吞吐量(Thoughput=运行用户代码时间(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99% )。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。
常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法。
多说一句:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程

  • cpu>8 N= 5/8
  • cpu<8 N=实际个数

复用上一节GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
  2. [GC (Allocation Failure) [PSYoungGen: 2009K->503K(2560K)] 2009K->803K(9728K), 0.7943182 secs] [Times: user=0.00 sys=0.00, real=0.79 secs]
  3. [GC (Allocation Failure) [PSYoungGen: 2272K->432K(2560K)] 2572K->2214K(9728K), 0.0020218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [PSYoungGen: 2448K->352K(2560K)] 4230K->3122K(9728K), 0.0017173 secs] [Times: user=0.11 sys=0.02, real=0.00 secs]
  5. [Full GC (Ergonomics) [PSYoungGen: 1380K->0K(2560K)] [ParOldGen: 6722K->2502K(7168K)] 8102K->2502K(9728K), [Metaspace: 2657K->2657K(1056768K)], 0.0039763 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2560K)] [ParOldGen: 6454K->6454K(7168K)] 8471K->6454K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0049598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  7. [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6454K->6454K(9728K), 0.0008614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6454K->6440K(7168K)] 6454K->6440K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0055542 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  9. java.lang.OutOfMemoryError: Java heap space
  10. at java.util.Arrays.copyOfRange(Arrays.java:3664)
  11. at java.lang.String.<init>(String.java:207)
  12. at java.lang.StringBuilder.toString(StringBuilder.java:407)
  13. at com.lun.jvm.GCDemo.main(GCDemo.java:22)
  14. Heap
  15. PSYoungGen total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  16. eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14810,0x00000000fff00000)
  17. from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  18. to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  19. ParOldGen total 7168K, used 6440K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  20. object space 7168K, 89% used [0x00000000ff600000,0x00000000ffc4a1c8,0x00000000ffd00000)
  21. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  22. class space used 286K, capacity 386K, committed 512K, reserved 1048576K

10. GC之ParallelOld收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6之前(Parallel Scavenge + Serial Old )

Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。在JDK1.8及后〈Parallel Scavenge + Parallel Old )

JVM常用参数:-XX:+UseParallelOldGC使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代Parallel Old。
复用上一节GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelOldGC
  2. [GC (Allocation Failure) [PSYoungGen: 1979K->480K(2560K)] 1979K->848K(9728K), 0.0007724 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  3. [GC (Allocation Failure) [PSYoungGen: 2205K->480K(2560K)] 2574K->2317K(9728K), 0.0008700 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [PSYoungGen: 2446K->496K(2560K)] 4284K->3312K(9728K), 0.0010374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [Full GC (Ergonomics) [PSYoungGen: 1499K->0K(2560K)] [ParOldGen: 6669K->2451K(7168K)] 8168K->2451K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0043327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [Full GC (Ergonomics) [PSYoungGen: 1966K->0K(2560K)] [ParOldGen: 6304K->6304K(7168K)] 8270K->6304K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0021269 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  7. [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6304K->6304K(9728K), 0.0004841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6304K->6290K(7168K)] 6304K->6290K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0058149 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
  9. java.lang.OutOfMemoryError: Java heap space
  10. at java.util.Arrays.copyOfRange(Arrays.java:3664)
  11. at java.lang.String.<init>(String.java:207)
  12. at java.lang.StringBuilder.toString(StringBuilder.java:407)
  13. at com.lun.jvm.GCDemo.main(GCDemo.java:22)
  14. Heap
  15. PSYoungGen total 2560K, used 81K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  16. eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd14768,0x00000000fff00000)
  17. from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  18. to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  19. ParOldGen total 7168K, used 6290K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  20. object space 7168K, 87% used [0x00000000ff600000,0x00000000ffc24b70,0x00000000ffd00000)
  21. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  22. class space used 286K, capacity 386K, committed 512K, reserved 1048576K

11. GC之CMS收集器

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

CMS非常适合地内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
图片.png
Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC开启该参数后会自动将-XX:+UseParNewGC打开。
开启该参数后,使用ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。

4步过程:

  • 初始标记(CMS initial mark) - 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  • 并发标记(CMS concurrent mark)和用户线程一起 - 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
  • 重新标记(CMS remark)- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
  • 并发清除(CMS concurrent sweep) - 清除GCRoots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

图片.png
优点:并发收集低停顿。

缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片。

由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。

标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认O,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
复用上一节GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=3497984 -XX:MaxTenuringThreshold=6 -XX:NewSize=3497984 -XX:OldPLABSize=16 -XX:OldSize=6987776 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
  2. [GC (Allocation Failure) [ParNew: 2274K->319K(3072K), 0.0016975 secs] 2274K->1043K(9920K), 0.0017458 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
  3. [GC (Allocation Failure) [ParNew: 2844K->8K(3072K), 0.0010921 secs] 3568K->2287K(9920K), 0.0011138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. [GC (Allocation Failure) [ParNew: 2040K->2K(3072K), 0.0037625 secs] 4318K->4257K(9920K), 0.0037843 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  5. [GC (CMS Initial Mark) [1 CMS-initial-mark: 4255K(6848K)] 6235K(9920K), 0.0003380 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. [CMS-concurrent-mark-start]
  7. [GC (Allocation Failure) [ParNew: 2024K->2K(3072K), 0.0013295 secs] 6279K->6235K(9920K), 0.0013596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. [GC (Allocation Failure) [ParNew: 1979K->1979K(3072K), 0.0000116 secs][CMS[CMS-concurrent-mark: 0.001/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  9. (concurrent mode failure): 6233K->2508K(6848K), 0.0031737 secs] 8212K->2508K(9920K), [Metaspace: 2657K->2657K(1056768K)], 0.0032232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  10. [GC (Allocation Failure) [ParNew: 2025K->2025K(3072K), 0.0000154 secs][CMS: 6462K->6461K(6848K), 0.0020534 secs] 8488K->6461K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0021033 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
  11. [Full GC (Allocation Failure) [CMS: 6461K->6448K(6848K), 0.0020383 secs] 6461K->6448K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0020757 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  12. [GC (CMS Initial Mark) [1 CMS-initial-mark: 6448K(6848K)] 6448K(9920K), 0.0001419 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  13. [CMS-concurrent-mark-start]
  14. [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  15. [CMS-concurrent-preclean-start]
  16. [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  17. [GC (CMS Final Remark) [YG occupancy: 50 K (3072 K)][Rescan (parallel) , 0.0002648 secs][weak refs processing, 0.0000173 secs][class unloading, 0.0002671 secs][scrub symbol table, 0.0004290 secs][scrub string table, 0.0001593 secs][1 CMS-remark: 6448K(6848K)] 6499K(9920K), 0.0012107 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  18. [CMS-concurrent-sweep-start]
  19. java.lang.OutOfMemoryError: Java heap space
  20. [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  21. [CMS-concurrent-reset-start]
  22. [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  23. at java.util.Arrays.copyOfRange(Arrays.java:3664)
  24. at java.lang.String.<init>(String.java:207)
  25. at java.lang.StringBuilder.toString(StringBuilder.java:407)
  26. at com.lun.jvm.GCDemo.main(GCDemo.java:22)
  27. Heap
  28. par new generation total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  29. eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a820, 0x00000000ff8b0000)
  30. from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  31. to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
  32. concurrent mark-sweep generation total 6848K, used 6447K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
  33. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  34. class space used 286K, capacity 386K, committed 512K, reserved 1048576K

12. GC之SerialOld收集器

Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client默认的java虚拟机默认的年老代垃圾收集器。

在Server模式下,主要有两个用途(了解,版本已经到8及以后):

  1. 在JDK1.5之前版本中与新生代的Parallel Scavenge 收集器搭配使用。(Parallel Scavenge + Serial Old )
  2. 作为老年代版中使用CMS收集器的后备垃圾收集方案。

复用上一节GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC

输出结果:

  1. Unrecognized VM option 'UseSerialOldGC'
  2. Did you mean '(+/-)UseSerialGC'?

在Java8中,-XX:+UseSerialOldGC不起作用。

13. GC之如何选择垃圾收集器

组合的选择

  • 单CPU或者小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大的吞吐量,如后台计算型应用
    • -XX:+UseParallelGC(这两个相互激活)
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停顿时间,需要快速响应如互联网应用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC | 参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 | | —- | —- | —- | —- | —- | | -XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 | | -XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 | | -XX:+UseParallelGC | Parallel [Scavenge] | 复制 | Parallel Old | 标记整理 | | -XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 | 标记清除 | | -XX:+UseG1GC | G1整体上采用标记整理算法 | 局部复制 |
      |
      |

14. GC之G1收集器

复用上一节GCDemo
VM参数:

  1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC

输出结果:

  1. -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
  2. [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0015787 secs]
  3. [Parallel Time: 0.8 ms, GC Workers: 8]
  4. [GC Worker Start (ms): Min: 106.4, Avg: 106.5, Max: 106.5, Diff: 0.1]
  5. [Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 2.2]
  6. [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  7. [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
  8. [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  9. [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  10. [Object Copy (ms): Min: 0.0, Avg: 0.3, Max: 0.3, Diff: 0.3, Sum: 2.1]
  11. [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
  12. [Termination Attempts: Min: 1, Avg: 5.3, Max: 10, Diff: 9, Sum: 42]
  13. [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.3]
  14. [GC Worker Total (ms): Min: 0.6, Avg: 0.6, Max: 0.7, Diff: 0.1, Sum: 4.9]
  15. [GC Worker End (ms): Min: 107.1, Avg: 107.1, Max: 107.1, Diff: 0.0]
  16. [Code Root Fixup: 0.0 ms]
  17. [Code Root Purge: 0.0 ms]
  18. [Clear CT: 0.3 ms]
  19. [Other: 0.5 ms]
  20. [Choose CSet: 0.0 ms]
  21. [Ref Proc: 0.2 ms]
  22. [Ref Enq: 0.0 ms]
  23. [Redirty Cards: 0.3 ms]
  24. [Humongous Register: 0.0 ms]
  25. [Humongous Reclaim: 0.0 ms]
  26. [Free CSet: 0.0 ms]
  27. [Eden: 4096.0K(4096.0K)->0.0B(4096.0K) Survivors: 0.0B->1024.0K Heap: 7073.4K(10.0M)->2724.8K(10.0M)]
  28. [Times: user=0.02 sys=0.02, real=0.00 secs]
  29. [GC concurrent-root-region-scan-start]
  30. [GC concurrent-root-region-scan-end, 0.0004957 secs]
  31. [GC concurrent-mark-start]
  32. [GC concurrent-mark-end, 0.0001071 secs]
  33. [GC remark [Finalize Marking, 0.0001876 secs] [GC ref-proc, 0.0002450 secs] [Unloading, 0.0003675 secs], 0.0011690 secs]
  34. [Times: user=0.00 sys=0.00, real=0.00 secs]
  35. [GC cleanup 4725K->4725K(10M), 0.0004907 secs]
  36. [Times: user=0.00 sys=0.00, real=0.00 secs]
  37. [GC pause (G1 Humongous Allocation) (young), 0.0009748 secs]
  38. [Parallel Time: 0.6 ms, GC Workers: 8]
  39. [GC Worker Start (ms): Min: 111.8, Avg: 111.9, Max: 112.2, Diff: 0.5]
  40. [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
  41. [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  42. [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
  43. [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  44. [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  45. [Object Copy (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.7]
  46. [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
  47. [Termination Attempts: Min: 1, Avg: 3.3, Max: 5, Diff: 4, Sum: 26]
  48. [GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.8]
  49. [GC Worker Total (ms): Min: 0.1, Avg: 0.5, Max: 0.6, Diff: 0.5, Sum: 3.6]
  50. [GC Worker End (ms): Min: 112.3, Avg: 112.3, Max: 112.4, Diff: 0.0]
  51. [Code Root Fixup: 0.0 ms]
  52. [Code Root Purge: 0.0 ms]
  53. [Clear CT: 0.1 ms]
  54. [Other: 0.2 ms]
  55. [Choose CSet: 0.0 ms]
  56. [Ref Proc: 0.1 ms]
  57. [Ref Enq: 0.0 ms]
  58. [Redirty Cards: 0.1 ms]
  59. [Humongous Register: 0.0 ms]
  60. [Humongous Reclaim: 0.0 ms]
  61. [Free CSet: 0.0 ms]
  62. [Eden: 1024.0K(4096.0K)->0.0B(4096.0K) Survivors: 1024.0K->1024.0K Heap: 6808.1K(10.0M)->2595.2K(10.0M)]
  63. [Times: user=0.00 sys=0.00, real=0.00 secs]
  64. [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0006211 secs]
  65. [Parallel Time: 0.2 ms, GC Workers: 8]
  66. [GC Worker Start (ms): Min: 113.3, Avg: 113.3, Max: 113.4, Diff: 0.1]
  67. [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.0]
  68. [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  69. [Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
  70. [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  71. [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  72. [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  73. [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
  74. [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
  75. [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  76. [GC Worker Total (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.1, Sum: 1.4]
  77. [GC Worker End (ms): Min: 113.5, Avg: 113.5, Max: 113.5, Diff: 0.0]
  78. [Code Root Fixup: 0.0 ms]
  79. [Code Root Purge: 0.0 ms]
  80. [Clear CT: 0.1 ms]
  81. [Other: 0.3 ms]
  82. [Choose CSet: 0.0 ms]
  83. [Ref Proc: 0.1 ms]
  84. [Ref Enq: 0.0 ms]
  85. [Redirty Cards: 0.1 ms]
  86. [Humongous Register: 0.0 ms]
  87. [Humongous Reclaim: 0.0 ms]
  88. [Free CSet: 0.0 ms]
  89. [Eden: 0.0B(4096.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4595.9K(10.0M)->4557.3K(10.0M)]
  90. [Times: user=0.00 sys=0.00, real=0.00 secs]
  91. [GC concurrent-root-region-scan-start]
  92. [GC pause (G1 Humongous Allocation) (young)[GC concurrent-root-region-scan-end, 0.0001112 secs]
  93. [GC concurrent-mark-start]
  94. , 0.0006422 secs]
  95. [Root Region Scan Waiting: 0.0 ms]
  96. [Parallel Time: 0.2 ms, GC Workers: 8]
  97. [GC Worker Start (ms): Min: 114.2, Avg: 114.3, Max: 114.4, Diff: 0.2]
  98. [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
  99. [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  100. [Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
  101. [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  102. [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  103. [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  104. [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
  105. [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
  106. [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  107. [GC Worker Total (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.9]
  108. [GC Worker End (ms): Min: 114.4, Avg: 114.4, Max: 114.4, Diff: 0.0]
  109. [Code Root Fixup: 0.0 ms]
  110. [Code Root Purge: 0.0 ms]
  111. [Clear CT: 0.1 ms]
  112. [Other: 0.3 ms]
  113. [Choose CSet: 0.0 ms]
  114. [Ref Proc: 0.1 ms]
  115. [Ref Enq: 0.0 ms]
  116. [Redirty Cards: 0.1 ms]
  117. [Humongous Register: 0.0 ms]
  118. [Humongous Reclaim: 0.0 ms]
  119. [Free CSet: 0.0 ms]
  120. [Eden: 0.0B(2048.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4557.3K(10.0M)->4547.6K(10.0M)]
  121. [Times: user=0.00 sys=0.00, real=0.00 secs]
  122. [Full GC (Allocation Failure) 4547K->4527K(10M), 0.0023437 secs]
  123. [Eden: 0.0B(2048.0K)->0.0B(3072.0K) Survivors: 1024.0K->0.0B Heap: 4547.6K(10.0M)->4527.6K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
  124. [Times: user=0.00 sys=0.00, real=0.00 secs]
  125. [Full GC (Allocation Failure) 4527K->4513K(10M), 0.0021281 secs]
  126. [Eden: 0.0B(3072.0K)->0.0B(3072.0K) Survivors: 0.0B->0.0B Heap: 4527.6K(10.0M)->4514.0K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
  127. [Times: user=0.00 sys=0.00, real=0.00 secs]
  128. [GC concurrent-mark-abort]
  129. java.lang.OutOfMemoryError: Java heap space
  130. at java.util.Arrays.copyOf(Arrays.java:3332)
  131. at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  132. at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
  133. at java.lang.StringBuilder.append(StringBuilder.java:136)
  134. at com.lun.jvm.GCDemo.main(GCDemo.java:22)
  135. Heap
  136. garbage-first heap total 10240K, used 4513K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
  137. region size 1024K, 1 young (1024K), 0 survivors (0K)
  138. Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  139. class space used 286K, capacity 386K, committed 512K, reserved 1048576K

以前收集器特点

  • 年轻代和老年代是各自独立且连续的内存块;
  • 年轻代收集使用单eden+s0+s1进行复机算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而快速地执行GC为设计原则。

    G1是什么

    G1 (Garbage-First)收集器,是一款面向服务端应用的收集器:
    The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection(GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage
    collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collectoris designed for applications that:

  • Can operate concurrently with applications threads like the CMS collector.

  • Compact free space without lengthy GC induced pause times.
  • Need more predictable GC pause durations.
  • Do not want to sacrifice a lot of throughput performance.
  • Do not require a much larger Java heap.

从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:

像CMS收集器一样,能与应用程序线程并发执行。
整理空闲空间更快。
需要更多的时间来预测GC停顿时间。
不希望牺牲大量的吞吐性能。
不需要更大的Java Heap。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。
G1是在2012年才在jdk1.7u4中可用。oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器。
主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region ,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

特点

  1. G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
  2. G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
  3. 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
  4. G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
  5. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。

[

](https://blog.csdn.net/u011863024/article/details/114684428)

15. GC之G1底层原理

Region区域化垃圾收集器 - 最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB∗2048=65536MB=64G内存
图片.png

humongous
英 [hjuːˈmʌŋɡəs] 美 [hjuːˈmʌŋɡəs]
adj. 巨大的;庞大的

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。
如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

回收步骤

G1收集器下的Young GC
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区。
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

图片.png
4步过程:

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行GC Roots Tracing的过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  4. 筛选回收:根据时间来进行价值最大化的回收

图片.png

16. GC之G1参数配置及和CMS的比较

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45。
  • -XX:ConcGCThreads=n:并发GC使用的线程数。
  • -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。


开发人员仅仅需要声明以下参数即可:
三步归纳:开始G1+设置最大内存+设置最大停顿时间

  1. -XX:+UseG1GC
  2. -Xmx32g
  3. -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
G1和CMS比较

  • G1不会产生内碎片
  • 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

[

](https://blog.csdn.net/u011863024/article/details/114684428)

17. JVMGC结合SpringBoot微服务优化简介

  1. IDEA开发微服务工程。
  2. Maven进行clean package。
  3. 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数。
  4. 公式:java -server jvm的各种参数 -jar 第1步上面的jar/war包名。

参考

https://blog.csdn.net/u011863024/article/details/114684428 Reference 详解 https://www.jianshu.com/p/f86d3a43eec5

[

](https://blog.csdn.net/u011863024/article/details/114684428)