JAVA JVM
2021-07-20 20:38:50 2 举报
AI智能生成
JVM详细结构讲解
作者其他创作
大纲/内容
JIT
即时编译器
just in time
just in time
背景
Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或某个代码块运行特别频繁时,
就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代
码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的就是即时编译器
就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代
码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的就是即时编译器
种类
C1编译器 -client
(客户端编译器)
(客户端编译器)
1、需要收集的数据比较少
2、编译、优化方面都做得比较浅
3、编译生成的代码执行效率没 C2 高
4、编译时耗费的 CPU 资源没 C2 高
2、编译、优化方面都做得比较浅
3、编译生成的代码执行效率没 C2 高
4、编译时耗费的 CPU 资源没 C2 高
C2编译器 -server
(服务端编译器)
(服务端编译器)
1、需要收集的数据比较多(此为触发条件)
2、优化得更彻底(分析后做优化)
3、编译时耗费 CPU 资源
4、执行效率更高
2、优化得更彻底(分析后做优化)
3、编译时耗费 CPU 资源
4、执行效率更高
分层编译技术
背景
由于即时编译器编译本地代码需要占用程序运行时间,编译出优化程度越高的代码,所花时间也越长
为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在 jdk 6 实现了分层编译
原理
解释器、客户端编译器和服务端编译器会同时工作,热点代码可能会被多次编译,
用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量
用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量
应用:系统启动初期的字节码优化采用C1,热机后采用C2编译器
阿里案例
热机切冷机故障
冷机:刚启动
热机:运行了一段时间
问题:并发性能快到瓶颈时,扩充了几台机器,然后把流量切过去,结果发现冷机崩了
原因:热机已经触发了C2编译器,生成了更优的字节码,承受的并发能力比冷机大很多
解决:先切部分流量过去,等触发C2编译器后,再把大流量切过去
触发条件
热点代码
多次执行的代码块
客户端模式下是1500次
服务端模式下是10000次
多次执行的方法
热点探测方式
基于采样的热点探测
基于计数器的热点探测
热度衰减
Mac为什么不支持JIT?
因为JIT需要申请一块可读可写可执行的内存区域,而Mac不允许申请可执行的内存
优化
方法内联
逃逸分析
概念
逃逸分析是一种确定指针动态范围的方法
新对象仅创建线程可见,外部线程不可见,否则就是发生了逃逸
发生时机
Java的逃逸分析只发生在JIT的即时编译中,因为收集到足够的运行数据JVM可以更好的判断对象是否发生了逃逸
Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别
JVM判断新创建的对象发生逃逸的依据
对象被赋值给堆中对象的字段(全局变量)或类的静态变量
对象被传进了不确定的代码中去运行
优化手段
栈上分配:如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以
分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率
分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率
同步锁消除:如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步
分离对象或标量替换:如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的
部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
基于逃逸分析的对象分配
先判断该对象是否发生逃逸,如果没有发生就在栈上分配
降低垃圾回收频率,提升性能
否则在堆上分配,再判断是否为大对象,大对象直接进入老年代
JVM参数设置:-XX:PretenureSizeThreshold
新生代默认开启线程本地分配缓存-TLAB(占Eden区的1%)
确保多线程分配对象的线程安全
如果TLAB放不下,又不是大对象,或者禁用了TLAB,则会使用CAS乐观锁在Eden区进行内存分配
公共子表达式消除
数组边界检查消除
JVM 调优
性能监控
常用工具/命令
jps:显示Java进程状况
-l:输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径
-v:查看虚拟机启动时显式指定的JVM参数列表
未被显示指定的可通过 jinfo -flag 查看
jinfo:实时查看和
调整JVM配置参数
调整JVM配置参数
查看JVM参数
jinfo PID
显示JVM详细参数,末尾显示JVM默认启动参数
jinfo -flag <name> PID
jinfo -flag MaxMetaspaceSize 18348
调整JVM参数
布尔类型的JVM参数
jinfo -flag [+|-]<name> PID
数字/字符串类型的JVM参数
jinfo -flag <name>=<value> PID
jstat:监视虚拟
机各种运行状态
机各种运行状态
-class:监视类加载、卸载数量、总空间以及类装载所耗费的时间
-gc:监视内存中各分代区域的使用情况
-gcutil:监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
使用示例
jstat -gc 2764 250 20
每250毫秒查询一次进程2764垃圾收集状况,一共查询20次
jstat -gcutil 2764
查看2764进程各分区占比以及Minor GC和Full GC发生的次数和耗时
-gcnew:监视新生代的垃圾收集情况
-gcold:监视老年代的垃圾收集情况
jmap:Java内存映像工具
用于查看堆中对象的统计信息 & 生成堆转储快照文件
扩展:其他方式生成堆转储快照文件
应用
测试环境压测时导出 dump 文件进行分析,生产环境利用高可用的备份服务器进行操作
设置参数:-XX:+HeapDumpOnOutOfMemoryError
JVM停顿时自动生成 dump 文件
在线定位
阿里巴巴JVM调优工具:ArtThas
jmap -histo PID
在线查看运行的程序中有多少类,每个类产生了多少个对象,共占用多少内存
jmap -histo PID|head 20:列出前20个类的对象个数及占用空间
jmap -histo:live PID:显示堆中对象统计信息,包括类、实例数量、合计容量
jmap -dump
生成Java堆转储快照文件,便于下载下来进行离线分析(借助分析工具)
jmap -dump:format=b,file=20210224.hprof PID
常见分析工具
MAT
JProfiler:好用但收费
jhat:堆转储分析工具(jdk自带)
jvisualvm:文件->装入hprof文件
jstack:Java堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,目的是定位线程
出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等
出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等
jstack -l PID
查看线程堆栈
jstack PID | grep tid -A100
扩展
getAllStackTraces()
可视化故障处理工具
JConsole
是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具
启动JConsole后,会自动搜索本机运行的所有JVM进程,相当于可视化的jps命令
“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存的变化趋势
“线程”页签的功能相当于可视化的jstack命令,用于分析线程停顿的原因,比如死循环,锁等待
Java VisualVM
jvisualvm是功能最强大的运行监视和故障处理程序之一,曾经是Oracle官方主力发展的虚拟机故障处理工具
它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)
优势:对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中
jps、jinfo、jstat、jstack、jmap、jhat等多种功能于一体
在Profiler页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析(生产不用)
BTrace动态日志跟踪插件
阿里巴巴JVM调优工具:ArtThas
下载解压,启动:java -jar arthas-boot.jar,输入编号选择进程并回车即可看到启动日志
常用命令
dashboard:展示当前进程的信息,按ctrl+c可以中断执行
thread
显示所有线程信息以及对应的CPU占用情况
thread tid:可以查看指定线程在做什么
thread -b:检测死锁
jad 类的全路径名
显示该类对应的类加载器和location
反编译
查看代码版本
查看动态代理对象
redifine /root/.../xxx.class
使线上修改的代码立即生效(慎用)
原理:classloader重新加载
trace 全限定类名 方法名
分析方法的运行时间与运行细节
方法调用追踪
JMC
Java Mission Control:可持续在线的监控工具,可用于生产环境
JHSDB
是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具
GC日志分析工具
GCEasy
GCViewer
故障定位
CPU
CPU 占用过高
先用 top 显示进程列表,找到占用率最高的 PID
此时也可挂上阿里的Arthas查看占用高的线程
再使用 top -Hp pid 找到占用最高的线程 tid
如果是业务线程,检查死循环或者大量计算的代码
如果是收集线程,检查是否频繁 FGC
如果 FGC 的回收正常,说明此时只是压力大
如果 FGC 每次回收很少,说明有内存泄漏
printf '%x\n' tid 将线程ID转换为16进制 (小写)
最后使用 jstack pid | grep tid -A60
频繁上下文切换
vmstat 2 3
每隔2秒打印一次CPU信息,共打印3次
cs:表示上下文切换次数(context switch)
pidstat -w pid
针对 pid 进行上下文切换监控
cswch/s:每秒自愿上下文切换(进程无法获取到可供执行的资源从而自愿发生上下文切换)
nvcswch/s:每秒非自愿上下文切换(进程时间片用完、被高优先级进程抢走、系统中断等发生的非自愿的场景)
内存
内存溢出
栈内存溢出
StackOverflowError
StackOverflowError
原因:线程请求栈的深度超过当前Java虚拟机栈的最大深度(默认大小是1M,1024KB)
代码举例
解决:检测是否有无穷递归、死循环,如果不是bug,可以通过调整虚拟机栈的大小来解决(-Xss2m)
堆内存溢出
OutOfMemory
OutOfMemory
原因:堆内存被用光,或者内存泄漏堆积导致
OOM
对象太多(Java heap space)
修复内存泄露,或使用 -Xmx 增加堆内存大小
代码举例
GC回收超时(GC overhead limit exceeded)
使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
本地内存不足(Direct buffer memory)
打印的堆栈跟踪信息,使用操作系统本地工具进行诊断
线程太多(unable to create new native thread)
减少虚拟机栈的大小或减少堆内存占用比例
元空间内存不足(Metaspace)
使用-XX: MaxMetaSpaceSize 增加 metaspace 大小
内存泄漏
程序在申请内存后,无法释放已申请的内存空间
导致频繁 GC
jstat -gc pid 1000
每秒打印一次GC详细信息(C:容量|U:已使用)(E:Eden|O:Old|M:MetaSpace)
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
jstat -gcutil 1000 3
查看最近3次GC空间占比
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
使用 jmap 生成 堆转储 快照文件 jmap -histo:live pid
通过 MAT 查看 Histogram 图即可找出是哪块代码
查看线程信息
top -H -p pid
左上角显示Threads: 28 total,下面显示详细的线程信息
线程数
pstree -p 8379 | wc -l
ls /proc/8379/task | wc -l
ps hH p 8379 | wc -l
调优阶段
上线前:根据需求(支撑多少QPS)进行JVM规划和预估调优(几台机器,多大内存,堆内存划分)
上线初期:根据日志优化JVM运行环境(解决慢、卡顿问题)
上线后期:解决JVM运行过程中出现的各种问题(OOM,逃逸分析,频繁 full gc)
调优指标
停顿时间
垃圾收集器做垃圾回收中断应用执行的时间
-XX:MaxGCPauseMillis:停顿时间(建议)
GC会尝试各种手段达到这个时间,比如减小年轻代
GC时间比例
GC时间占用程序运行时间的百分比:1/(1+n)
-XX:GCTimeRatio = n(默认值99)即 1% 时间用于垃圾收集
例如将 n=19 则垃圾收集时间为1/(1+19) 即 5% 时间用于垃圾收集
吞吐量
吞吐量=应用程序时间/(应用程序时间+垃圾收集时间) 即 1-1/(1+n)
调优参数
标准参数,-开头
通过 java 命令查看
常用参数
java -version
java -help
非标准参数
-X开头
通过 java -X more 查看
常用参数
-Xms200m -Xmx200m:最小堆和最大堆
-Xmn60m:新生代
-Xss1m:栈空间大小
扩展:为什么最小堆和最大堆设置一样大?
防止内存震荡(否则JVM会将堆内存进行扩容和缩容)
扩展和回缩需要大量的计算,影响程序的执行效率
-XX开头
通过 java -XX:+PrintFlagsFinal -version | less 命令查看
常用参数
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
调优案例
日均百万交易系统
JVM堆栈大小设置?
JVM 调优,让其几乎不发生 Full GC?
线程死锁
四个必要条件
互斥、请求和保持、不可剥夺、环路等待
预防死锁
破坏四个必要条件中任意一个都可以
指定加锁顺序、设置超时时间
银行家算法
保证系统动态分配资源后不进入不安全状态
死锁检测工具
Jstack和Jconsole
手写死锁(伪代码)
举例
哲学家用筷子吃饭,每人抢到一根
常见问题
线上进程 kill 不掉怎么办?
先用 ps aux 看看 stat 栏,如果是Z,那就是zombie僵尸进程
ps -ef | grep 僵尸进程ID,可以找到父进程ID,kill 父进程即可
磁盘空间快满了怎么办?
先用 df -h 看看磁盘使用情况
检查项目部署的tomcat或者nginx的日志,按天分割,定期删除(crontab)
如果不是日志问题,那就 find / -size+100M | xargs ls -lh,找出大于100M的文件
如果不是大文件导致的,那就 du -h > fs_du.log 查看各目录占用磁盘大小,是否有大量小文件
this指针是何时赋值的?
谈谈你对Java数组的理解
动态数据类型
数组长度的计算方式与指针压缩的关系
class文件在内存中是如何存储的?
类加载器把 class 文件加载到内存生成 class content
把 class content 解析成 InstanceKlass 放在方法区
同时生成一份镜像类 InstanceMirrorKlass 存在堆区
同时生成一份镜像类 InstanceMirrorKlass 存在堆区
Class 对象是 InstanceMirrorKlass 类的实例
对象头中的 Klass Point 指向 InstanceKlass
栈帧中的动态链接中存储的是什么?还有其他思路吗?
oop 最大支持的堆大小是多少?如何扩容?
字节
问题
问题
Object obj = new Object();
这个对象占多少字节?
这个对象占多少字节?
占用16字节(B)jdk6 以后默认开启 指针压缩(-XX:+/-UseCompressedOops)
开启:MarkWord(8B)+KlassPoint(4B)+数组长度0+实例数据0+对齐填充4B的0
关闭:MarkWord(8B)+KlassPoint(8B)+数组长度0+实例数据0+对齐填充0
图示
Int[] arr = {0,1,2};
这个数组对象占多少字节?
这个数组对象占多少字节?
开启是32字节:MarkWord(8B)+KlassPoint(4B)+数组长度4B+实例数据12B+对齐填充4B
关闭是40字节:MarkWord(8B)+KlassPoint(8B)+数组长度4B+头部填充4B+实例数据12B+对齐填充4B
图示
依据
HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说任何对象的大小都必须是8字节的整数倍
一般情况下,对象头中的MarkWord占8字节,KlassPoint开启指针压缩占4字节(默认开启),如果对象为数组,则数组长度占4字节,
实例数据根据数据类型做具体判断,如果实例数据是引用类型:开启指针压缩占4字节,不开启占8字节,对齐填充补满8的倍数
实例数据根据数据类型做具体判断,如果实例数据是引用类型:开启指针压缩占4字节,不开启占8字节,对齐填充补满8的倍数
对象
个数
个数
String str = "silver";
在JVM中创建了几个String对象?
在JVM中创建了几个String对象?
创建1个String对象,先在字符串常量池中创建"sliver"对象,然后将该对象的引用赋值给str
String str2 = new String("cool"); 呢?
常量池中如果有"cool"则创建1个String对象,如果没有,则创建2个String对象
1. 类加载机制
(按需加载)
(按需加载)
1.1 加载过程
加载:将二进制 class 文件加载到内存
连接
验证:确保类加载的正确性(文件格式、元数据、字节码、符号引用等)
准备:给类的静态变量分配内存,并赋予默认值
解析:把符号引用转化为直接引用
初始化:为静态变量赋予正确的初始值,也就是实际值,准备阶段赋予的是默认的初始值
使用、卸载
对象探秘
对象的创建
过程
1、当虚拟机遇到一条 new 指令时,先去 常量池 检查对应的类是否被加载过,如果没有,则先进行类的加载
2、在类加载检查通过后,为新对象分配内存(内存分配方式有两种:指针碰撞和空闲列表)
3、然后将分配到的内存进行初始化操作,初始化为零值
4、接着做一些必要的对象设置,比如 设置对象头 里的哈希码、元数据、分代年龄、是否偏向锁等
5、最后执行 init() 方法
内存分配方式
指针碰撞
如果Java堆的内存是规整的,即所有用过的内存放在一边,空闲的内存放在另一边。分配内存时
将位于中间的指针向空闲的内存移动一段与对象大小相等的距离即可
将位于中间的指针向空闲的内存移动一段与对象大小相等的距离即可
空闲列表
如果Java堆的内存不是规整的,虚拟机必须维护一个列表来记录哪些内存是可用的,分配内存时
从列表中找到一块足够大的空间划分给新创建的对象,并更新列表上的记录
从列表中找到一块足够大的空间划分给新创建的对象,并更新列表上的记录
分配内存时还要 考虑并发 问题
CAS同步处理
虚拟机采用 CAS+失败重试,保证更新的原子性
本地线程分配缓冲(TLAB)
对象内存结构
对象头(Header)
Mark Word
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
占8个字节
类型指针
对象所属类的元信息在方法区的内存地址
开启指针压缩:占4个字节
不开启指针压缩:占8个字节
数组长度
若对象是一个Java数组,对象头中还必须有一块记录数组长度的数据
占4个字节
注意数组类型在关闭指针压缩时,会涉及到头部填充
实例数据(Instance Data)
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容
对齐填充(Padding)
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
对象的访问定位
使用句柄
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址
(通过栈上的reference数据来操作堆上的具体对象,reference是一个指向对象的引用)
(通过栈上的reference数据来操作堆上的具体对象,reference是一个指向对象的引用)
直接指针
reference中直接存储对象地址,少一次间接访问的开销
两种方式各有优劣
使用句柄访问的最大好处就是reference中存放的是稳定的句柄地址,在对象
被移动时只会改变句柄中实例数据的指针,而reference本身不需要被修改
被移动时只会改变句柄中实例数据的指针,而reference本身不需要被修改
使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销
HotSpot主要使用这种方式进行对象访问(有例外,考虑Shenandoah收集器)
HotSpot主要使用这种方式进行对象访问(有例外,考虑Shenandoah收集器)
1.3 双亲委派机制
简介
原理
优先委托父类加载器加载,父类加载器都不能加载时才自己加载
作用及优势
安全,防止核心类被随意修改,比如自己写的String类不会被加载,或者自己写了 java.lang.test 类也不会被加载
避免重复加载,父类加载器已经加载过,则子类不需要再次加载
沙箱安全机制
为什么要打破双亲委派?
防止类冲突,做资源隔离(不同的类加载器加载相同的class文件是不同的类)
如何打破双亲委派?
使用用户自定义类加载器就可以打破
打破双亲委派举例?
Tomcat、JDBC、部分热部署框架都打破了双亲委派
其他注意
java中表示两个class对象是否同一个类存在两个必要条件
类的完整类名必须一致。包括包名
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
类加载器的引用
jvm 必须知道一类型是由启动加载器加载的还是由用户类加载器加载的
如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的
四大组成
1.2 类加载器
种类
启动类加载器:Bootstrap Classloader JVM自带的
扩展类加载器:Extension ClassLoader
加载 JAVA_HOME/jre/lib/ext 下的jar文件
概念
加载的API路径
系统类加载器:Application ClassLoader(应用类加载器)
加载 classpath 下的文件
自定义类加载器:必须继承 ClassLoader
负责加载用户自定义路径下的类包
自定义类加载器实现步骤
通过继承抽象类java,lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
在jdk1.2之前,会去继承ClassLoader类并重写loadClass()方法,之后不再建议去重写loadClass()方法,建议把自定义的类加载逻辑写在 findClass() 方法中。
如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其取字节码流的方式,使自定义类加载器更加简洁。
获取集中加载类的方式
作用
将类的字节码文件从JVM外部加载到内存中
确定一个类的唯一性
提供隔离特性,为中间件开发者提供便利(例如Tomcat)
运行时数据区
概念
java虚拟机定义了若干中程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机推出而销毁(红色区)。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁(灰色区)。
灰色的为单独线程私有的,红色的为多个线程共享的。
每个线程:独立包括程序计数器、栈、本地栈
线程共享:堆、堆外内存(永久代或元空间、代码缓存)
程序计数器
(PC寄存器)
(PC寄存器)
当前线程所执行的字节码的行号指示器
实现流程控制 和 记录执行位置
线程私有
运行时数据区中唯一不会出现OOM的区域,没有垃圾回收
如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址
如果正在执行的本地方法,这个计数器值则应为空。(undefined)
虚拟机栈
描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧。虚拟机栈线程私有(一个线程拥有一个栈),生命周期和线程一致
每个方法从调用到执行完成,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
栈运行原理
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
java方法有两中返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的结构
局部变量表
存储方法参数和定义在方法体内的局部变量
基本数据类型
对象引用
returnAddress类型
在使用前,必须进行显示赋值!否则,编译不通过
操作数栈
给变量赋值的操作数做加减运算的临时空间
被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,
并更新PC寄存器中下一条执行的字节码指令
并更新PC寄存器中下一条执行的字节码指令
代码演示
byte short char boolean int 都以int型来保存。
局部变量表中非静态的方法默认带有this变量占据0位
局部变量表中非静态的方法默认带有this变量占据0位
把15放到操作数栈
PC寄存器移动2,15存到局部变量表
把8放到操作数栈
PC寄存器移动5,8存到局部变量表
从局部变量表取出索引为1的放到操作数栈
从局部变量表取出索引为2的放到操作数栈
8和15出栈,由执行引擎解析iadd字节码指令为
机器指令被CPU运算,结果放到栈内
机器指令被CPU运算,结果放到栈内
存放到局部变量表,return结束
栈顶缓存技术
由于栈指令不停的入栈出栈,频繁地执行内存读写,影响速度,
所以HotSpot JVM 的设计者提出了栈顶缓存技术。将栈顶元素
全部缓存在物理CPU的寄存器中,以此降低堆内存的读写次数,
提升执行引擎的执行效率
所以HotSpot JVM 的设计者提出了栈顶缓存技术。将栈顶元素
全部缓存在物理CPU的寄存器中,以此降低堆内存的读写次数,
提升执行引擎的执行效率
寄存器:指令更少,执行速度块
动态链接
把符号引用转换为直接引用
代码示例
编译字节码后看methodB()
第9行指令对应#7符号位
第9行指令对应#7符号位
看Constant pool #7的符号位
对应#8和#31,常量池作用就是
把公共变量或同类型可以有相同
的符号位,否则每个方法都有会
占用内存
Constant pool核心就是提供一些
符号和常量,便于指令的识别
对应#8和#31,常量池作用就是
把公共变量或同类型可以有相同
的符号位,否则每个方法都有会
占用内存
Constant pool核心就是提供一些
符号和常量,便于指令的识别
#31对应#19和#13 也就是方法
methodA:()V,#19其实对应的
就是methodA(),#13对应的是
void,#32对应的是类信息,告
诉我在那个类下
methodA:()V,#19其实对应的
就是methodA(),#13对应的是
void,#32对应的是类信息,告
诉我在那个类下
方法的调用(额外讲解)
引用
动态链接
静态链接
虚方法
虚方法
调用指令
invokevirtual 调用所有虚方法,被final修饰的除外
invokeinterface 调用接口,被final修饰的除外
虚方法表
子级调用或重写上级方法需要找到上级的方法才能执行,回影响性能
,所以把方法都引入到一张表中,用颜色或其他来代替是子类方法,还是
调用父类方法会更快。
,所以把方法都引入到一张表中,用颜色或其他来代替是子类方法,还是
调用父类方法会更快。
非虚方法
调用指令
invokestatic 调用静态方法,解析阶段确定唯一方法版本
invokespecial 调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
动态指令 invokedynamic
静态类型:java是一种静态类型语言,需要准确生命一个变量的类型
动态类型:是根据值的类型判断出变量的类型
方法退出(方法返回地址)
存放调用该方法的PC寄存器的值
字节码异常表:
告诉你从指令4-8有正常处理就到16.
如果有异常发生就会到11行
告诉你从指令4-8有正常处理就到16.
如果有异常发生就会到11行
虚拟机栈不需要垃圾回收器来回收,随着方法的执行,线程结束后正常回收即可
虚拟机栈会
有两种情况
有两种情况
栈空间不可扩展,当前栈深度> JVM规定的栈深度, 会抛出栈溢出错误
StackOverflowError
栈空间可扩展,扩展时无法申请到足够内存,会抛出内存溢出错误
OutOfMemoryError
设置占内存大小
我们可以使用参数 -Xss2m 选线来设置现成的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
相关面试题
举例栈溢出的情况?
StackOverflowError:当栈帧深度不足的时候,通过设置 -Xss可以设置栈大小
OutOfMemoryError:在动态扩容的时候发现内存不足的时候
调整栈大小,就能保证不出现溢出嘛?
不能保证,例如递归,原本可能是5000次,但调大了无非可能涨到7000次还是会溢出
分配的栈内存越大越好吗?
不是,栈越大,能开的线程就越少
垃圾回收是否会涉及到虚拟机栈?
不会的
程序计数器:不存在Error 也不存在GC
虚拟机栈:会存在Error,不存在GC 直接出栈
本地方法栈:无法调用的是C相关的方法,存在Error,不存在GC
堆空间 Heap:存在Error,存在GC
方法去:时间比较长的数据,比如类数据本身,存在Error,存在GC
方法中定义的局部变量是否线程安全的?
本地方法栈
线程私有
本地方法栈与虚拟机栈类似,只不过虚拟机栈是为Java方法提供服务,本地方法栈是为native(c/c++)方法服务
堆 heap
线程共享,但会为线程划分私有缓冲区(TLAB)
jinfo -flag UseTLAB pid 查看JVM是否启动该参数
数组 和 对象 可能永远不会存储在栈上(除了:逃逸分析),
因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
实战
逃逸分析
当一个对象在方法中被定义,对象旨在方法内部使用,则认为没有发生逃逸
该对象在线程的栈中
当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸。例如作为参数传递到其他地方中
sb 引用被返回到供其他方法使用
逃逸分析代码优化
栈上分配
同步省略
代码中hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中
,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
分离对象或标量替换
point 对象就是 聚合量
经过标量替换后可以看到,Point 这个聚合量经过逃逸分析后,发现他并没有逃逸,
就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大的减少堆内
存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换
为栈上分配提供了很好的基础。
就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大的减少堆内
存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换
为栈上分配提供了很好的基础。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
占内存最大的一块。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可
堆空间大小设置
-Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize,默认情况大小=物理电脑内存大小 / 64
-Mxx 用于表示堆区的最大内存,等价于 -XX:MaxHeapSize,默认情况大小=物理电脑内存大小 / 4
查看内存分配情况
jps 获取进程 id
jstat -gc id
-XX:+printGCDetails
一旦堆区中的内存大小超过 -Xmx 所指定的最
大内存时,将会抛出OutOfMemoryError异常
大内存时,将会抛出OutOfMemoryError异常
GC的主要区域
新生代 young sapce(1/3)
Eden : Survivor (from | to)
Eden : s0 : s1 = 8:1:1
老年代 old space(2/3)
配置新生代与老年代在堆结构的占比
默认 -XX:NewRation=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
配置新生代
-Xmn 可以设置新生代的空间大小,大破比例分配
如果 -Xmn 和 -XX:NewRation 都配置了 则以-Xmn为准
配置Survivor
默认是 8:1:1
但实际是 6:1:1,要想 8:1:1,需要显示的使用
-XX:SurvivorRatio=8
-XX:SurvivorRatio=8
对象回收过程
当Eden满了的时候,s0和s1参与的是被动回收
如果s0或s1其中一个被占满,可能会直接到old区
当然Eden里的对象也有可能会直接到old区
如果s0或s1其中一个被占满,可能会直接到old区
当然Eden里的对象也有可能会直接到old区
阈值可以通过 -XX:MaxTenuringThreshold=<N> 进行设置,默认15
GC种类
部分收集 Partial GC
部分收集不是完整收集整个Java堆的垃圾收集,其中又分为
新生代收集(Minor GC/Young GC),只是新生代的垃圾收集
老年代收集(Major GC/Old GC)只是老年代的垃圾收集
目前只有CMS GC 会有单独收集老年代的行为
注意,很多时候Major GC和Full GC混淆使用,
需要具体分辨是老年代会后还是整堆回收
需要具体分辨是老年代会后还是整堆回收
混合收集(Mixed GC)收集整个新生代以及部分老年代的垃
圾收集,目前只有G1 GC会有这种行为
圾收集,目前只有G1 GC会有这种行为
整堆收集 Full GC
收集整个java堆和方法去的垃圾收集
方法区/元空间
(线程共享)
(线程共享)
栈、堆、方法区关系
存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码缓存 等数据
元空间在1.8中不再与堆是连续的物理内存,而是改为使用本地内存(Native memory)
方法区
方法区是规范,永久代/元空间是实现
永久代和元空间的区别?
方法区在jdk1.8以前叫永久代,jdk1.8开始叫元空间
永久代是存放在堆区的,元空间存在于直接内存
为什么要移除永久代?
为了避免发生OOM异常,替换成元空间不占用堆内存
元空间的调优
最小、最大元空间设置成一样大
大小设置为物理内存的 1/32
预留20%-30%的空间
常量池
静态常量池
字节码文件中的常量池(静态描述)
字面量
符号引用量
静态常量池只是一个文件结构,运行时常量池是一块内存区域
运行时常量池
虚拟机会在类加载后把各个静态常量池载入到运行时常量池中,用于存放编译期生成的各种 字面量 和 符号引用
运行时常量池可以在运行期间将符号引用解析为直接引用(地址)
JDK1.8废弃了永久代的概念,转移到 元空间 中
字符串常量池
String Pool
String Pool
字符串常量池可以理解为运行时常量池的一部分,加载时对于静态常量池,如果其中包含字符串则被加载到这里
底层是hashtable
key
通过字符串的内容计算hashValue
String.hashCode
对hashValue进行哈希计算出index
value
存储的是字符串的引用
JDK1.7后字符串常量池中只保存字符串引用,实际存储位置转移到 堆 中
执行引擎
执行字节码的方式
字节码解释器
Java字节码 -> c/c++代码 -> 硬编码(机器码)
模板解释器
Java字节码 -> 硬编码(机器码)
底层原理
1、将new方法的硬编码拿过来
2、申请一块可读可写可执行的内存
3、将硬编码写入这块内存
4、声明一个函数指针指向这块内存
5、通过这个函数指针调用这块内存
2、申请一块可读可写可执行的内存
3、将硬编码写入这块内存
4、声明一个函数指针指向这块内存
5、通过这个函数指针调用这块内存
执行字节码的模式
-Xint:纯字节码解析器运行(interpreted mode)
-Xcomp:纯模板解释器运行(compiled mode)
-Xmixed:默认混合模式(mixed mode)
本地库接口
联系
GC
标记垃圾对象
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是可回收的
当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是可回收的
不能解决循环引用的问题
可达性分析算法
GC Roots
两个栈: Java栈 和 Native 栈中所有引用的对象
方法区:方法区中的常量和静态变量
所有跨代引用的对象
和已知 GCRoots 对象同属一个CardTable 的其他对象
引用类型
强引用:只要强引用还在,垃圾收集器就不会回收
软引用:在内存不足的情况下,发生内存溢出之前回收
应用:特别适合缓存使用
弱引用:非必须对象,只要发生GC就会把它干掉
应用:ThreadLocal对象使用弱引用来防止内存泄露
虚引用:相当于没有引用,被GC干掉会收到通知
背景:GC只能管理JVM内存,不能直接管理直接内存
作用:管理直接内存,虚引用被回收前释放直接内存
原理:虚引用的使用需要借助队列,虚引用被回收时会往队列中存入数据,监听队列即可获取通知
应用:Netty中零拷贝(Zero Copy)
不可达对象一定会被回收吗?
三色标记
白色
表示对象 尚未 被垃圾收集器访问过
可达性分析刚开始的时候所有对象都是白色,若分析结束时仍为白色,则认为该对象不可达
黑色
表示对象 已经 被垃圾收集器访问过,且这个对象的 所有引用都已经扫描过
黑色表示安全存活的对象,其他引用指向黑色则无需重新扫描,不能直接指向白色
灰色
表示对象 已经 被垃圾收集器访问过,但这个对象上 至少存在一个引用还没有被扫描过
可达性分析的扫描过程可以看作 一股以灰色为波峰的波纹从黑向白推进的过程
注意
可达性分析时如果只有收集器线程在工作,那不会有任何问题,但是 用户线程和收集器线程并发工作时会有问题
两种后果
一种是把原本消亡的对象错误标记为存活,这不好但可以容忍,下次收集清理掉浮动垃圾即可
另一种是把原本存活的对象错误标记为已消亡,这是非常致命的,程序肯定会因此发生错误
造成原因
条件一:赋值器插入了一条或多条从黑色对象到白色对象的新引用
条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
CMS解决方案:增量更新
(Incremental Update)
(Incremental Update)
破坏第一个条件:将原本的黑色转变为灰色,这样收集器线程会重新扫描灰色对象下的引用
CMS缺点
并发标记时有漏标
解决方案就是要从头重新标记(remark)
导致CMS的重新标记阶段 STW 时间较长
使用标记-清除算法会产生大量碎片,如果有大对象进来后发现没有
连续的空间存放,则会触发担保机制产生很长的 STW
连续的空间存放,则会触发担保机制产生很长的 STW
G1 解决方案:原始快照(STAB)
(Snapshot At The Beginning)
(Snapshot At The Beginning)
破坏第二个条件
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,
在并发扫描结束之后,再将记录过的引用关系中的灰色对象为根,重新扫描一次
在并发扫描结束之后,再将记录过的引用关系中的灰色对象为根,重新扫描一次
简化理解:无论引用删除与否,都会按照刚开始扫描那一刻的对象图的快照来搜索
G1 的优点
相比CMS
G1采用标记-整理算法,不会产生空间碎片
并发标记后,只扫描标记的灰色对象,停顿时间短
停顿时间可控,在不牺牲吞吐量的前提下,实现低停顿垃圾回收
充分利用CPU,多核条件下的硬核优势来缩短 STW
垃圾回收算法
复制算法
此算法把内存空间划为两个相等的区域,每次只使用其中的一个区域,
垃圾回收时,遍历当前使用区域,把还存活的对象复制到另外一个区域
垃圾回收时,遍历当前使用区域,把还存活的对象复制到另外一个区域
优缺点
存活对象少时效率高,不产生碎片
可用内存变为原来的一半,存活对象多时效率低
标记清除
此算法执行分两阶段:第一阶段从引用根节点开始标记所有被引用
的对象,第二阶段遍历整个堆,把未标记的对象清除
的对象,第二阶段遍历整个堆,把未标记的对象清除
优缺点
相比于复制算法不浪费内存
会产生碎片,效率低,因为要扫描两次
标记整理
分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个
堆,清除未标记的对象并且把存活对象“压缩”到堆的其中一块,按顺序排列
堆,清除未标记的对象并且把存活对象“压缩”到堆的其中一块,按顺序排列
优缺点
避免了碎片问题和空间占用问题
效率比复制算法低,因为要多维护一个链表使幸存对象连续
分代回收
新生代
复制算法
一个Eden区、两个Survival区(From Survival、To Survival)(8:1:1)
大多数情况下对象在Eden区中分配,当Eden区没有足够的空间时,发起一次Minor GC
内存分配
回收策略
回收策略
大对象直接进入老年代
为了降低大对象分配内存时造成的内存复制的开销
长期存活的对象进入老年代
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁
动态对象年龄判断
Minor GC之后,如果检测到可能长期存活的对象,则让其尽早迸入老年代
老年代空间分配担保机制
Minor GC之前做风险判断,是否允许担保失败,如不允许则改为Full GC
老年代
标记-清除/整理-算法
Major GC/Full GC
整个Java堆
方法区/永久代/元空间
对永久代的回收主要包括废弃的常量和无用的类
永久代和元空间的区别在于永久代位于JVM的方法区
中,元空间并不在虚拟机内存中,而是使用本地内存
中,元空间并不在虚拟机内存中,而是使用本地内存
Full GC
垃圾收集器
分代模型
新生代
Serial
简单高效,单线程,收集时暂停其他所有线程(STW)
ParNew
Serial的多线程版本,是首选新生代收集器,可配合CMS
Parallel Scavenge
以吞吐量为优先的多线程收集器,不支持CMS
老年代
Serial Old
Serial的老年代版本,可作为CMS的后备预案
Parallel Old
Parallel Scavenge的老年代版本,多线程,吞吐量优先
CMS
并发收集、低停顿
适用于当今互联网网站或基于浏览器的B/S系统的服务器上,这类应用通常都
较为关注服务的响应速度,尽量缩短系统的停顿时间,给用户更好的交互体验
较为关注服务的响应速度,尽量缩短系统的停顿时间,给用户更好的交互体验
流程
初始标记:标记GC Roots能直接关联到的对象,速度很快,STW
并发标记:从GC Roots的直接关联对象开始遍历整个对象图,耗时长
重新标记:修正并发标记期间可能造成的改动,速度较慢,STW 略长
并发清除:清除标记阶段已经死亡的对象,此阶段和用户线程并发工作
缺点
对CPU敏感,并发阶段虽然不会导致用户线程停顿,但却由于占用资源导致应用程序变慢,降低总吞吐量
由于CMS在清理时与用户线程并发,运行期间还伴随有新垃圾产生,有可能触发担保机制而产生较大停顿
基于标记清除算法,造成大量的空间碎片,导致没有足够的连续空间存放大对象,不得不提前触发Full GC
组合使用
Parallel Scavenge + Parallel Old(JDK1.8默认)
分区模型
G1:逻辑分代
物理不分代
物理不分代
引入分区的思路,弱化了分代的概念
把连续的Java堆划分为多个大小相等的独立区域(Region)
专门用来存放大对象的区域(Humongous)
流程
初始标记:标记GC Roots能直接关联到的对象,需要停顿用户线程,但时间很短
STW
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长
最终标记:标记那些在并发标记阶段发生变化的对象,将被回收,需要暂停用户线程
STW
筛选回收:对各个Regin的回收价值和成本进行排序,根据用户所期望的停顿时间来
制定回收计划,回收一部分Region(必须暂停用户线程,由多条收集器线程并行完成)
制定回收计划,回收一部分Region(必须暂停用户线程,由多条收集器线程并行完成)
STW
G1 收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,也就是说,它并非
纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量
纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量
Young GC
Mixed GC
特点
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势
优点
采用标记整理算法,不会产生空间碎片
停顿时间可控,在不牺牲吞吐量的前提下,实现低停顿垃圾回收
充分利用CPU,多核条件下的硬核优势来缩短 STW
缺点
内存占用高
执行负载大
常见问题
G1如何实现停顿时间可控?
它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region
大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集
大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集
在后台维护一个优先级列表,优先回收价值大的 Region(Garbage First的由来)
保证了 G1 收集器在有限的时间内获取尽可能高的收集效率
保证了 G1 收集器在有限的时间内获取尽可能高的收集效率
怎样建立起可靠的停顿预测模型?
G1 收集器的停顿预测模型是以衰减平均值(Decaying Average)为理论基础来实现的
衰减平均值更准确地代表“最近的”平均状态,Region的统计状态越新越能决定其回收的价值
Region里面存在的跨Region引用对象如何解决?
使用记忆集避免全堆作为GC Roots扫描
在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
CMS 收集器采用增量更新算法实现
G1 收集器则是通过原始快照(SATB)算法来实现的
ZGC
Shenandoah
其他收集器
Epsilon
PGC、C4、OpenJ9等
调优
Parallel常用参数
-XX:SurvivorRatio
-XX:PreTenureSizeThreshold:大对象到底多大
-XX:MaxTenuringThreshold:
-XX:+ParallelGCThreads:并行收集器的线程数,一般设置为和CPU核数相同(同样适用于CMS)
-XX:+UseAdaptiveSizePolicy:自动选择各区大小比例
CMS常用参数
-XX:+UseConcMarkSweepGC
-XX:ParallelCMSThreads
CMS线程数量
-XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值)
如果频繁发生SerialOld卡顿,应该调小(频繁CMS回收)
如果频繁发生SerialOld卡顿,应该调小(频繁CMS回收)
-XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收
-XX:GCTimeRatio
设置GC时间占用程序运行时间的百分比
-XX:MaxGCPauseMillis
停顿时间(建议)GC会尝试各种手段达到这个时间,比如减小年轻代
G1常用参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis
建议值,G1会尝试调整Young区的块数来达到这个值
-XX:+GCPauseIntervalMillis
GC的间隔时间
-XX:+G1HeapRegionSize
分区大小,建议逐渐增大该值
G1NewSizePercent
新生代最小比例,默认为5%
G1MaxNewSizePercent
新生代最大比例,默认为60%
GCTimeRatio
GC时间建议比例,G1回根据这个值调整堆空间
ConcGCThreads
线程数量
InitiatingHeapOccupancyPercent
启动G1堆空间占用比例
垃圾收集日志
日志级别从低到高
Trace、Debug、Info、Warning、Error、Off 共六种
默认为 Info
命令
查看GC基本信息
-XX:+PrintGC
JDK9之前使用
-Xlog:gc
JDK9之后使用
查看GC详细信息
-XX:+PrintGCDetails
JDK9之前
-Xlog:gc*
JDK9之后
查看GC前后 堆和方法区 可用容量的变化
-XX:+PrintHeapAtGC
-Xlog:gc+heap=debug
查看GC过程中用户线程并发时间以及停顿的时间
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-Xlog:safepoint
查看熬过收集后剩余对象的年龄分布信息
-XX:+PrintTenuring-Distribution
-Xlog:gc+age=trace
0 条评论
下一页