深入理解JVM虚拟机
2020-10-10 10:32:20 0 举报
AI智能生成
深入理解JVM,涵盖面试所有知识点,包括线程优化
作者其他创作
大纲/内容
JVM & GC
哪些对象需要回收?
java 堆中
java 对象在内存中的存储布局
markword
内存布局可以使用 org.openjdk.jol 这个 maven 库来查看
java 运行时内存布局
如何确定哪些对象需要回收?
再谈引用
强引用
软引用
弱引用
虚引用
GC 算法
引用计数式
如今主流 JVM 很少使用,不做介绍
缺点:不能解决循环引用
追踪式/可达性分析
分代收集(假说)理论:
弱分代假说: 绝大数对象都是朝生夕死的。
强分代假说:熬过越多垃圾收集的对象就越难消亡
跨代引用假说: 跨代引用相对于同代引用只占少数。
建立在分代收集理论至上的回收算法
标记清除
缺点: 产生甚多内存碎片
标记复制
缺点:浪费一半内存,大量内存复制开销
一般用在年轻代
标记整理
缺点:移动对象,造成 STW
一般用在老年代
HotSpot 虚拟机的垃圾收集器
HotSpot 虚拟机中出现过的垃圾收集器
Serial 收集器
ParNew 收集器
Parallel Scavenge 收集器
Serial Old 收集器
Parallel Old 收集器
CMS 收集器: 并发收集,低停顿标记-清除 + 标记-整理
建立在分代理论至上
CMS 是一种以获取最短回收停顿时间为目标的收集器
CMS 运行时内存布局
CMS YGC 过程
缺点:
1.采用标记清除:产生大量碎片空间,不利于大对象分配
2.处理器资源敏感
3.浮动垃圾问题
---------------------------------------G1 是一个划时代的 GC--------------------------从 G1 开始,最先进的垃圾收集器的设计都变为开始追求能够应付应用的内存分配速率, 而不追求一次性吧整个 java 堆收集干净,这样应用在分配,同时收集器在收集,只要手机速率跟得上分配速率, 那么一些就会运转的很完美。(乐观收集 <-->悲观收集)这种新的设计思路从工程上看是从 G1 兴起的,所以说 G1 是收集器技术发展的一个里程碑。
G1 收集器(JDK11 默认垃圾收集器)局部收集,Region
虽然也是建立在分代收集理论至上,但是内存布局独辟蹊径
开创了收集器面向局部收集的思路和基于 Region 的内存布局形式。
停顿时间模型
人们希望指定一个消耗在垃圾收集上的时间大概率不超过 N 毫秒的 这样的目标
如何实现 停顿时间模型?
局部收集
Region
G1 优先处理那些回收价值最大的 Region,这也是 Garbage First 的由来。
可以让用户指定一个期望的停顿时间是 G1 收集器很强大的一个功能。(推荐 100ms - 300ms),时间越长回收的对象就越多。
G1 收集流程
使用 G1 收集器思路遇到的问题?
Region 之间的跨代引用如何解决?
并发标记阶段如何保证收集线程和用户线程互不干扰?
怎样建立可靠的停顿预测模型?
G1 YGC 过程:
CMS 和 G1 比较
G1 优点(收集算法不同)
G1 缺点
在用户线程运行中,G1 垃圾收集时内存占用和运行时额外执行负载比较高
G1 和 CMS 如何选型?
还是看具体场景:小内存: CMS (6-8G)大内存: G1
低延迟的垃圾收集器(衡量垃圾收集器的三个指标)
内存占用
吞吐量
延迟
上面三个指标被称为不可能三角,就想 CAP 理论一样。最多只能满足其二。但是,在这三项指标里,延迟的重要性日益凸显,随着硬件性能的提升,内存更大更便宜CPU 更快更便宜,对内存占用和吞吐量都是有好处的。但是内存增大对延迟室友负面影响的。
垃圾收集器性能对比
ZGC (任意堆内存下把 GC 停顿时间限制在 10 毫秒以内,分配速率大于使用速率)
基于动态 Region布局
不设分代
读屏障
染色指针
内存重映射
收集算法是内存回收的方法论;垃圾收集器是内存回收的实践者。
两个主要功能:1.自动给对象分配内存。2.自动回收分配给对象的内存。
需要注意的是,Region 也是基于分代收集理论的,也有 Eden 区,Survivor 区,Old 区
虚拟机性能监控,故障处理工具
基础故障处理工具
jps:虚拟机进程状况工具
jps(JVM Process Status Tool)
列出正在运行的虚拟机进程
jstat:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)
用于监视虚拟机各种运行状态信息的命令行工具。
CMS
G1
jinfo:java 配置信息工具
jstack:java 堆栈追踪工具
jstack:(Stack Trace For Java)
用于生成虚拟机当前时刻的线程快照。(一般称为 threaddump 或者 javacore 文件)
jmap:java 内存映射工具
jmap(Memory Map For Java)
用于生成 堆 转储快照。(一般称为 heapdump 或者 dump 文件)
可视化故障处理工具(基础故障处理工具的集合)
JConsole:Java 监视与管理控制台
最古老
基于 JMX(java Management Extensions)的可视化监控管理工具通过 JMX 的 MBean 对系统进行信息收集和参数动态调整。
使用方法:
在 JDK 安装的目录的/bin 下双击 jconsole 程序
内存监控
线程监控:了解线程长时间停顿(等待外部资源,死循环,锁等待)原因
JHSDB:基于服务型代理的调试工具
JDK9 开始提供
VisualVM
JDK9 之前 JDK 自带,不是 JDK 中的额正式成员,但可免费下载使用功能强大,支持插件,无限可能。
有了插件扩展的支持,VisualVM 可以做到
显示虚拟机进程以及进程的配置,环境信息(jps,jinfo)
监控应用程序的处理器,垃圾收集,堆,方法区以及线程的信息(jstat,jstack)
dump 以及分析堆转储快照(jmap,jhat)
方法级别的程序性能分析,找出被调用最多,运行时间最长的方法。
离线程序快照:收集程序的运行时配置,线程 dump,内存 dump 等信息建立一个快照,可以将快照发送给开发者进行 Bug 反馈。
其它插件带来无限可能性
VisualGC 插件
使用举例
1. 生成,浏览 堆转储快照
2.分析程序性能
3. 线程死锁分析,线程 dump
4.BTrace 动态日志跟踪
JMC:可持续在线(不会玩啊)
从 JDK11开始被移出 JDK,生产环境付费使用需要单独去 Oracle 官网下载
支持插件
功能面板
JFR(Java Filght Recorder)(飞行记录仪)
一般信息:关于虚拟机,操作系统和记录的一般信息
内存:关于内存管理和垃圾收集的信息
代码:关于方法,异常错误,编译和类加载的信息
线程:关于应用程序中,线程和锁的信息
I/O:关于问价和套接字输入,输出信息
系统:关于正在运行 java 虚拟机的系统,进程和环境变量的信息
事件:关于记录中事件类型的信息,可以根据线程或者堆栈跟踪,按照日志或者图形的格式查看。
JFR 工作流程:
垃圾收集器和 GC 算法是一对多的关系。
JVM 和 垃圾收集器是一对多的关系
虚拟机类加载机制
运行期类加载
在 java 语言里,类型的加载,连接和初始化过程都是在程序运行期完成的。
类加载过程
双亲委派模型
对象头
Integer 的占用
JAVA 内存模型(JMM)与线程
java 内存模型(图)
为什么要定义 java 内存模型?
《java 虚拟机规范中》定义的 java 内存模型是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让 java 在各种平台上都能达到一致的内存访问效果。
主内存与工作内存
主内存
工作内存
每条线程都有自己的工作内存,线程工作内存保存了该线程使用的变量副本。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读取主内存的数据。不同线程之间无法访问对方工作内存中的变量。(图)
java 内存模型的三个特征:
原子性(Automicity)
基本数据类型的访问,读写都是原子的;long double 除外。更大范围的原子操作,提供了:lock 和 unlock 指令,虽然没有开方给用户,但是提供了更改层次的字节码指令: monitorenter 和 monitorexit,反映到 java 代码中就是 同步块(synchronized)关键字。
可见性(Visibility)
可见性:当一个线程修改了共享变量时,其它线程能够立即得知这个修改。
volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新
synchroized 也可以保证可见性
有序性(Ordering)
有序性指的是:指令重排序现象,和 工作内存与主内存同步延迟现象(图)
happens-before原则
倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下
啥是 happens-before 原则?
简单来说就是,符合这个原则的代码,可以保证多线程之间内存可见性
线程
volatile
线程可见性
被 volatile 修饰的变量,每次使用都要强制刷新最新变量到主内存,能够保证此变量对所有线程都是一样的
volatile 典型使用场景
boolean 类型判断(图)
用 java 写一个单例:(图)
禁止指令重排序
线程安全与锁优化
如何保证并发程序的正确性和线程安全?
线程安全
发生线程不安全的两个特点:
多线程
共享数据
Java 语言下的线程安全
如何实现线程安全?(阻塞同步,非阻塞同步,无同步)
1.阻塞同步-- 悲观锁
同步:保证共享数据在同一时刻只被一个线程使用阻塞:实现同步的一种手段。
synchronized(同步块)
特点:
1.被 synchronized 修饰的同步块对同一条线程是可重入的。
1.被 synchronized 修饰的同步块在持有锁的情况下,会无条件的阻止后面其它线程进入。
3.非公平锁
4.synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁
Lock(锁接口)
ReentrantLock
特点
1.等待可中断
2.支持公平锁(先来先获得)
ReentrantLock 默认是非公平的,公平的会降低性能
3.可绑定多个条件
4.ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁
synchronized 和 ReentrantLock 性能、选择
synchronized 在虚拟机层面已经进行了很多优化,性能和 ReentrantLock 相差无几所以,性能不再是选择二者的决定性因素
那该怎样选择呢?
阻塞同步缺点:
阻塞同步面临的问题进行线程阻塞和唤醒需要带来性能开销;java 的线程是和内核线程 1:1 的,挂起线程和回复线程都需要转入内核态完成,开销比较大。
悲观的并发策略:总是认为只要不去做正确的同步措施(如加锁)就会出现线程安全。无论共享数据是否出现竞争,都会加锁。
2.非阻塞同步(无锁编程)-- 乐观锁(CAS)
乐观并发需要 硬件指令集的支持。
比较并交换(CAS)
对于内存中的某一个值V,提供一个旧值A和一个新值B。如果提供的旧值V和A相等就把B写入V。这个过程是原子性的。
CAS 使用示例
AutomicIntegeter race = new AutomicIntegeter(0);race.incrementAndGet();incrementAndGet() 方法在一个无限循环中,不断尝试讲一个比当前值大 1 的新值赋给自己,如果失败了,那就说明在执行 CAS 问题时,旧值已经改变,于是再次循环,知道成功为止。
CAS 特点
需要注意的是,CAS 无法涵盖所有阻塞同步的使用场景。
CAS 存在一个 “ABA”漏洞:读取完 V 值为 A 后,还未进行比较,另一个线程把这个 V 值先改为 B,再改为 A,那么 CAS 就会误认为这个值没有改变过。
大部分情况下,ABA 问题并不会影响程序并发的正确定,如果确实需要解决 ABA 问题,那就只能采用阻塞同步的方式。可能比原子操作更高效哦
3.无同步方案(保证不存在共享数据)
我们需要明白一点:同步和线程安全没有必然的联系。同步只是保障存在共享数据并发访问的正确定的手段。
锁优化?(synchronized 锁的升级过程)首先需要明白,java 虚拟机的锁优化是针对 synchronized 来说的
(自适应)自旋锁
为什么需要自旋锁?
挂起线程和恢复线程都要转入到内核态完成,开销比较大。
共享锁的持有状态大都只会持续很短的时间,为了这段时间去挂起、恢复线程并不值得。
自旋锁流程(图)
但是自旋会增加无用的 CPU 消耗,应当指定自旋次数,超过一定次数后就不再自旋改为传统阻塞方式。
自适应自旋(图)
锁消除(无锁)
定义
如果判断一段代码,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁也就无需再进行。
锁粗化
我们总是推荐同步代码块要尽量的小,及时存在锁竞争,等待锁的线程也能尽快拿到锁。
但是,如果一系列的连续操作都对同一个对象反复进行加锁和解锁,频繁的进行互斥同步操作会导致不必要的性能消耗
举例:StringBuilder 的 append()操作
轻量级锁:在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量
依据的一条经验:“对于绝大部分锁,在整个同步周期内,都是不存在竞争的”
为什么要有轻量级锁?
它的设计初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
偏向锁:在无竞争的情况下把整个同步都消除掉,连 CAS 都不用做了。
他的意思是这个锁会偏向于第一个获取它的线程,如果在接下来的执行过程,该锁一直没雨被其它线程获取,则持有偏向锁的线程无需同步。
一旦出现另一个线程尝试获取这个锁,那么偏向锁模式会宣告结束,上升为轻量级锁
synchronized 锁升级流程
目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
无锁:
锁消除。
即使已经加了 sychronized 的代码,jvm判定它不会有线程安全,就不会给它加锁。
偏向锁:
第一个获取他的线程,持有的就是偏向锁,一旦有第二个线程来,偏向锁立刻失效,转入轻量级锁。
轻量级锁
在出现竞争的情况下,不挂起线程,使用CAS尝试获取同步资源。
重量级锁
线程挂起,转入内核态,使用操作系统互斥量。
图
0 条评论
回复 删除
下一页