java JVM知识
2020-04-07 11:56:23 1 举报
AI智能生成
Java JVM知识总结
作者其他创作
大纲/内容
程序编译与优化
逃逸分析
分析对象的动态作用域
不逃逸
方法逃逸
线程逃逸
1.栈上分配
不在堆中分配对象内存,降低gc的发生频率
支持方法逃逸和不逃逸
2.标量替换
若一个数据已经无法再分解为更小的数据表示,则称为标量,根据访问情况,将其用到的成员变量恢复为初始变量,其过程称为标量替换
作为栈上分配的特例,实现较为简单
仅支持不逃逸
3.线程同步消除
4.公共子表达式消除
5.数组边界检查消除
高效并发
JAVA内存模型
主内存与工作内存
工作内存为线程私有内存,线程自己使用的内存
主内存为jvm管理的内存
内存间的8大交互步骤
1.lock(锁定),作用于主内存
2.unlock(解锁)作用与主内存
3.read(读取)作用于主内存,将主内存中的变量传输到工作内存
4.load(载入)作用域工作内存,把read操作中的变量加载到工作内存中的变量副本中
5.use(使用)作用于工作内存的变量,将工作内存中的变量传递给执行引擎
6.assign(赋值)作用于工作内存 将执行引擎获取到的值,赋予工作内存中的变量
7.store(存储)将工作内存中的变量传递到主内存中,以便于write使用
8.write(写入):作用于主内存,将store操作从工作内存中的变量放入到主内存变量中
内存间操作交互的规则限制
1.不允许read和load,store和write操作单独出现,即不允许一个变量从主内存读取了变量当工作内存不接受和工作内存发起来回写,但是主内存不接受
不允许一个线程无原因的丢弃它最近的assign操作,即变量在工作内存中改变了,必须同步到主内存中
一个新的变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量也即是对一个变量实施use,store之前必须先执行assign和load操作
一个变量在同一时刻只能由一个线程对其进行lock操作,但可以多次被一个线程lock操作,但是只有执行相同次数的unlock操作,变量才被解锁
如果对一个变量进行lock操作,那么将清空工作内存中此变量的值,在执行引擎使用这个变量前,必须重新执行load或者assign操作来重新初始化该变量
不允许一个线程无原因的(即没有assign操作)把数据从工作内存同步到主内存中
对一个变量执行unlock操作前,必须将变量同步到主内存(store,write操作)中
如果事先没有执行lock操作,不能对变量执行unlock操作
volatile变量的特殊规则
提供最轻量级的同步机制
两项特性
保证此变量对所有线程可见
其含义时是,当一个线程修改了该变量,新值是对其他线程可知的,普通变量的值需要通过主内存中来完成
禁止指令重排序优化
long double类型变量特殊规定
允许虚拟机执行选择是否保证64位数据类型的load,store,read,write的原子性(64位机器没有这个问题)
原子性
java内存模型保证read,load、assign,use,store,write的原子性,java提供了lock,unlock操作满足大范围的原子性
可见性
就是指当一个线程修改了共享变量,其他线程是否立刻得知
volatile变量
final 变量,构造器没有把this引用没有传递出去
有序性
本线程观察是有序的,在另外的线程观察是无序的
先行发生原则
程序次序原则
在线程内,按照程序书写前后顺序原则
管程锁定原则
unlock操作先行发生于lock操作
vodatile变量规则
volatile变量的写操作先行发生于后面对这个变量的读操作
线程启动原则
thread的start方法先行于此线程任何操作前
线程终止原则
线程中所有的操作先行发生于此线程的终止检测
线程中断原则
对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
对象终结原则
对象初始化完成(构造函数的执行结束)先行于finalize方法的开始
传递性原则
A操作先于B,B操作先于C,则A先于C
线程安全与锁优化
线程比例关系
java线程原生于操作系统线程,其比例为1:1的关系
线程调度方式
协同式:线程之间的切换由线程自己决定
抢占式:线程之间的切换由操作系统决定
线程状态
新建
运行
无期限等待
限期等待
阻塞
结束
协程
轻量,无需用户态和核心态之间转换,其数目基本无限制,调度由程序自己决定
java的 loom项目就是协程的实现
锁优化
自旋锁
忙循环等待其他线程释放锁
锁消除
代码要求同步,但是检测到不会发生共享竞争的锁进行消除
锁粗化
如果一系列操作都会对同一对象加锁解锁,这时就会对加锁同步范围扩大到整个操作范围内
轻量级锁
在代码即将进入同步代码块的时候,虚拟机先在栈帧中获取复制该锁信息,并使用CAS机制将该对象锁指定为该线程,如果锁定成功就获取了该对象的锁,如果失败,就膨胀为重量级锁,就进入阻塞队列之中,其原则是大部分锁不存在竞争,如果存在竞争,因为由CAS操作,会比重量级锁更慢
偏向锁
会偏向于第一个获取这个锁的线程,如果在接下来的过程中,没有锁竞争,则偏向锁的线程永远不需要同步,一旦出现另外的线程去获取锁,偏向模式就会取消。会提高有同步但无竞争的程序性能,如果大多数锁总数被多个线程竞争,则该模式就是多余的
1.自动内存管理
Java内存区域与内存溢出
运行时数据区域
1.程序计数器(pc program Counter Register)
每个线程含有独立的pc
指示执行下一条字节码指令
2.虚拟机栈
抛出异常
1. 当栈深度超出虚拟机栈规定的深度时,抛出StackOverflowError
2.当栈扩展时,无法申请到足够的内存时,抛出 OutOfMemoryError
3.堆
1.存放实例对象
2.垃圾收集器管理的主要内存区域
3内存无法扩展时,抛出outofmemoryerror
4.方法区
1存储虚拟机加载的类型信息,常量,静态变量,即时编译器产生的代码缓存等
2各线程公用的内存区域
3.该区域在jdk8之前称为永久代,之后成为元数据空间(meta space),元数据空间没有限制内存,直接使用的时jvm管理之外的内存
5.运行时常量池
1.存放编译期间生成的各种字面量和符号引用
2.具备动态性,程序运行期间,也可将新的常量放入到池中,如String.intern()方法。
6.会因内存大小限制,会抛出outofmemoryerror
5.直接内存
1.不属于虚拟机管理的内存,也会被java程序经常使用(java的NIO)
2.会因本机内存大小限制,会抛出outofmemoryerror
垃圾回收器于内存分配策略
1.对象死亡判断
1.引用计数法
1.对象中添加引用计数器,如有引用,将引用加一,引用失效,引用计数器减一
2.较难处理循环引用的问题
2.可达性分析
1.从gcroot节点开始,根据引用关系搜索引用链条,如果对象没有从gcroot没有任何引用链,那说明对象不可达,即对象不可以用,代表对象已死
gcroot
1.虚拟机栈引用对象(栈帧中引用的对象,如参数,局部变量,临时变量)
2.方法区中静态属性,常量引用对象
3.本地方法栈引用对象
4.虚拟机内部的引用
5.同步器(synchronized)持有的对象
6.反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存
7.g1垃圾回收器中的记忆集(记录跨代引用的对象)
3.对象引用的类别
1.强引用:只要强引用关系存在,垃圾回收器就不会回收被引用的对象
2.软引用:被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围进行第二次回收。
3.弱引用:用来描述非必须对象,被弱引用引用的对象只能生存到下次垃圾回收器发生为止。也即是讲,弱引用对象只能生存在前后垃圾回收器的期间
4.虚引用:唯一目的就是在对象被回收时,收到一个系统通知
4.finalize方法
如果对象进行可达性分析后发现没有与gc root相连的对象,那他将会被第一次标记随后进行筛选,条件是此对象是否有必要执行finalize方法,假如对象没有覆盖finalize方法,或者已经被虚拟机调用过了,那么虚拟机将这两种情况都视为没有必要执行,如果对象被判断有必要执行finalize方法,将会把对象放到F-queue队列之中,去执行finnalize方法,但不承诺执行结束,是因为对象的finalize方法有可能会发生执行缓慢或者其他意外情况(死循环或者内存崩溃的情况)finalize中可以做到拯救自己(重新与引用链的对象重新关联上即可),而且只能拯救一次(因为finalize方法只能执行一次)
5.回收方法区
回收方法区常量
该常量没有任何引用
回收方法区类型
1.该类所有实例被回收(该类及其派生类)
2.加载该类的类加载器被回收了
3.该类的class对象不含有任何引用
该区域回收价值不大,一般不做过多讨论
2.垃圾回收器算法
分代收集理论
绝大多数对象朝生夕灭
熬过多次垃圾回收过程的对象就越难消亡
跨代引用相对于同代引用占少数
1.标记-清除算法
标记所有要清理的对象,标记完成后,统一回收所有标记的对象
缺点
1.效率不够稳定,如有大量对象标记清除,执行效率降低
2.会产生内存碎片问题
2.标记-复制算法
标记存活对象,然后复制存活对象
优点
不会产生内存碎片
缺点
内存空间使用效率降低
存活对象较多时,效率较低
较多用于新生代内存区域
3.标记-整理算法
标记不用回收的对象,而后移动标记对象到内存区域另一端
优点
没有内存碎片
内存使用效率高
缺点
降低系统吞吐量
产生stop world现象
较多用于老年代回收
hotspot算法细节
1.根节点枚举
枚举根节点时必须要停顿
2.安全点和安全区域
安全点:用户程序执行时并非在代码指令流任何时刻都能停顿下来进行垃圾回收,而是强制到安全点的时候,才能够暂停
安全区域:能够确保在某一段代码片段之中,引用关系不会发生变化,因此在这一区域内进行垃圾回收都是安全的
3.记忆集和卡表
为了解决对象跨代对象引用的问题,用于避免整个老年代加进gcroot扫描范围
卡表是记忆集的一种实现方式
写屏障:维护卡表的动作放到每一个赋值操作中,类似于AOP操作,为了维护在垃圾回收过程中,卡表的变化,进而导致引用链的变化
伪共享:
并发的修改在一个缓存行中的多个独立变量,看起来是并发执行的,但实际在CPU处理的时候,是串行执行的,并发的性能大打折扣。
卡表在高并发情况下,面临伪共享的问题,现代处理器缓存系统都是以缓存行为单位的,当多线程修改互相独立的变量时,如果这些变量恰好在一个缓存行内,就会彼此影响(写回,无效化或者同步)而导致性能下降,这就是伪共享问题
当卡表中的元素与其他线程中的变量处在同一缓存行时,就会导致更新卡表时写入缓存行时导致性能下降。为了避免伪共享问题,先检查卡表元素标记,未被标记时才将其标记为变脏。jdk7新增参数-XX:UseCondCardMark来判断卡表更新的条件判断
解决方案
手动缓存行填充
使用Contended注解,jvm进行缓存行填充(需开启)-XX:-RestrictContended
4.并发的可达性分析
三色标记法
白色:未被垃圾回收器访问的对象或者分析结束后仍旧为白色的对像,即不可达对象
黑色:垃圾回收器已经访问过了,且对象的引用都已经扫描了,表示安全存活(黑色对象不可能直接指向白色对象)
灰色:被来垃圾回收器访问过,但是对象上至少还有一个引用未被扫描
并发扫描对象消失问题
当且仅当以下两个条件满足时,产生对象消失问题:
1.插入了一条到多条的黑色对象到白色对象的引用
2.删除了全部灰色对象到该白色对象的全部引用
解决
增量更新
破快第一个条件:记录黑色到白色对象之间的引用关系变更,后面在以黑色对象为根点进行可达性分析
原始快照
破坏第二个条件:当灰色对象删除白色引用时,记录下来操作,后面根据灰色对象为根进行可达性分析
3.经典垃圾回收器
新生代垃圾回收器
1.serial收集器
客户端模式下的默认回收器
2.parNew 收集器
serial收集器的多线程版本
新生代收集器
3.parallel Scavenge收集器
新生代垃圾回收器
基于标记复制算法
老年代回收器
Serial Old 收集器
Parallel Old 收集器
CMS收集器
目标:最短回收停顿时间
阶段
1.初始标记
stop the world
仅仅标记gc root节点
2.并发标记
使用增量更新记录引用对象变更
3.重新标记
stop the world
处理并发标记阶段的引用变更
4.并发清除
缺点
1.对处理器资源敏感(单核模式,多核模式性能不错)
2.无法处理浮动垃圾
在并发标记和并发清理过程中,由于用户线程仍然执行,所以会产生垃圾
3.大量碎片内存
使用了标记-清除算法
新生代老年代共用
g1
将内存动态分为一系列动态集合,新生代和老年代不再固定,以region为回收单位,建立可预测的停顿模型(根据region内的统计信息),优先回收价值较高的对象,这也是g1名称的由来
阶段
1.初始标记
枚举根节点
stop the world
2.并发标记
使用原始快照模式记录引用变更
3.最终标记
stop the world
处理并发标记阶段的引用变更
4.筛选回收
更新region统计信息
使用标记复制算法
stop the world
优点:
不会产生内存碎片
大内存模式有优势
缺点
需要占用更多的内存维护记忆集
低延迟收集器
shenandoah收集器
特点
1.支持并发整理算法
2.默认不使用分代收集
3.不维护记忆集,改为维护连接矩阵(链接矩阵中存储着region间的引用关系)
阶段
1.初始标记
标记与gcroot相关联的对象
stop the world
2.并发标记
与用户线程一起标记出全部可达对象
3.最终标记
使用快照更新上一步骤产生的引用变更
评估回收价值最高的region,将这些region组成为回收集
4.并发清理
清理整个区域没有一个存活对象的区域
5.并发回收
将回收集中的存活对象,复制到未被使用的region中
6.初始引用更新
确保所有并发回收阶段的收集器已经完成分配给他的移动对象任务
stop the world
7.并发引用更新
进行引用更新操作(将原有引用更新到复制过后的对象上)
8.最终引用更新
修正gc root中的引用更新
stop the world
9.并发清理
经过并发回收和引用更新后,回收region内存空间
转发指针
不需要时,指向自己,内存回收时,指向被复制的对象。
zgc收集器
zgc的Page(与g1中的region类似)具有动态性,动态创建,销毁,修改容量,分为大,中,小三种page(与g1中的region类似)
并发算法的实现:
1.染色指针技术
直接将少量额外信息存储在指针上的技术(64位指针位置未完全使用)
优势
1.能够使得某个region对象被移走,该region就能够立刻被释放和使用
2.大幅减少垃圾手机过程中内存屏障的使用数量(尤其写屏障,zgc暂时不支持分代回收)
3.可以作为可扩展的存储结构,记录相关数据,进一步提高性能
需要操作系统支持
1.虚拟内存映射技术
阶段
1.并发标记
stop the world
标记的是指针,而不是对象
2.并发预备重分配
根据特定的查询条件统计得出本次收集过程中需要清理哪些region,将这些region组成重分配集
每次回收扫描所有region,用范围更大的扫描省去g1中记忆集的维护成本
重分配集中的对象会复制到其他region中,而后整个region释放掉
3.并发重分配
核心阶段,把重分配集的存活对象复制到新的region中
并为重分配集中的每个region维护转发表,记录从旧对象到新对象之间的转向关系
如果用户线程访问了重分配集中的对象,都会转发到新的对象之中并修正该应用指向的对象
一旦重分配集合中的某个region对象复制完毕,则整个region区域就可以释放掉了(转发表不能释放)
4.并发重映射
修正整个堆中的重分配集中的旧对象的所有引用,当所有引用被修正,转发表就可以释放掉了
由于未分代的原因,会产生大量的浮动垃圾(回收速度小于分配速度,内存会被消耗殆尽)
内存分配策略
1.对象优先分配在新生代eden区
2.大对象直接分配在老年代
3.长期存活对象进入老年代(默认经过16次回收进入老年代)
4.空间分配担保
根据-XX:HandlePromotionFailure参数判断
1.在进行新生代内存回收时,判断老年代最大可用连续空间是否大于新生代所有存活对象
2.如果满足,则新生代回收操作是安全的
3.否则有可能是失败的(假如说新生代所有对象存活,需要老年代进行分配担保,把新生代survivor无法容纳的对象直接送入老年代)
虚拟机性能监控,故障处理工具
非可视化工具
jstack:java堆栈分析工具,生成当前时刻的线程快照
jmap
java内存映射工具,生成堆转存储快照,即dump文件
也可以使用-XX:+HeapDumpOnOutOfMemoryError参数,在内存溢出时,生成堆转存储快照
也可使用-XX:+HeapDumpOnCtrlBreak参数控制生成堆转存储快照,使用ctrl+break键
也可在linux系统性下,使用kill -3 命令生成
jhat:虚拟机堆转存储快照分析工具,一般不使用,基本上都是把快照复制出来,使用其他分析工具(如eclipse memory analyzer)
jinfo:java配置信息工具 实时查看和调整虚拟机各项参数
jstat:虚拟机统计信息监视工具只有纯文本信息,是排查虚拟机性能问题常用工具
jps: 查找虚拟机进程pid
可视化工具
JHSDB:基于服务型代理实现的进程外调试工具
Jconsole:java监视与管理控制台
VisualVM:多合-故障处理工具
Java Mission Control:可持续在线监控工具
调优案例与实战
线上cpu过高
线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程
通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。
步骤
通过 top命令查看CPU情况,如果CPU比较高,则通过top -Hp 命令查看当前进程的各个线程运行情况,找出CPU过高的线程之后,将其线程id转换为十六进制的表现形式,然后在jstack日志中查看该线程主要在进行的工作。这里又分为两种情况
如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU;
如果通过 top 命令看到CPU并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。
如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过jstack查看堆栈信息,找到阻塞点;
如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出jstack日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程;
如果通过jstack可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。
一般步骤
top:找到占用CPU高的进程PID
jstack PID >> java_stack.log:导出CPU占用高进程的线程栈
top -Hp PID:找出PID的进程占用CPU过高的线程tid。(或使用命令 ps -mp PID -o THREAD,tid,time | sort -rn | less)
printf “%x\n” tid:将需要的线程ID转换为16进制格式。
less java_stack.log:查找转换成为16进制的线程TID,找到对应的线程栈,分析并处理问题。
线上内存过高
步骤
top:找到占用内存(RES列)高的Java进程PID。
jmap -heap PID:查看heap内存使用情况。
jps -lv :查看JVM参数配置。
jstat -gc PID 1000:收集每秒堆的各个区域具体占用大小的gc信息。
jmap -dump:live,format=b,file=heap_dump.hprof PID :导出堆文件。
使用MAT打开堆文件,分析问题
堆外内存泄漏
top:找到占用内存(RES列)较高的Java进程PID。
jstat -gcutil PID 1000 查看每秒各个区域占堆百分比,若gc正常,则分析堆外内存使用情况。
jcmd PID VM.native_memory detail,该命令需要添加JVM参数 -XX:NativeMemoryTracking=detail,并重启Java进程才能生效,该命令会显示内存使用情况,查看输出结果,总的committed的内存是否小于物理内存(RES),因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。
pmap -x PID | sort -rn -k 3:查看内存分布,是否有地址空间不在jcmd命令所给出的地址空间中。
用工具定位堆外内存,如gperftools、gdb、strace等。
虚拟机运行子系统
类加载机制
时机
1.new对象
2.读取类的静态字段
3.调用类静态方法
4.使用java反射调用对象
阶段
1.加载
各种加载渠道(网络,文件系统等),并在加载后生成Class对象
2.验证
目的:确保文件格式正确和当代码运行时不会危害虚拟机的安全
阶段
文件格式
元数据校验
字节码校验
符号引用校验
3.准备
正式为类中的定义的类变量分配内存和设置类变量的初始值
4.解析
将常量池中的符号引用替换为直接引用
部分
类或接口解析
字段解析
方法和接口方法解析
5.初始化
根据程序代码计划去初始化资源,执行类构造器静态<Client>()方法(java编译器自动生成的),即静态代码块
6.使用
7.卸载
双亲委托模型
三层类加载器
启动类加载器(BootstrapClassLoader)
扩展类加载器(ExtensionClassLoader)
应用程序类加载器(ApplicationClassLoader)
自定义加载器(UserClassLoader,需继承应用程序类加载器),可无
如果类加载器收到类加载请求,首先委托给父类加载器,如父类加载完成,则直接不加载,如父类未加载完成,则自己加载
可以保证类加载的唯一性
Java模块化系统
加载器
启动类加载器(BootstrapClassLoader)
平台类加载器(PlatformClassLoader)
应用程序类加载器(ApplicationClassLoader)
自定义加载器(UserClassLoader,需继承应用程序类加载器),可无
当平台及应用程序类加载器收到加载类请求时,在委派给其父类前,要先判断该类是否属于哪个模块,如果属于哪个模块,就优先指派那个模块的类加载器加载。
字节码执行引擎
案例于实战
收藏
收藏
0 条评论
下一页
为你推荐
查看更多