JVM
Basic
JVM主要组成
- 运行时数据区(即JVM内存区域)
- 类加载子系统
- 执行引擎
- 本地库接口
- 本地方法库
JVM核心功能
- 执行指令:解释和执行字节码
- 管理内存:分配和回收内存块
Advanced
Region
JVM运行数据分区
- 线程共享
- 方法区:包含常量池和类的元数据(放在永久代或者元空间中)
- 堆区:包含对象的数据
- 线程私有
- 虚拟机栈:包含局部变量和对象的引用
- 本地方法栈:
- 程序计数器:
Allocate
JVM内存分配策略
- 堆上分配
- 栈上分配
- TLAB上分配
ps:TLAB(Thread-local allocation buffer):线程私有的内存区域
JMM
Basic
JMM内存模型的作用:定义了线程和主内存之间的操作和关系
- 三大特性是基本原则
- as-if-serial和happens-before是指导思想
- 内存屏障是实现手段
Advanced
Feature
- JMM三大特性详解
- JMM三大特性之指令重排入门
- JMM三大特性之指令重排详解
- JMM三大特性之内存可见性入门
- JMM三大特性之内存可见性入门
- JMM三大特性之内存屏障入门
- JMM三大特性之内存屏障详解
- JMM三大特性之volatile和指令重排
- JMM三大特性之volatile和内存可见性
- JMM三大特性之volatile和内存屏障
- JMM三大特性之volatile和happens-before
- JMM三大特性之volatile和lock前缀指令
- JMM三大特性之MESI缓存一致性协议入门
- JMM三大特性之MESI缓存一致性协议详解
- JMM三大特性之MESI缓存一致性协议之状态转移
- JMM三大特性之cpu有了缓存一致性协议为什么还需要volatile关键字
- JMM三大特性之总线锁和缓存锁(MESI)入门
- JMM三大特性之总线锁和缓存锁(MESI)详解
- 原子性:通过互斥机制实现,来解决资源的互斥使用问题
- 可见性:通过内存屏障实现,解决数据的不一致性问题
- 有序性:通过内存屏障实现,解决指令的重排序问题
MESI缓存一致性协议
- M:缓存行已修改
- E:缓存行是独占的且未被修改(变量只被一个处理器读取过)
- S:缓存行是共享的且未被修改(变量被多个处理器读取过)
- I:缓存行已失效
ps:处理器为了性能并未完全的遵守MESI缓存一致性协议,会存在弱一致的问题,需要使用volatile关键字来插入内存屏障从而保证强一致性
MESI缓存一致性协议之变换过程
- 处理器1读取了变量,此时处理器1的缓存行变为E状态
- 处理器2读取了变量,此时处理器1和2的缓存行变为S状态
- 处理器1修改了变量,此时处理器1的缓存行变为M状态,处理器2的缓存行变为I状态
MESI缓存一致性协议之回写策略
- 写通(write through):同步更新操作,修改缓存的数据后会
立即
更新到内存中 - 写回(write back):异步更新操作,修改缓存的数据后会
稍后
更新的内存中
Principle
as-if-serial:单线程原则,无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致
happens-before:多线程原则,在发生操作B之前,操作A产生的影响都能被操作B观察到
happens-before的8大原则
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作
- lock的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作
- volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作
- 线程启动的happen-before原则:线程的start操作happen-before此线程的其它操作
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断的代码
- 线程终止的happen-before原则:线程中的所有操作都happen-before线程的终止操作
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用
- happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作
Operation
JMM的8种原子操作
- lock(锁定):
- 对象:作用于
主内存
- 作用:对变量进行
加锁
- 对象:作用于
- unlock(解锁):
- 对象:作用于
主内存
- 作用:对变量进行
解锁
- 对象:作用于
- read(读取):
- 对象:作用于
主内存
- 作用:将变量的值从
主内存
传输到工作内存
中
- 对象:作用于
- load(载入):
- 对象:作用于
工作内存
- 作用:将变量的值
加载
到工作内存
中的变量副本中
- 对象:作用于
- use(使用):
- 对象:作用于
工作内存
- 作用:执行引擎
使用
变量的值
- 对象:作用于
- assign(赋值):
- 对象:作用于
工作内存
- 作用:执行引擎
设置
变量的值
- 对象:作用于
- store(存储):
- 对象:作用于
工作内存
- 作用:将变量的值从
工作内存
传输到主内存
中
- 对象:作用于
- write(写入):
- 对象:作用于
主内存
- 作用:将变量的值
写入
到主内存
中的变量中
- 对象:作用于
JMM的8种操作要求
- 操作要完整
- read和load操作必须同时出现且必须顺序执行(不是连续执行),store和write同理
- 即不允许变量从主存读取时工作内存却不接受或者从工作内存写入时主存却不接受的情况
- 变更必须同步
- 不允许线程丢弃它的最近的assign操作
- 即变量在工作内存中改变了就必须同步回主内存
- 未变更不允许同步
- 不允许线程无原因地(比如没有发生过assign操作)将数据从工作内存同步回主内存中
- 即变量在工作内存中未改变就不能同步回主内存
- 不能使用未初始化的变量
- 不允许在工作内存中直接使用一个未被初始化(load或assign)的变量
- 即对一个变量实施use和store操作之前必须先执行过了load和assign操作
- lock需要支持可重入
- 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
- lock时需要将工作内存中的变量副本清空
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
- unlock之前得先执行过lock
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
- unlock时需要将工作内存中的变量副本同步回主内存中
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
Garbage
Basic
垃圾分代技术
- 新生代(Young Generation):属于内存中的
堆区
,包含1个Eden
区和2个Survivor
区 - 老年代(Old/Tenured Generation):属于内存中的
堆区
- 永久代(Permanent Generation):属于内存中的
方法区
ps:如果只有一个Survivor区,Survivor区会存在内存碎片
ps:如果没有Survivor区,垃圾对象会直接进入老年代,而老年代的回收效率很低
ps:java8中永久代已经被元空间(Metaspace)取代了
永久代的缺点
- 动态生成的类及方法的信息等比较难确定其大小,容易超出永久代的限制而引发OOM
- 永久代的垃圾回收效率低下
- 永久代的字符串常量池无法回收
垃圾标记算法
对象存活判断方法
- 引用计数算法:不能解决循环引用的问题
- 可达性分析算法:可以解决循环引用的问题
对象存活标记算法
- 三色标记法(标记对象是否活着)
并发标记步骤
- 初始标记
- 并发标记
- 重新标记
- 并发清除
并发标记问题
- 多标:某个
灰色对象A
的所有引用被断开,因为A已经被标记为灰色了,所以标记完成后A会被标记为黑色且不可以回收
,然而此时A因引用被断开已变为不可达,造成了A应该回收却不会回收的问题 - 漏标:某个
白色对象A
的所有引用被断开,因为A的所有引用都被断开了,所以标记完成后A会被标记为白色且可以回收
,但是此时有一个被标记为黑色的对象B
重新引用了A,因为B为黑色代表已经标记完成,所以不会通过B重新标记A,然而此时A因被重新引用已变为可达,造成了A不该回收却被回收了的错误
ps:多标会造成应该回收的不被回收,问题不大,可以在下一次进行回收
ps:漏标会造成不该回收的被回收了,属于bug,jvm通过重新标记解决了
并发漏标解决
漏标形成条件
- 至少一个有黑色对象在自己被标记之后引用了白色对象A
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象A的引用
漏标解决方案
- 增量更新算法(IU:Incremental Update):破坏漏标形成条件1,将白色对象标记为灰色并放到Mod-Union Table中,以便重新标记阶段能够重新扫描这个对象
- 原始快照算法(SATB:Snapshot At The Beginning):破坏漏标形成条件2,在白色对象的所有引用被删除前将白色对象标记放到线程私有的SATB Queue并汇总到全局的SATB Queue中,以便重新标记阶段能够重新扫描这个对象
ps:SATB比IU算法更快,因为IU需要重新扫描,SATB则是用空间换时间
ps:CMS用的是IU算法,G1用的是SATB算法
垃圾回收算法
- 标记清除算法(MarkSweep):清除对应的内存块,有内存碎片
- 标记复制算法(MarkCopy):复制到另一个区域,适合新生代使用
- 标记整理算法(MarkCompact):移动到区域的一端,适合老年代使用
ps:新生代存活对象少,复制开销小,移动开销大,所以适合用标记复制算法
ps:老年代存活对象多,复制开销大,移动开销小,所以适合用标记整理算法
垃圾回收时机
- Minor GC:回收新生代的垃圾
- Major GC:回收老年代的垃圾
- Full GC:回收新生代、老年代和永久代(方法区)的垃圾
Minor GC的触发条件
- Eden区空间不足时会触发
ps:Survivor区空间不足不会触发Minor GC,而是将对象直接迁移到老年代
Major GC的触发条件
- 老年代空间不足时触发
ps:Major GC执行时通常会先执行一次着Minor GC
Full GC的触发条件
- 调用System.gc时,建议系统执行Full GC,但是不一定要执行
- 老年代空间不足
- 方法区空间不足
垃圾回收过程
- 新创建的对象会放到Eden区
- Eden区满了会触发Minor GC的触发条件
- 经过扫描和标记后,将Eden区和S0区中的存活对象的年龄加1并进行移动
- 如果对象的年龄超限了(默认为15),则将对象移动到老年代
- 如果S1区满了或者剩余空间不足,则将对象移动到老年代
- 如果S1区没有满并且剩余空间充足,则将对象移动到S1区,并调换S0和S1区
垃圾回收器分类
- 新生代
- Serial:
串行回收器
,使用标记复制算法
- PN(ParNew):
并行回收器
,使用标记复制算法
,Serial的多线程版本
- PS(Parallel Scavenge):
并行回收器
,使用标记复制算法
,追求最大吞吐量
- Serial:
- 老年代
- Serial Old:
串行回收器
,使用标记整理算法
,Serial的老年代版本
- CMS(Concurrent Mark Sweep):
并发回收器
,使用标记清除算法
,追求最短停顿时间
- Parallel Old:
并行回收器
,使用标记整理算法
,Parallel Scavenge的老年代版本
- Serial Old:
ps:
吞吐量
是指用户线程时间除以总时间(用户线程时间和GC线程时间之和)的比值,因为总时间固定,则GC线程时间越少,用户线程时间越多,吞吐量越高
ps:停顿时间
是指用户线程被暂停(STW)的时间
ps:Parallel Scavenge
是通过增大内存空间
从而减少垃圾回收次数
的方式来提高吞吐量
的
ps:Concurrent Mark Sweep
是通过并发标记
的方式来减少停顿时间
的
ps:前台交互型
应用追求的是最短停顿时间
,比如Web服务
应用
ps:后台计算型
应用追求的是最大吞吐量
,比如大数据计算
应用
新生代垃圾回收器对比
- Serial:单线程标记,单线程回收,全程STW
- PN(ParNew):多线程标记,多线程回收,全程STW,Serial的
多线程版本
- PS(Parallel Scavenge):多线程标记,多线程回收,全程STW,追求
最大吞吐量
老生代垃圾回收器对比
- Serial Old:单线程标记,单线程回收,全程STW
- CMS(Concurrent Mark Sweep):初始标记时单线程,并发标记时多线程,重新标记时单线程,并发清除时多线程,初始标记和重新标记时会STW,并发标记和并发清除时不会STW,追求
最短停顿时间
- Parallel Old:多线程标记,多线程回收,全程STW,Parallel Scavenge的
老年代版本
主要垃圾回收器的特点
- Parallel Scavenge:吞吐量优先垃圾回收器
- 优点
- 相比于ParNew支持控制停顿时间和吞吐量
- 相比于ParNew多了自适应调节策略
- 缺点
- 相比于ParNew需要消耗更多的系统资源
- 优点
- CMS:低延迟垃圾回收器
- 优点
- 相比于Parallel Scavenge的停顿时间更短
- 缺点
- 相比于Parallel Scavenge的吞吐量更低
- 并发标记时多标的问题会产生浮动垃圾
- 使用的标记清除回收算法会导致内存碎片
- 优点
- G1:分区垃圾回收器
- 优点
- 相比于CMS使用了分区和标记整理算法解决了内存碎片的问题
- 相比于CMS可以更好的控制停顿时间
- 相比于CMS可以处理更大的内存堆
- 缺点
- 相比于CMS需要消耗更多的系统资源
- 优点
支持并发标记的垃圾回收器
- CMS(Concurrent Mark Sweep)
- G1(Garbage First)
比较经典的垃圾回收器
- PS(Parallel Scavenge):吞吐量优先垃圾回收器
- CMS(Concurrent Mark Sweep):低延迟垃圾回收器
- G1(Garbage First):分区垃圾回收器
垃圾回收器搭配
- Serial Old可以和Serial、PN(ParNew)、PS(Parallel Scavenge)搭配
- CMS(Concurrent Mark Sweep)可以和Serial、PN(ParNew)搭配
- Parallel Old只能和PS(Parallel Scavenge)搭配
Advanced
Serial
ParNew
Parallel Scavenge
Serial Old
CMS
Parallel Old
G1
ZGC
Class
类加载机制
类加载器分类
- 自举加载器:Bootstrap ClassLoader,负责加载
JAVA_HOME/jre/lib
目录中的jar包(rt.jar) - 扩展加载器:Extension ClassLoader,负责加载
JAVA_HOME/jre/lib/ext
目录中的jar包 - 应用加载器:Application ClassLoader,负责加载
classpath
目录中的jar包 - 自定义加载器:Custom ClassLoader,可以自己定义加载逻辑
ps:自举加载器又叫启动加载器,应用加载器又叫系统加载器
类加载过程
- 加载(load):将类文件的数据加载到内存中
- 链接(link):
- 验证(verify):检查类文件数据是否符合规范和要求
- 准备(prepare):为类成员分配内存和设置默认值
- 解析(resolve):将符号引用替换为地址引用
- 初始化(initiate):静态变量初始化和静态代码块初始化
- 使用(use):使用
- 卸载(unload):将类文件的数据从内存中卸载
双亲委派机制
双亲委派机制的作用
- 保证系统类先加载,避免攻击者替换系统类进行破坏活动
- 避免重复加载,前面的加载器加载后自己就不需要加载了
为什么有时候要打破双亲委派机制
- jdbc的DriverManager是由自举加载器加载的,而第三方厂商的DriverManager的实现类则是由应用加载器加载的,由于应用程序类加载器加载的类对启动类加载器是不可见的,导致找不到DriverManager的实现类,此时就需要打破双亲委派机制
- web容器中可以存放多个应用,而这些应用可能会使用版本不同的同名类,而双亲委派机制只会加载第一个同名类,所以就需要对应用的类进行隔离,此时就需要打破双亲委派机制
如何打破双亲委派机制
- 重写ClassLoader的loadClass自定义加载过程
- 利用Thread.currentThread().getContextClassLoader()返回的ClassLoader加载
- 利用java的SPI机制
ps:重写findClass方法可以自定义类加载过程,但不能打破双亲委派机制
Object
对象生命周期
- 实例化(Instantiation)
- 初始化(Initialization)
- 构造(Constrction)
- 使用(Usage)
- 析构(Destruction)
ps:
实例化(instantiation)
和初始化(initialization)
不是同一个概念,实例化会分配内存
和设置默认值
,初始化会设置初始值
ps:
默认值(default)
和初始值(initial)
不是同一个概念,比如int的默认值
为0
,初始值
可以为100
对象初始化过程
- 父类静态变量和代码块
- 子类静态变量和代码块
- 父类非静态变量和代码块
- 父类构造函数
- 子类非静态变量和代码块
- 子类构造函数
对象内存结构
- 在开启了压缩指针的情况下,Object 默认会占用 12 个字节,但是为了避免伪共享问题,JVM 会按照 8 个字节的倍数进行填充,所以会填充 4 个字节变成 16 个字节长度。
- 在关闭了压缩指针的情况下,Object 默认会占用 16 个字节,16 个字节正好是 8 的整数倍,因此不需要填充
Lifecycle
Compile
Runtime
Problem
jvm常见问题
- OOM
- cpu占用高
- 死锁
ps:内存溢出(Out Of Memory)和内存泄漏(Memory Leak)不是一个概念
jvm问题排查工具
jmap:排查内存问题(OOM)
jstack:排查线程问题(CPU负载高和死锁)
- 命令行工具
- jps:java进程查看工具
- jmap:java内存诊断工具
- jstack:java线程诊断工具
- jstat:JVM运行状态查看工具
- jinfo:JVM参数查看和调整工具
- jcmd:JVM综合诊断工具
- jhsdb:JVM综合诊断工具
- jhat:java内存分析工具(网页版)
- 可视化工具
- jconsole:java自带工具(老版)
- jvisualvm:java自带工具(新版)
- jprofile:商业工具(可以在idea中使用)
ps:jhat不常用(因为分析工作比较耗性能,一般不在应用服务器上用jhat分析,而是在其他机器上用jvisualvm分析)
jvm问题排查过程
- OOM问题排查
- 查看内存的统计信息:
jmap -histo pid
,可以看出当前哪个对象最消耗内存 - 查看内存的详细信息:
jmap -heap pid
,可以查看内存的配置和使用情况 - 导出内存的详细信息:
jmap -dump:file=a.dump pid
,然后导入到jvisualvm
中进行分析
- 查看内存的统计信息:
- CPU负载高问题排查
- 找出cpu占用飚高进程:
top
,然后按P
键后按照CPU负载排序并找导致cpu占用飚高的进程id - 找出cpu占用飚高线程:
top -H -p pid
,找出导致cpu占用飚高的线程id - 将线程id转化为十六进制:使用python的
hex
函数或者其他支持进制转换工具 - 查看cpu占用飚高线程的堆栈信息:
jstack pid | grep -A 10 tid(十六进制的线程id)
- 堆栈信息中会显示线程停留在代码的哪一行
- 找出cpu占用飚高进程:
- 死锁问题排查
- 用jstack查看是否有deadlock字样
- 用jvisualvm查看是否有死锁
ps:pid(进程id),tid(线程id)
Performance
Other
Linux
- summary
- top:可以查看cpu负载、进程的cpu和mem占用
- vmstat:可以查看系统的cpu、mem、disk的使用情况
- pidstat:可以查看进程的cpu、mem、disk的使用情况
- cpu
- mpstat:可以查看系统的cpu的使用情况
- mem
- free:可以查看系统的mem的使用情况
- disk
- iostat:可以查看系统的disk的读写情况
- net
- netstat:可以查看系统的net的连接情况