大厂面试题
2021-01-18 13:59:25 4 举报
AI智能生成
大厂面试题个人总结
作者其他创作
大纲/内容
JUC 多线程及高并发
谈谈你对volatile 理解
1),请你谈谈你对volatile的理解
是Java虚拟机提供的一个轻量级的同步机制
保证可见性
不保证原子性
禁止指令重排
2), JMM(Java 内存模型)你谈谈
简介
JMM(Java Memory Model) 本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定
1, 线程解锁前,必须把共享变量的值刷新回主内存
2, 线程加锁前,必须读取主内存的最新值到自己的工作内存
3,加锁解锁是同一把锁
JMM
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内从拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须在主内存来完成,
可见性
代码
通过JMM简介,我们知道各个线程对主内存共享变量的操作都是各个线程各自拷贝到自己工作内存区域,进行操作然后再写入主内存中
这就可能存在一个线程AAA修改共享变量X的值,但是未写入主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时AAA线程共享变量X对线程B来说是不可见,这种工作内存与主内存同步延迟的线程造成了可见性问题
原子性
代码
被拆分为
如何解决原子性问题?
使用 JUC下面的 atomicInteger类
代码
有序性
简介
计算机在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排,一般分为一下3种
源代码 ->编译器优化的重排->指令并行重排 -> 内存系统的重排 -> 最终执行的指令
1), 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
2), 处理器在进行重排时必须要考虑指令之间的数据依赖性
3), 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
重排Case1
问题 语句4可以重排后变为第一条?
答案是不可以以,因为y 没有被初始化
重排 Case2
重排 Case3
禁止指令重排的总结
volatile 实现了禁止指令重排的优化,从而避免了多线程环境下出现乱序执行的现象
先来了解一个概念, 内存屏障(Memory Barrier) 又称内存栅栏,是一个cpu指令,作为有2个
1),保证特定操作的执行顺序
2),保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier 则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障来禁止在内存屏障前后的指令执行重排序的优化,内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何cpu上的线程都能读取到这些数据的最新版本
线程安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized 或者 volatile 关键字来解决,它们都可以使一个线程修改后的变量立即对其它线程可见
对指令重排导致可见性问题和有序性问题,可以利用 volatile 关键字来解决,因为volatile 的另一个作用就是禁止指令重排
3),你在那些地方用过 volatile ?
单例模式 DCL代码
单例模拟 volatile 分析
DCL(双端检索)机制不一定线程安全,原因是有指令重排序的存在,加入 volatile 可以禁止指令重排序
原因在于某一线程执行第一次监测,读取到的 instance 不为null时,instance 的引用对象可能没有完成初始化
instance =new SingletonDemo() 分为以下3步完成
1),分配对象内存空间
memory = allocate()
2),创建实例对象
instance(memory)
3), 设置instance 指向 内存地址,此时instance不为空
instance = memory
步骤2和步骤3不存在依赖关系,而且无论重排前和重排后程序的执行结果在单线程状态下并没有改变,因此这种重排优化是允许的
多线程环境下就有可能出现问题
1),分配对象内存空间
memory = allocate()
3),设置instance指向刚分配的内存空间,此时instance 不为空,但是对象没有完成初始化
instance = memory
2), 对象完成初始化
instance(memory)
但是指令重排只会保证串行语义执行的一致性(单线程),但并不会关心多线程之间语义一致性
所以当一条线程访问 instance 不为null时,由于 instance 实例未必已初始化完成,也就造成了线程安全性问题
CAS你知道?
比较并交换-----(真实值和期望值相同修改成功,真实值和期望值不相同,修改失败,类似于svn/github 代码提交)
CAS底层原理? 如果知道,谈谈你对 UnSafe 的理解
CAS是什么
CAS全称为 Compare -And-Set (比较并交换),它是一条CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新值,这个过程是原子的
CAS并发原语提现在Java 语音就是UnSafe 类中的各个方法,调用UnSafe 类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能那,通过它实现了原子操作,再次强调,由于CAS十一中行系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致
atomicInteger.getAndIncrement()
Unsafe
UnSafe 是CAS的核心类,由于Java 方法无法直接访问底层系统,需要通过本地native方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据,UnSafe 类存在于 sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于 UnSafe类的方法
注意,UnSafe 类中所有方法都是native修饰的,也就是说UnSafe 类中的方法都直接调用操作系统底层资源执行响应任务
代码验证
UnSafe
var1 : AtomicInteger 对象
var2 : 该对象值引用地址
var4 : 需要变动的数量
var5 : 用 var1 和var2 找出主内存中真实的数据
用该对象当前值与 val5进行比较,如果相同更新 var5+var4并返回true,如果不同则继续取值然后比较,直到更新完成
假设 A,B两个线程同时执行 getAndAddInt 方法(比分跑在不同cpu上)分为以下5步
1), 假设 AtomicInteger 的初始值为3,即主内存的值为3,这时A,B两个线程各自copy主物理内存的值到自己的工作空间
2), 线程A,通过 var5 = this.getIntVolatile(var1, var2); 拿到var5=3,此时线程A被挂起了
3),线程B通过 var5 = this.getIntVolatile(var1, var2); 也拿到了var5=3,此时更好线程B没有被挂起,执行 compareAndSwapInt 对比发现自己的值和主内存的值相同,此时线程B把主物理内存的值修改为4
4),此时A线程恢复,执行 compareAndSwapInt 发现自己的var5=3的值和 主物理内存的值4不同,说明该值已经被其它线程修改过了,那次线程A修改失败,只能重新读取一次var5
5),线程A重新执行 var5 = this.getIntVolatile(var1, var2); 拿到var=4 再执行 compareAndSwapInt 发现var5的值和主物理内存的值相同,修改主物理内存的值,
小总结
CAS(Compare And Set)
比较当前工作内存中的值和主内存的值,如果相同则执行规定操作,如果不同则继续重新获取主内存的值,然后执行相关操作,直到最后成功
CAS 应用
CAS有三个操作数
内存中值V, 旧的预期值A, 新值B
仅当内存值V和旧值A相同时,把内存的值修改为B ,否知什么也不做
CAS缺点
循环时间长,开销大(如果do while 比较失败,会一直进行尝试,如果CAS长时间不成功,可能会给CPU带有很大的开销)
只能保证一个共享变量的原子操作 (但是对于多个共享变量,循环CAS就无法保证操作的原子性,这个时候可以用锁来保证原子性)
引出来ABA问题???
原子类 AtomicInteger 的ABA 问题谈谈? 原子更新引用知道?
ABA 问题怎么产生的
CAS会导致ABA问题,CAS算法实现了一个重要的前提需要取出来内存中某时刻的数据并在当下时刻比较替换,那么再这个时间差类会导致数据的变化
比如说一个线程one从内存位置V取出A,这时候另一个线程two也从内存中取出A,并且线程two 进行了一些操作将值变成了B,然后线程two又将V位置数据变成了A,这时候线程one 进行CAS操作发现内存中仍然是A,然后线程one 操作成功
尽管线程one 操作成功,但是不代表这个过程是没有问题的
原子引用
java.util.concurrent.atomic Class AtomicReference<V>
ABA问题的解决
java.util.concurrent.atomic Class AtomicStampedReference<V>
我们知道 ArrayList 是线程不安全的,请编写出一个不安全的案例并给出解决方案
公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解,请手写一个自旋锁
公平锁和非公平锁
是什么
公平锁: 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
非公平锁: 指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先来获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象
两者区别
公平锁/非公平锁 : 并发包中 ReetranLock 的创建可以指定构造函数的 boolean 类型来得到公平锁和非公平锁,默认是非公平锁
区别如下
1),公平锁,就很公平,在并发环境中,每个线程在获取锁时会先查看此锁的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否则就会加入到等待队列中,以后展昭FIFO的规则从队列中取到自己
2),非公平锁,比较粗鲁,上来就直接尝试占有锁,如果尝试失败,在用类似公平锁那种方式
题外话
Java ReetranLock 而言,通过构造函数指定该锁是否公平锁,默认是非公平锁,非公平锁的有点在于吞吐量比公平锁大
Synchronized 也是一种非公平锁
可重入锁(有名递归锁)
是什么?
指的是同一线程外层函数获取锁之后,内层递归函数仍能获取锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁 即, 线程可以进入任何一个它已经拥有锁所同步的代码块
ReetranLock(显示锁) / Syncharonized(隐式锁) 就是一个典型的可重入锁
可重入锁最大的作用就是避免死锁
Synchronized 的重入的实现原理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针
当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有其他线程所持有,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java 虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减一,计数器为零代表锁已经释放
ReetranLock 显示锁
由于加锁次数和解锁次数不一样,第二个线程始终无法获取到锁,导致一直在等待
Synchronized / ReetranLock
Case1
Case2
自旋锁 (skinlock)
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文的切换消耗,缺点是循环会消耗CPU
Case
独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁
指该锁只能被一个线程所持有,对ReetrankLock 和synchronized 而言都是独占锁
共享锁
指该锁可能被多个线程持有,对 ReetrankReadWriteLock 其读锁是共享锁,其写锁就是独占锁,该锁的共享锁可以保证并发读是非常高效的,读写,和写读,写写的过程是互斥的
before Cache1
我们发现在 线程2写入的时候线程3和线程4进行了加塞,违背了原子性操作,数据一致性
after Cache2
结果看,数据保证了数据的原子性操作.数据一致性
CountDownLatch / CycliBarrier /Semaphore 使用过?
CountDownLatchDemo
让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
CountDownLatch 主要有2个方法,当一个或多个线程调用await 方法时,调用线程会被阻塞,其它线程调用 countDown() 方法将会计数器减一,方法的线程不会被阻塞,当计数器的值变为 0 时,因调用 await 方法被阻塞的线程会被唤醒,继续执行
使用枚举模拟秦始皇灭六国,统一华夏
枚举类为
countDownLatch类
避免了 countDownLatch类大量的if else 的使用
CyclicBarrierDemo
CyclicBarrier 的字面意思是可循环(cyclic) 使用屏障 (Barrier) ,它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续干活,线程进入屏障通过 CyclicBarrier 的await() 方法
Semaphore
信号量主要用于两个目的,一个用于多个共享资源的互斥使用,另一个用于并发线程数的控制
阻塞队列知道?
参考JUC的 blockingQueue
synchronized 和 lock 有什么区别 ,用新的 lock 有什么好处,举例说说
原始构成
1), synchronized 是关键字属于 JVM层面的
monitorenter : 底层是通过 monitor对象来完成,其实wait / notify 等方法也依赖与 monitor 对象只有在 同步块中才能掉 wait /notify 等方法
monitorexit
2),Lock 是具体类(java.util.concurrent.locks.lock) 是api底层的锁
使用方法
synchronized 不需要用户手动去释放锁,当 synchronzied 代码执行完后系统会自动让线程释放对锁的占用
reentrankLock 则需要手动去释放锁,若没有主动释放锁,则可能导致出死锁现象 ,需要 lock 和 unlock 方法配合 try catch finally 语句块来使用
等待是否可中断
synchronized 不可中断,除非抛出异常或者正常运行完成
reentranklock 可中断,
1),设置超时时间 tryLock(long timeout,TimeUnit unit)
2), lockInterruptibly() 放入代码块中,调用 interrupt()方法可中断
加锁是否公平
synchronized 是非公平锁
reentranklock 可以非公平锁(默认),可以公平锁,构造方法根据传入 true 的话就是公平锁,false就是非公平锁
锁绑定多个条件的condition
synchronized 没有
reentranklock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized 要么随机唤醒一个要么唤醒全部线程
线程池用过? ThreadPoolExecutor 谈谈你的理解
参考JUC的线程池
线程池用过? 生产上你如何设置合理的参数
参考JUC的线程池
死锁编码及定位分析
产生死锁的原因
死锁是指两个或者两个以上的进程在执行过程中,因争夺资源从而造成的一种 互相等待的现象,若无外力干涉那些它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能就很低,否则就会因争夺有限的资源而陷入死锁
1),系统资源不足
2),进程运行推进的顺序不合适
3),资源分配不当
死锁代码
如何定位分析
在 jdk安装下的/lib/目录有这样两个
jps.exe
命令定位进程
jstack.exe
找到死锁查看
找出死锁位置
LockSupport
是什么
用于创建锁和其他同步类的基本线程阻塞原语。
LockSupport 类使用了一种名为 Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit), permit只有两个值1或0
可以许可看成一种(0,1)信号量,但与 Semaphore 不同的是,许可的累加上限是1
能干嘛
主要两个方法 park() 和 unpark(Thread thread) ,分别作用是 阻塞线程和接触阻塞线程
在哪下
java.util.locks.LockSupport
线程等待唤醒机制(wait / niotify)
3种让线程等待和唤醒的方法
1), 使用Object 的wait() 让线程等待,使用 Object 的notify() 方法唤醒线程
2), 使用JUC包中Condition 的 await() 方法让线程等待,使用 signal() 让线程唤醒
3), LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object 类中的 wait 和notify 方法实现线程等待和唤醒
如果去掉 synchronized (对象)运行,会产生什么问题
恢复原来的代码 ,如果让T1线程睡3秒钟,这样让T2线程先执行,然后在执行T1线程,会产生什么问题?
Condition 接口中的 await 后 signal 方法实现线程的等待和唤醒
如果 注释掉 lock.lock(); 和 lock.unlock() 运行会出现什么问题?
代码恢复,让T1线程 睡3秒,默认让T2线程(通知)执行,会有什么问题呢?
传统的 synchronized 和 lock 实现等待唤醒通知的约束
线程必须要获得并持有锁,必须在锁块(synchronzied 或 lock)中
必须要先等待后唤醒,线程才能被唤醒
LockSupport 类中的 park 等待和 unpark唤醒
正常和无锁块要求
之前错误的先唤醒后等待,LockSupport 照样支持
重点说明
LockSupport 是用来创建锁和其它同步类的基本线程阻塞原语
LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,归根结底,LockSupport 调用的 Unsafe 中的native代码
LockSupport 提供 park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程
LockSupport 和每个使用它的线程都有一个许可 (permit) ,permit 相当于1,0的开关,默认是0,调用一次unpark 就加1变为1,调用一次park() 会消费 permit 将1变为0,同时 park 会立即返回
如果再次调用 park 会变成阻塞(因为 park 为0 会阻塞在这里,一直到permit 为1)这是调用 unpark 会把permit 设置为1,每个线程都有一个相关的 permit ,permit最多只有一个,重复调用unpark 也不会累计叠加
常见LockSupport 面试题
为什么先唤醒线程后阻塞线程?
因为unpark 获得一个凭证,之后再调用 park 方法,就可以名正言顺的凭证消费,姑不会阻塞
为什么唤醒2次后阻塞2次,但最终结果还是会阻塞线程?
因为凭证的数量最多为1,连续调用2次 unpark 和调用 一次 unpark 效果一样,只会增加一个凭证,而调用2次park却需要2个凭证,证书不够,所以阻塞
AbstractQueueSynchronized 之AQS
前置知识
公平锁和非公平锁
可重入锁
LockSupport
自旋锁
数据结构之链表
设计模式之模板设计模式
是什么
字面意思: 抽象的队列同步器
CLH ( Craig Landin and Hagersten ) 队列是一个单向链表,AQS 中的队列是CLH 变体的虚拟双向队列FIFO
技术解释: 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO 队列来完成资源获取线程的排队工作,并通过一个int 类型变量表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
与AQS相关的
ReetranLock
CountDownLatch
ReetranReadWriteLock
Semaphore
...
进一步理解锁和同步器的关系
锁: 面向锁的使用者
同步器: 面向锁的实现者
能干嘛
加锁会导致阻塞
解释说明
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入队列中,这个队列就是AQS的抽象表现,它将请求共享资源的线程封装成队列的特点(Node) 通过CAS,自旋以及 LockSupport.park()的方式,维护state 变量的状态,使并发达到同步的控制效果
AQS初识
AbstractQueuedSynchronizer
有阻塞就需要排队,实现排队必然需要队列
AQS 使用了一个 volatile 的int类型的成员变量来表示同步状态,通过内置的FIFO 队列是完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node 节点来实现锁的分配,通过CAS完成对state 值的修改
AQS内部体系架构
AQS自身
AQS 的int变量
AQS 的同步状态state 成员变量
类似银行办理业务窗口状态
零就是没人,自由状态可以办理
大于等于1,有人占用窗口,等着去
AQS 的CLH 队列
CLH队列(三个大牛的名字组成) 为一个双向队列
类似银行候客区的等待顾客
小总结
有阻塞就需要排队,实现排队必然需要队列
state + CLH变种的双向队列
内部类Node(Node 类在AQS的内部)
Node 的int变量
Node的等待状态 waitState 成员变量
说人话就是: 等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node
Node 类的讲解
共享 static final Node SHARED = new Node();
独占 static final Node EXCLUSIVE = null;
线程被取消了 static final int CANCELLED = 1;
后续线程需要被唤醒 static final int SIGNAL = -1;
等待condition唤醒 static final int CONDITION = -2;
共享式同步状态获取将会无条件地传播下去 static final int PROPAGATE = -3;
初始为0,状态时上面几种 volatile int waitStatus;
前置节点 volatile Node prev;
后置节点 volatile Node next;
节点排队的线程 volatile Thread thread;
AQS同步队列的基本结构
从我们的ReetranLock 开始源码解读AQS
Lock 接口的实现类,基本上都是通过 [聚合] 了一个[队列同步器]的子类完成线程访问控制的
Reetrantlock 的原理
从最简单的lock 方法开始看 公平锁和非公平锁
从源码中可以看出公平锁和非公平锁的lock() 方法唯一的区别就是 在于公平锁在获取同步状态时多了一个限制条件, hasQueuedPredecessors()
非公平锁走起,方法 lock()
对比公平锁和非公平锁的 tryAcquire() 方法 的实现代码,其实差别就在于 非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()
hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下
公平锁: 公平讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有了线程在等待,那么当前线程就会进入等待队列中
非公平锁: 不管是否有等待线程,如果可以获取锁,则like占有锁对象,也就是说队列的第一个排队线程在 unpark() ,之后还是需要竞争锁(存在线程竞争的情况)
AQS 源码深度分析走起
lock()
acquire(1)
tryAcquire(arg)
addWaiter(Node.EXCLUSIVE)
模拟A线程获取锁,这时B线程就要进入LCH队列
调用addWaiter
enq入队操作
模拟A线程获取锁,这时B线程已经进入了LCH队列,这时C也要进入队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
predecessor()
shouldParkAfterFailedAcquire(p, node)
parkAndCheckInterrupt()
AQS方法 unlock()
JVM+GC 解析
JVM 垃圾回收的时候如何确定垃圾? 是否知道什么是GC Roots
什么是垃圾
简单的说就是内存中已经不再被使用到空间就是垃圾
要进行垃圾回收,如何判断一个对象是否可以被回收?
引用计数法
Java中,引用和对象是有关联的,如果要操作对象则必须用引用进行
因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以进行回收,简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器加1,每当有过一个引用失效时,计数器减一
任何时刻计数器值为0的对象就是不可能被使用的,那么这个对象就是可回收的对象
那为什么主流的Java 虚拟机里面没有选用这种算法,其中最主要的原因就是它很难解决对象之间相互循环引用的问题
枚举根节点做可达性分析(根搜索路径)
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法
所谓 GC roots 或者说 tracing GC 的根集合, 就是一组必须活跃的引用
基本思路就是通过一系列名为 GC Roots 的对象作为起始点,从这个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用,也即给定一个集合的引用作为跟出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为活,没有被遍历到的自然被判定为死亡
GC Roots 案例
Java 中可以作为GC Roots 的对象
虚拟机栈 (栈帧中局部变量区,也叫作局部变量表) 中引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(native 方法)引用的对象
你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值
JVM 的参数类型
标配参数
- version
-help
java -showversion
X参数(了解)
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式
XX参数
Boolean 类型
-XX:+或者-某个属性值
+表示开启
-表示关闭
是否打印GC收集细节
idea 设置
测试结果
是否使用串行垃圾回收器
和上面同理 默认是
-XX:-UseSerialGC
idea修改后再次访问
-XX:+UseSerialGC
KV 设值类型
-XX: 属性key=属性值value
案例
-XX:MetaspaceSize=12582912
MaxTenuringThreshold=15
jinfo 举例,如何查看当前系统运行程序的配置
jinfo -flag 配置项 进程编号
参考 GC 收集细节
jinfo -flags 12416
题外话
两个经典参数: -Xms 和 -Xmx
你如何解释
-Xms 等待于 -XX:initialHeapSize
-Xmx 等价于 -XX:MaxHeapSize
盘点家底---查看JVM默认值
-XX:+PrintFlagsInitial
主持要查看初始默认
java -XX:+PrintFlagsInitial
-XX:PrintFlagsFinal
主要查看修改更新
java -XX:+PrintFlagsFinal -version
其中结果出现的=和:=是什么意思
=是没有修改过的,默认加载的
:= 人为改过或JVM加载不一样修改过加载值
以 MetaspaceSize 为例,运行java 命令同时打印出参数
MetaspaceSize = xxx
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m
MetaspaceSize
:= 536870912
java -XX:+printCommandLineFlags
打印命令行参数
java -XX:+PrintCommandLineFlags -version
你平时工作用过的JVM 常用基本配置参数有哪些
-Xms
初始大小内存,默认为物理内存的1/64 等价于 -XX:InitialHeapSize
-Xmx
最大内存大小,默认为物理内存的1/4,等价于 -XX:MaxHeapSize
-Xss
设置单个线程栈的大小,一般默认为 512K-1024K, 等价于 -XX:ThreadStackSize
-Xmn
设置年轻代大小
-XX:MetaspaceSize
设置元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间和永久代之间最大区别在于 元空间并不在虚拟机中,而是使用本地内存 , 因此,默认情况下,元空间大小仅受本地内存限制
-XX:MetaspaceSize = 1024m
典型设置案例
-XX:PriontGCDetails
输出详细GC收集日志信息
GC
GC [DefNew(新生代): 1641K->320K(3072K), 0.0016319 secs][Tenured(老年代): 250K->569K(6848K), 0.0038326 secs] 1641K->569K(9920K), [Metaspace: 2064K->2064K(4480K)], 0.0067679 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
0.0016319 secs / 0.0038326 secs / 0.0067679 secs 表示GC消耗时间(秒)
1641K 和 569K 表示GC之前内存区域使用的内存容量
320K 和 556K 表示GC之后内存区域使用的内存容量
(3072K) 和 (6848K) 该内存区域总容量
user=0.01 新生代用户耗时
sys=0.00 系统耗时
real=0.01 secs 新生代实际耗时
GC / Full GC GC类型
FullGC
[Full GC[Tenured: 569K->556K(6848K), 0.0026919 secs] 569K->556K(9920K), [Metaspace: 2064K->2064K(4480K)], 0.0027213 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-XX:SurvivorRatio
设置 新生代中 eden 和s1/s0 空间比例 默认是 -XX:SurvivoRatio=8, Eden:S0:S1=8:1:1
假设 -XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
-XX:NewRatio
配置年轻代与老年代在堆结构的占比默认为 -XX:NewRatio=2 新生代占1,老年代2 ,年轻代占整个堆的1/3
假如 -XX:NewRatio=4 新生代占1,老年代4,年轻代占整个堆的1/5 ,NewRatio 值就是设置老年代的占比,剩下的1给新生代
-XX:MaxTenuringThreshold
设置垃圾最大年龄
JDK1.8 开始默认就是15,不能进行修改
强引用, 软引用 , 弱引用 , 虚引用 分别是什么
整体架构
强引用(默认支持模式) ---Reference
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算出现了OOM也不会对该对象进行回收,死都不收
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象,在Java中最常见的就是强引用,把一个对象赋给另一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于不可达状态,它是不能被回收的,即使该对象以后永远都不会被用到JVM也不会被回收,因此强引用时造成Java 内存泄漏的主要原因之一
对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集了(当然了具体收集时机还要看垃圾收集策略)
软引用 --- SoftReference
软引用时一种相对强引用弱化了一些的引用,需要用到 java.lang.SoftReference 类来实现,可以让对象豁免一些垃圾收集
对于只有软引用的对象来说 当系统不足的时候它会被回收,当系统内存充足时,它不会被回收
软引用通常用在对内存敏感的程序中,比如告诉缓存就有用到软引用,内存够用的时候就保留,不够用就回收(mybatis 缓存就有用到软引用)
弱引用 --- WeakReference
弱引用需要用 java.lang.WeakReference 类来实现,它比软引用的生命周期更短
对于只有弱引用的对象来说,只要垃圾回收器一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存
软引用 和弱引用的适用场景
假设一个应用需要读取大量的本地图片
如果每次读取图片都是从硬盘读取则会造成严重影响性能
如果一次性全部加载到内存中有可能造成内存溢出
此时使用软引用可以解决这个问题
用一个HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效避免了OOM的问题
Map<String,SoftReference<BitMap>> imagesCache=new HashMap<>()
你知道弱引用的话,能谈谈 WeakHashMap ???
HashMap 使用的是强引用就算SystemGC也不会被回收
WeakHashMap 使用的是弱引用System.gc 会被回收
虚引用 --- PhantomReference
虚拟用需要java.lang.PhantomReference
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的声明周期
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可以能被垃圾回收器回收,它不能单独地使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅提供了一种确保对象被finalize 以后,做某些事情的机制, PhantomReference 的get方法总是返回null, 因此无法访问对应的引用对象,其意义在于说明一个对象已经进入了 finalization 阶段,可以被gc回收,用来实现比 finalization 机制更灵活的回收操作
换句话说, 设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,Java技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作
java.lang.ref.ReferenceQueue 被回收之前需要被引用队列保存下
GC Roots 四大引用小总结
请谈谈 你对OOM认识
java.lang.StackOverflowError
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC 回收时间过长时会抛出 OutOfMemoryError 过长的定义是,超过98%的时间来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出,加入不抛出 GC ioverhead limit 错误会发生什么情况呢?
那就是GC 清理的这么点内存很快再次填满,迫使GC再次执行,这样就造成了恶性循环,CPU使用率一直是100% ,而GC却没有任何成果
java.lang.OutOfMemoryError: Direct buffer memory
写NIO 程序经常使用 byteBuffer 来读取或写入数据,这是一种基于通道(Channel) 与缓冲区 (Buffer) 的I/O方式 ,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的 DirectByBuffer 对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native 堆中来回复制数据
ByteBuffer.allocate(capacity) 第一种方式分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocateDirect(capacity) 第二种方式分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快
但如果不断分配本地内存,对内存很少使用,那么JVM就不需要执行GC ,DirectByteBuffer 对象们就不会被回收,这时候堆内存充足,但本地内存可能已经用完了,再次尝试分配本地内存就会出现OutOfMemoryError ,那程序就直接崩溃了
java.lang.OutOfMemoryError: unabale to create new native thread
高并发请求服务器时,经常出现如下异常, Java.langOutOfMemoryError: unable to create new native thread 准确的讲该 native thread 异常与对应的平台无关
导致原因
1), 你的应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
2), 你的服务器并不允许你的应用创建这么多线程,linux 系统默认允许单个进程可以创建的线程数是1024 ,你的应用创建超过这个数量,就会报 OutOfMemoryError:unable to create new native thread
1), 你的应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
2), 你的服务器并不允许你的应用创建这么多线程,linux 系统默认允许单个进程可以创建的线程数是1024 ,你的应用创建超过这个数量,就会报 OutOfMemoryError:unable to create new native thread
解决办法
1)想办法降低你引用程序创建线程的数量,分析引用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
2 ), 对于有的应用,确实需要创建很多线程,远超过 linux 系统的默认1024个线程的限制,可以通过修改linux 服务器配置扩大默认限制
1)想办法降低你引用程序创建线程的数量,分析引用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
2 ), 对于有的应用,确实需要创建很多线程,远超过 linux 系统的默认1024个线程的限制,可以通过修改linux 服务器配置扩大默认限制
Linux服务器级别调优
ulimit -u
修改 /etc/security/limits.d/xx-nproc.conf
java.lang.OutOfMemoryError: Metaspace
java -XX:+PrintFlagsInitial 查看元空间大小
Java8 以后的版本使用Metaspace 来替代元空间, Metaspace 是方法区在Hotspot 中的实现,它与持久带最大的区别在于: Metaspace 并不在虚拟机内存中而是使用本地内存,也即在java8中, class metadata, 被存储在叫做 Metaspace 的native memory
永久代存放一下信息
虚拟机加载的类信息
常量池
静态变量
即使编译后的代码
△GC垃圾回收算法和垃圾收集器的关系,分别是什么请你谈谈
GC (引用计数/复制/标清/标整) 是内存回收的方论, 垃圾收集器就是算法落地实现
目前为止,还没有完美的收集器出现,更没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集
4种垃圾收集器
原理图
Serial --- 串行垃圾回收器
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境
Parallel --- 并行垃圾回收器
多个垃圾收集线程并行工作,此时用户线程是暂停的适用于科学计算/大数据处理首台处理等弱交互场景。
CMS --- 并发垃圾回收器
用户线程和垃圾收集线程同时执行(不一定是并行的,可能交替执行),不需要停顿用户线程互联网公司多用它,适用对响应时间有要求场景
G1 --- G1垃圾回收器
G1 垃圾回收器将对内存分隔成不同的区域 , 然后并发的对其进行垃圾回收
△怎么查看服务器默认的垃圾收集器是哪个? 生产上如何配置垃圾收集器的? 请你谈谈你对垃圾收集器的理解???
怎么查看默认的垃圾收集器是哪个?
java -XX:+PrintCommandLineFlags -version
-XX:+UseParallelGC
默认的垃圾收集器有哪些?
Java 的GC 回收类型主要有几种
UseSerialGC
UseParallelGC(默认)
UseConcMarkSweepGC
UseParallelNewGC
UseParallelOldGC
UseG1GC
垃圾收集器
垃圾收集器就来具体实现这些GC算法并实现内存回收, 不同的厂商,不同版本的虚拟机实现差别很大,Hotspot 中包含的收集器如下图所示
红色表示java8版本开始,对应的垃圾收集器 Deprecated ,不推荐使用
参数预先说明
DefNew
Default New Generation
Tenured
Old
ParNew
Parallel New Generation
PSYoungGen
Parallel Scavenge
ParOldGen
Parallel Old Generalation
JVM 的 Server/Client 模式分别是什么意思
适用范围: 只需要掌握 Server 模式即可,Client 模式基本不会用
操作系统 1), 32位操作系统,不论硬件如何都默认使用 Client 的JVM模式
2), 32位其它操作系统,2G内存同时又2个cpu 以上用Server模式,低于 该配置还是 Client模式
3), 64位 Only server 模式
新生代
串行GC(Serial) / (Serial Copying)
一个单线程的收集器,在进行垃圾收集的时候,必须暂停其它所有的工作线程直到它收集结束
串行收集器时最古老,最稳定以及收集效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(stop -the- world 状态) ,虽然在收集过程中需要暂停所有其它的工作线程,但是它简单高效,对于限定单个CPU 环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此 Serial 垃圾手气其依然是 java 垃圾收集器依然是java虚拟机运行在client 模式下默认新生代垃圾收集器
对应参数是: -XX:+UserSerialGC
开启后会使用: Serial(Young 区用) +Serial Old (old 区用)的收集器组合 ,表示: 新生代,老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
Case 代码验证
结果看 def new generation 和 tenured generation 在收集器进行垃圾回收时, 新生代用的是串行垃圾收集器,老年代用的也是串行垃圾收集器
并行GC(ParNew)
使用多线程进行垃圾回收,在垃圾收集时,会 stop-the -world 暂停其它所有的工作线程直到它收集结束
ParNew 收集器其实就是Serial 收集器新生代的并行多线程版本,最常见的应用场景是配合 老年代的CMS GC工作,其余的行为和Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其它的工作线程,它和很多java虚拟机运行在server 模式下新生代的默认垃圾收集器
常见对应JVM参数: -XX:+UseParNewGC 启动ParNew 收集器,只影响新生代收集,不影响老年代
开启上述参数后, 会使用: ParNew(Young 区用)+ Serial Old 的收集器组合,新生代使用复制算法,老年代采用 标记整理算法
Case 案例
ParNewGC +Tenured 这样的搭配,Java8已经不推荐使用了,从这里可以看出
备注, -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数
并行GC (Paralle ) /(Parallel Scavenge)
Paralle Scavenge 收集器类似 ParNew 也是新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量有限收集器,一句话: 串行收集器在新生代和老年代的并行化
可控制的吞吐量 (Toughout =运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即 比如程序运行100分钟,垃圾收集时间1分钟,弹出量也就是99%), 高吞吐量意味着高效利用CPU 的时间,它多用于后台运算不需要太多交互任务
自适应调节策略也是 ParalleScavenge 收集器与ParNew 收集器的一个重要区别, (自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间,-XX:MaxGCPauseMillis) 或最大的吞吐量
常见JVM参数 : -XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活) 使用 parallel Scavenge 收集器 , 开启该参数后,新生代使用复制算法,老年代使用标记-整理算法
-XX:ParallelGCThreads=数字N ,表示启动多少个GC线程 cpu>8,N=5/8 , cpu<8 ,N=实际个数
Case案例
老年代
串行GC(Serial Old)/(Serial MSC)
Serial Old 是Serial 垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要运行在 Client 默认的Java 虚拟机默认的老年代垃圾收集器
在Server 模式下,主要有两个用途
1), 在JDK1.5之前版本中与新生代的 Parallel Scavenge 收集器搭配使用
2), 作为老年代版本中使用CMS 收集器的后备垃圾收集方案
Case 案例
并行GC(Parallel Old) / (Parallel MSC)
Parallel Old 收集器是Parallel Scavenge 的老年代版本,使用多线程的标记-整理算法,Parallel Old 收集器在JDK1.6才开始使用
在JDK1.6之前,新生代使用 Parallel Scavenge 收集器只能搭配老年代的 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
Case 案例
我们修改了老年代的UseParallelOldGC 这样新生代自动为我们设置了 ParallelScavenge
如果我们把 老年代的删除 UseParallelOldGC 这样结果和这个结果相同,因为JDK1.8默认就是这个组合
并发标记清除GC(CMS)
CMS 收集器(Concurrent Mark Sweep : 并发标记清除) 是一种以获取最短回收停顿时间为目标的收集器
适合应用在互联网网站或者B/S系统服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短
CMS 非常适合堆内存大,CPU核数多的服务端应用,也是G1出现之前大型应用的首选收集器
Concurrent Mark Sweep 并发标记清除, 并发收集底停顿,并发指的是与用户线程一起执行
JVM参数 : -XX:+UseConcMarkSweepGC 开启该参数后会自动将-XX:+UseParNewGC打开 , 开启该参数后,使用 ParNew(新生代) +CMS(老年代)+Serial Old 的收集器组合, Serial Old 将作为CMS出错后的后背收集器
4步过程
1),初始标记 (CMS initial mark)
只是标记一下 GC Roots 能直接关联的对象,速度很快,任然需要暂停所有的工作线程
2),并发标记(CMS concurrent mark )和用户线程一起
进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程,主要标记过程,标记全部对象
3),重新标记(CMS remark)
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停虽有的工作线程
由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
4),并发清除(CMS concurrent sweep)和用户线程一起
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清理对象
由于耗时最长的并发标记和并发清理过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行
优点
并发收集底停顿
缺点
并发执行堆cpu资源压力比较大
由于并发进行,CMS在收集与应用线程同时会增加对堆内存的占用,也就是说,CMS 必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS 回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大的停顿时间
采用的标记清除算法会导致大量的内存碎片
标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩,CMS也提供了参数 -XX:CMSFullGCsBeForeCompation(默认为0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC
Case 案例
如何选择垃圾收集器??
单CPU 或小内存,单机程序
-XX:+UseSerialGC
多CPU需要大吞吐量,如后台计算型应用
-XX:+UseParalleGC / -XX:+UseParalleOldGC
多CPU ,追求低停顿时间,需快速响应如互联网应用
-XX:+UseConcMarkSweepGC
△G1 垃圾收集器
以前垃圾收集器特点
年轻代和老年代是各自独立且连续的内存块
年轻代收集使用但 eden +so +s1 进行复制算法
老年代收集必须扫描整个老年代区域
都是以尽可能少而快速地执行GC为设计原则
G1( Garbage - First )是什么
是一款面向服务端应用的收集器,应用在多处理和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集器暂停时间的要求,另外还具备以下特性
像CMS收集器一样,能与应用线程并发执行
整理空闲空间更快
需要更多的时间预测GC停顿的时间
不希望牺牲大量的吞吐性能
不需要更大的Java Heap
G1 收集器的设计目标是取代CMS 收集器, 它同CMS相比,在以下方面表现的更出色
G1 是一个有整理内存的垃圾收集器,不会产生很多内存碎片
G1的 Stop the world 更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
CMS 垃圾收集器虽然减少了暂停应用程序的运行时间, 但是它还是存着内存碎片的问题,与是,为了去除内存碎片的问题,同时又保留了CMS垃圾收集器低暂停时间的优点,Java7发布了一款新的垃圾收集器---G1垃圾收集器
G1 是2012年才在JDK1.7中可用, oracle 准备在JDK9中将G1 变为默认的垃圾收集器来替代CMS,它是一款面向服务应用的收集器,主要应用在多CPU 和大内存服务环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换Java8以前的CMS收集器
主要改变是将 Eden ,Survivor 和 Tenured 等内存区域不再是连续的了, 而是变为了一个个大小一样的 region ,每个region 从1M到32M不等,一个 region 有可能属于 Eden ,Survivor 或者 Tenured 等区域
G1 特点
G1 充分利用多CPU , 多核环境硬件优势,尽量缩短STW
G1 整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
宏观上看G1 之后不再区分年轻代和老年代,把内存划分为成多个独立的子区域(Region) ,可以理解为一个围棋棋盘的意思
G1 收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但是它们不再是物理隔离的,而是一部分Region 的集合且不需要Region 是连续的, 也就是说依然会采用不同的GC方式来处理不同的区域
G1 虽然也是分代收集器,但整个内存不分区不存在物理上的年轻代和老年代的区别,也不需要完全独立 survivor (to space) 堆做复制准备, G1 只有逻辑上的分代概念,或者说每个分区都可能随G1 的运行在不同代之间前后切换
底层原理
Region 区域化垃圾收集器
最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可
G1 算法将堆划分为若干个区域(Region) 它仍然属于分代收集器, 这些Region 的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活的对象拷贝到老年代或者Survivor空间
这些Region 的一部分包含老年代,G1收集器通过将对象从一个区域复制到另一个区域,完成了清理工作,这就意味着,在正常的处理过程中,G1完成了对压缩,这样也就不会有CMS 内存碎片问题的存在了
在G1 中,还有一种特殊的区域,叫 Humongous (巨大的)区域,如果一个对象占用空间超过了分区容量的50%以上,G1收集器就认为这个是巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集造成负面影响,为了解决这个问题,G1 划分了一个Humongous 区,用它来专门存放巨型对象,如果H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC
G1回收步骤
针对Eden 分区进行收集,Eden 区耗尽后会触发,主要是小区域收集+形成了连续的内存块,避免了内存碎片
Eden 区的数据移动到Survivor 区,假如出现了 Survivor 区空间不够,Eden 区会部分晋升到Old 区
Survivor 区的移动到新的 Survivor 区,部分数据晋升到 Old 区
最后Eden 区收拾干净了,GC结束,用户应用程序继续执行
G1 收集器4步骤
1), 初始标记 : 只标记GC Roots 能直接关联到的对象
2),并发标记: 进行GC Roots Tracing 的过程
3), 最终标记: 修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
4),筛选回收: 根据时间来进行价值最大化的回收
Case 案例
常用配置参数(了解)
-XX:+UseG1GC
采用 Garbage First (G1) 收集器
-XX:G1HeapRegionSize=n
此参数的默认值根据堆大小的人工进行确定。最小值为 1Mb 且最大值为 32Mb。
-XX:MaxGCPauseMillis=n
设置最大GC 暂停时间。这是一个大概值,JVM 会尽可能的满足此值
-XX:InitiatingHeapOccupancyPercent=n
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。默认值 45.
-XX:ConcGCThreads=n
并发垃圾收集器使用的线程数。默认值因与 JVM 运行的平台而不同。
-XX:G1ReservePercent=n
设置作为空闲空间的预留内存百分比以降低晋升失败的可能性。默认值10
和CMS相比的优势
1),G1 不会产生内存碎片
2), 可以精确控制停顿,该收集器是把整个堆(新生代,老年代) 划分为多个固定的内存区域,每次根据允许停顿的时间去收集垃圾最多的区域
生产环境服务器变慢,诊断思路和性能评估谈谈?
整机 : top
按1 可以查看是那个cpu 比较慢,主要看图片这两个
top的精简版本 uptime
17:12:25 up 12 min, 2 users, load average: 1.98, 1.84, 1.03
CPU : vmstat
一般vmstat 工具的使用是通过 两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数
- procs
r : 运行和等待CPU时间片的进程数,原则上1 核的CPU 的运行队列不要超过2,整个系统队列不能超过总核数的2倍,否则代表系统资源压力过大
b : 等待资源的进程数,比如正在等待磁盘I/O , 网络I /O 等
-cpu
us : 用户进程消耗cpu 时间百分比,us值越高,用户进程消耗cpu 时间多,如果长期大于50% , 优化程序
sy : 内核进程消耗的cpu 时间百分比
us+sy 参考值为80% ,如果大于80% 说明可能存在cpu 不足
查看cpu 核信息
mpstat -P ALL 2
每个进程使用cpu 的用量分解信息
pidstat -u 1 -p 进程编号
内存: free
20%<应用程序可用内存/系统物理内存<70%内存基本够用
pidstat -p 进程号 -r 采样间隔秒数
硬盘: df
网盘IO: iostat
util 一秒钟有百分之几的时间用于I/O 操作,接近100% 时,表示磁盘带款跑满,须臾奥优化程序或者增加磁盘
pidstat -d 采样间隔秒数 -p 进程号
网络IO: ifstat
△假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位
结合Linux 和JDK命令一块分析
案例步骤
1),先用top 命令找出cpu 占比最高的
2), ps -ef或者 jps 进一步定位,得到是一个怎么样的一个后台程序给我惹事
3), 定位具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
找出线程号 2361
参数解释
-m 显示所有的线程
-p pid 进程使用cpu 的时间
-o 该参数是用户自定义格式
4), 将需要的线程ID 转换为16进制格式(英文小写格式)
5), jstack 进程ID |grep tid (16进程线程id小写英文) -A60
jstack 2327(1步得到线程号)|grep 939[]进程号-4步骤] -A60[打印60行]
对于JDK 自带的JVM监控和性能分析工具用过哪些? 一般你是怎么使用的?
oracle官方文档/lib/
性能监控工具
jps(JVM Process Status Tools):虚拟机进程状况工具
jinfo(Configuration Info for java):Java配置信息工具
jmap(Memory Map for java):java内存映像工具
jstack(Stack Trace for java):java虚拟机自带的堆栈跟踪工具
github 骚操作
常用词含义
watch : 会持续受到该项目的动态
fork : 复制某个项目到自己的github 仓库中
star : 可以理解为点赞
clone : 将项目下载到本地
follow : 关注你感兴趣的作者,会收到它们的动态
in 关键词限制搜索范围
公式: XXX 关键词 in:name 或 description 或 readme
XXX in:name
XXX in:description
XXX in:readme
可以组合使用
XXX in:name,readme
stars 或 fork 数量关键词去查找
公式 xxx关键字 stars 通配符
:> 或者 :>=
范围 数字1..数字2
查找springboot stars数大于等于5000的项目
查找forks 大于500的springcloud项目
查找fork 在100到200之间并且stars数在80到100之间的springboot项目
awesome 加强搜索
awesome 关键字
awesome 系统一般是用来收集学习,工具,书籍类的项目
高亮显示一行代码
公式
1行
url地址+#L数字
多行
url地址+L数字1-L数字2
项目内搜索
英文t
搜索某个地区内的大佬
公式
location:地区
language:语言
location:hangzhou language:java
java 基础
java字符串常量池
String 类的 intern()
考点? 为什么s1 == s1.intern 结果为true 而s2 == s2.intern() 结果为false
方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,所以这两个区域溢出测试可以放在一起执行,前面曾经提到 Hotspot 从JDK1.7之后开始逐步去永久代的计划, 并在JDK1.8中完全使用元空间来代替永久代的背景故事
Spring.intern() 是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String 对象的引用,否则,会将此String 对象包含的字符串添加到常量池中,并返回此String 对象的引用
why
按照代码结果,java 字符串答案为 false 必然是两个不同的java ,那另一个java 字符串是如何加载进来的?
有一个初始化的Java 字符串(JDK 娘胎自带的) 在加载 sun.misc.Version 这个类的时候进入常量池
Open JDK1.8源码说明
System 代码解析
initializeSystemClass
Version
类加载器和rt.jar --根加载器提前部署加载rt.jar
将jdk1.8源码 配置到idea 上
将下载的 openJdk1.8下载解压并放入idea 的sdk 的classpath 的+
两个数求和
暴力破解法
map 解法
手写 LRU 算法--redis 最后一章
java中文在线文档
spring
spring 的aop 顺序
Aop 常用注解
@Before ---前置通知,目标方法之前执行
@After -- 后置通知,目标方法之后执行
@AfterReturning --返回后通知: 执行方法结束前执行(异常不执行)
@AfterThrowing -- 异常通知,出现异常时候执行
@Around -- 环绕通知,环绕目标方法执行
面试题
你肯定知道spring ,那你说说 aop 的全部通知顺序,springboot 或springboot2对aop执行顺序影响?
说说你使用aop 中碰到的坑?
springboot 1.5.9版本案例
service
aspt
测试类
结果正常执行 输入结果1,2
结果错误执行 输入1,0
springboot 2.x.版本案例
结果正常执行 输入结果1,2
结果错误执行 输入1,0
spring 循环依赖
大厂面试题
你解释一下spring中的三级缓存?
三级缓存分别是什么? 三个Map 有什么异同
什么是循环依赖?请你谈谈,看过spring 源码? 一般我们说的spring 容器是什么
如何检测是否存在循环依赖? 实际开发中见过循环依赖的异常?
多例情况下,循环依赖问题为什么无法解决?
什么是循环依赖?
多个Bean 之间相互依赖,形成了一个闭环,比如A依赖于B , B依赖于C, C依赖于A
通常来说, 如果问 Spring 容器内部如何解决循环依赖,一定是指 默认的单例Bean 中,属性互相引用的场景
两种注入方式对循环依赖的影响
结论: 我们AB循环依赖问题只要 A的注入方式是 setter 且 singleton 就不会有循环依赖问题
spring 容器循环依赖报错演示 BeanCurrentlyInCreationException
循环依赖现象在spring容器中注入依赖对象,有2中情况
1)构造器方式注入
ServerA
ServerB
测试类
2), 以set 方式注入依赖(可以解决循环依赖问题)
ServerA
ServerB
测试类
重要 Code 案例演示 -- spring容器
默认单例 (singleton) 的场景是支持循环依赖的,不会报错
原型( prototype ) 的场景是不支持循环依赖的, 会报错
重要结论 ( spring 内部通过 3级缓存来解决循环依赖)
DefaultSingletonBeanRegistry
只有单例的Bean 会通过三级缓存提前暴露来解决循环依赖的问题, 而非单例的bean ,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean 是没有缓存的,不会将其放入到三级缓存中
singletonObjects(一级缓存)
存放已经经历了完整声明周期的Bean 对象
earlySingletonObjects(二级缓存)
存放早期已经暴露出来的Bean 对象,Bean 的声明周期未结束(属性还未填充完毕)
singletonFactories(三级缓存)
存放可以生成Bean 的工厂
循环依赖 Debug
实例化 / 初始化
实例化
内存中申请一块内存空间(租赁好房子,自己的家具东西还没有搬进去)
初始化属性填充
完成属性的各种赋值(装修,家电家具进场)
3个Map 和 四大方法,总体相关对象
A/B 两对象在三级缓存中的迁移说明
1), A创建过程中需要B 于是A将自己放入到三级缓存里面, 去实例化B
2), B实例化的时候发现需要A , 于是B 先查一级缓存,没有再查二级缓存,还是没有再查三级缓存,找到了A 然后把三级缓存中的这个A放入二级缓存中,并删除三级缓存里面的A
3),B 顺利初始化完毕,将自己放到一级缓存里面(此时B 里面的A 依然是创建状态),然后回来接着创建A,此时B已经创建完成,直接从一级缓存中拿到了B ,然后完成创建,并将A放入一级缓存里面
总结 spring是如何解决循环依赖?
运行流程
1), 调用 doGetBean() 方法,想要获取 beanA,于是调用 getSingleton() 方法从缓存中查找 beanA
2), 在getSigleton() 方法中,从一级缓存中查找,没有,返回null
3), doGetBean() 方法中获取到 beanA为null, 于是走对应的处理逻辑,调用 getSingleton() 的重载方法( 参数为 ObjectFactory )
4), 在getSingleton() 方法中,先将 beanA_name 添加到一个集合中,用于标记该bean 正在创建中,然后回调匿名内部类的 creatBean
5), 进入 AbstractAutowireCapableBeanFactory#doCreateBean ,先反射调用构造器创建出beanA的实例,然后判断,是否为单例 , 是否允许提前暴露引用(对于单例一般为true),是否正在创建中(即是否在第四步集合中) 判断为true 则将 beanA添加到 三级缓存中
6),对 beanA 进行属性填充,此时检测到 beanA 依赖于 beanB ,于是开始查找 beanB
7), 调用 doGetBean() 方法和上面 beanA 的过程一样,到缓存中查找 beanB ,没有则创建,然后给 beanB 填充属性
8), 此时 beanB依赖于 beanA 调用 getSingleton() 获取 beanA 一次从一级,二级,三级缓存中查找,此时三级缓存中获取到 beanA 的创建工厂, 通过创建工厂获取到 singletonObject ,此时这个 singletonObject 指向的就是上面 doCreateBean() 方法获取实例化beanA
9), 这样beanB 就获取到了 beanA 的依赖,于是beanB 顺利完成初始化,并将 beanA 从三级缓存中移动到二级缓存中
10), 随后beanA 继续他的属性填充工作,此时也获取到了 beanB , beanA也随之完成了创建,将beanA从二级缓存移动到一级缓存中
Redis
安装redis6.0.10
中文官网
以英文官网
redis 传统5大数据结构的落地应用
8大常用数据类型
list
set
zset
string
hash
bitmap(位图)
hyperloglog统计()
GEO(地理)
命令不区分大小写,而key 是区分大小写的
help @类型名词
String
最常用
set k v
get k v
mset k1 v1 k2 v2 ...
mget k1 k2 k3 ...
incr k
decr k
strlen k
分布式锁
setnx k v
set k v [EX seconds][PX milliseconds][NX|XX]
EX: key在多少秒之后过期
PX : key在多少毫秒之后过期
NX: 当key不存在的时候,才创建 key,效果等同于 setnx
XX: 当key存在的时候覆盖key
应用场景
商品编号,订单号采用INCR命令生成
是否喜欢的文章
hash
类似java Map<String,Map<Object,Object>>
一次设置一个值
hset key field value
一次获取一个字段值
hget key field
设置多个值
hmset key field1 values1 field2 values2 ...
hmget key field1 field2 ...
获取所有字段
hgetall key
获取某个key内的全部数量
hlen key
删除一个key
hdel key
应用场景
购物车早期,当前小中厂可用
新增商品
hset shopcar userid 100 3344[产品id] 1
新增商品
hset shopcar userid 100 3355[产品id] 1
增加商品数量
hincrby shopcat userid 100 3355[产品id] 1
商品总数
hlen shopcar userid 100
全部选择
hgetall shopcar userid 100
list
向左边添加元素
lpush key value ...
向右添加元素
rpush key values ...
查看列表
lrange key start stop
获取列表个数
llen key
引用场景
微信文章公众号 1:N
set
添加元素
sadd key member
删除元素
srem key member
获取集合中的所有元素
smembers key
判断元素是否在集合中
sismember key member
获取集合中的元素个数
scard key
从集合中随机弹出一个元素,元素不删除
srandmember key [数字]
从集合中随机弹出一个元素,出一个删一个
spop key [数字]
集合运算
集合的差集运算A-B
属于A但不属于B的元素构成集合
sdiff key
集合的交集运算ANB
属于A也属于B共同拥有的元素构成的集合
sinter key
集合的并集运算U
属于A或者属于B 元素合并后的集合
sunion key
应用场景
微信抽奖小程序
微信朋友圈点赞
微博好友关注社交关系
qq内退可能认识的人
zset
向有序集合中加入一个元素和该元素的分数
添加元素
zadd key score member
按照元素分数从小到大的顺序返回索引从 start 到 stop 之间的所有元素
zrange key start stop
获取指定分数范围的元素
zrangebyscope key min max
增加某个元素的分数
zincrby key incrment member
获取集合中元素的数量
zcard key
获取指定分数范围内的元素个数
zcount key min max
按照排名范围删除元素
zremrangebyrank key start stop
获取元素的排名
从小到大
zrank key member
从大到小
zrevrank key member
应用场景
根据商品销售对商品进行排序显示
抖音热搜
知道分布式锁? 有哪些实现方案? 你谈谈对redis 分布式锁的理解,删除key的时候有什么问题?
面试题
redis 除了拿来做缓存,你还见过基于 redis 的什么用法?
redis做分布式锁的时候有什么需要注意的问题
如果是 redis 是单点部署的,会带来什么问题
那你准备怎么解决单点问题?
集群模式下, 比如主从模式,有没有什么问题呢?
那你就简单的介绍一下 redlock 吧,你简历上写 redisson,你谈谈
redis 分布式锁如何续期,看你狗你知道?
Case 案例(boot+redis)
使用场景: 多个服务间,保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
建moudle
boot-redis1001
boot-redis1002
pom
yml
主启动
业务类
config
controller
小测试
http://localhost:1001/goods
上面的问题代码
1),单机版没有加锁
如何解决呢?
用 synchronized 代码块把业务代码围起来[2.0版本程序]
思考, 加 synchronized 还是加 reetranklock 俩个都可以?
都可以,synchronized 是会一直等待上一个线程处理完成之后,下一个才能进行处理(不见不散)
reetanklock 是可以使用trylock() 设置多少秒后,不再继续等待,让下一个线程来处理 (过期不候)
2), nginx 分布式微服务架构
分布式部署后,单机锁还是出现超卖现象,需要分布式锁
如何产生问题
nginx 配置负载均衡 下面这2个微服务
使用jmeter 高并发模拟
就会产生一个商品被多卖2次
如何解决呢?
redis 分布式锁 setnx
redis 具有极高的性能,且其命令对分布式锁支持友好,借助set 即可实现加锁处理
3.0升级后的代码
3), 如果出现异常的后,可能无法释放锁,必须在代码层面finally 释放锁
解决
4.0版本程序
4),部署了微服务jar 包的机器挂了,代码层面根本没有走到 finally 这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
解决,需要对 key 设置过期时间的设定,修改为5.0如下
5), 设置key +过期时间分开了,必须要合并成一行具备原子性
解决,升级为6.0版本
6),张冠李戴,删除了别的线程的锁
自己删除自己的锁,不许删除别人的,修改为7.0版本的
7), finally 块的判断+del 删除操作不是原子性的
如何解决呢?
1使用redis 事务,修改为 8.0版本
2使用 LUA脚本
8), 确保redis lock过期时间大于业务执行时间的问题
redis 分布式锁如何实现续期?
集群+CAP 对比 zookeeper
redis -- AP
redis 异步复制造成的锁丢失,比如主节点没来的及把刚刚set 进来的数据给从节点,就挂了
zookeeper -- CP
9),综合上述
redis 集群环境下,我们自己写的也不 ok , 直接上 redlock 之 reedisson 实现
修改config 代码
升级为9.0版本代码,修改controller代码
10), 在超高并发下面可能出现这样一个异常
attempt to unlock ,not locked by current thread by node id
升级为10.0版本
redis 缓存过期淘汰策略
面试题
生产上你们的redis 内存不设置多少?
如果不设置maxmemory 或者设置它的值为0, 在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB 内存
生产上如何配置? 一般推荐redis设置为内存为最大物理内存的3/4
如何配置,修改redis 的内存大小
查看redis 最大占用内存 redis.conf 文件
1), 修改 redis.conf 文件
2),动态手工修改
什么命令查看redis 内存使用情况
info memory
如果redis 内存满了你怎么办
redis 清理内存的方式? 定期删除和惰性删除了解过?
redis 的缓存淘汰策略
redis 的 LRU 了解过? 可否手写一个 LRU算法
...
redis 的LRU算法简介
0 条评论
下一页