jvm
Java
- 概览
- 平台无关性
- GC
- 语言特性
- 面向对象
- 类库
- 异常处理
字节码执行
- 概览
- 过程概览
- 一般情况下,一开始由解释器解释执行
- 当虚拟机发现某个方法或代码块运行较多,就会认为这些代码是”热点代码”
- 为了提高热点代码的执行效率,会用即时编译器(JIT)将这些代码编译成机器码,并执行各层次优化
解释执行
- 由解释器一行一行翻译执行
- 优势在于没有编译的等待时间
- 性能相对差些
- -Xint:设置JVM的执行模式为解释执行模式
编译执行
- 把字节码编译成机器码,直接执行机器码
- 运行效率会高很多,一般认为比解释执行快一个数量级
- 带来额外的开销
- -Xcomp:JVM优先以编译模式运行,不能编译的,以解释模式运行
Hotspot的即时编译器-C1
- 是一个简单快速的编译器
- 主要关注局部性的优化
- 适用于执行时间较短或对启动性能有要求的程序(如GUI)
- 也被称为Client Compiler
Hotspot的即时编译器-C2
- 是为长期运行的服务器端应用程序做性能调优的编译器
- 适用于执行时间较长或对峰值性能有要求的程序
- 也被称为Server Compiler
分层编译
- 分层编译-1
- 解释执行
- 简单C1编译:会使用C1编译器进行一些简单的优化,不开启Profiling
- 受限的C1编译:仅执行带方法调用次数以及循环回边执行次数Profiling的C1编译
- 分层编译-2
- 完全C1编译:会执行带有所有Profiling的C1代码
- C2编译:使用C2编译器进行优化,该级别会启用一些编译耗时较长的优化
- 只想开启C2:
-XX:-TieredCopilation
(禁用中间编译层) - 只想开启C1:
-XX:+TieredCopilation -XX:TieredStopAtLevel=1
热点代码
- 基于采样的热点探测
- 基于计数器的热点探测
- Hotspot内置的两类计数器-1
- 方法调用计数器(统计方法被调用的次数,不开启分层编译时,C1默认阈值1500次,C2是10000次)
- Hotspot内置的两类计数器-2
- 回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为”回边”(C1默认阈值13995,C2是10700)
- 建立回边计数器的主要目的是为了触发OSR编译
- 当开启分层编译时,JVM会根据当前待编译的方法数以及编译线程数来动态调整阈值,
-XX:CompileThreshold,-XX:OnStackReplacePercentage
都会失效
方法内联
- 将目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用
- 内联条件
- 方法体足够小
- 热点方法(如果方法体小于325字节会尝试内联,可用
-XX:FreqInlineSize
修改大小) - 非热点方法(如果方法体小于35字节会尝试内联,可用
-XX:MaxInlineSize
修改大小) - 被调用的方法运行时的实现被可以唯一确定
- static方法,private方法以及final方法,JIT可以唯一确定具体的实现代码
- public的实例方法,指向的实现可能是自身,父类,子类的代码,仅且仅当JIT能够唯一确定方法的具体实现时才有可能完成内联
- 可能带来的问题
- CodeCache的溢出,导致JVM退化成解释执行模式
逃逸分析、标量替换、栈上分配
- 逃逸分析
- 分析变量能否逃出它的作用域
- 全局变量赋值逃逸
- 方法返回值逃逸
- 实例引用逃逸
- 线程逃逸
- 逃逸状态标记-1(全局级别逃逸:一个对象可能从方法或者当前线程中逃逸)
- 逃逸状态标记-2(参数级别逃逸:对象被作为参数传递给一个方法,但是在这个方法之外无法访问/对其他线程不可见)
- 标量替换
- 不可被进一步分解的量(基础数据类型,对象引用)
- 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是创建它的成员变量来代替
- 栈上分配
- 通过逃逸分析,能够确认对象不会被外部访问,就在栈上分配对象
类加载
- 概览
- 过程概览
- .java文件->.class文件->不同平台jvm进行解析-jvm加载.class文件
- Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析
- Native Interface:融合不同开发语言的原生库为Java所用
- Runtime Data Area:JVM内存空间结构模型
ClassLoader种类
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
- 自定义ClassLoader:Java编写,定制化加载
双亲委派机制:
- 自底向上检查类是否已经加载
- 自上向下尝试加载类
类的加载方式
- 隐式加载:new
- 显式加载:loadClass,forName等
- loadClass和forName的区别
- Class.forName得到的class是已经初始化完成的
- Classloader.LoadClass得到的class是还没有链接的
类的加载过程
- .java
- .class
- 通过ClassLoader加载class字节码文件
- 读取类的二进制流
- 转为方法区数据结构,并存到方法区
- 在Java堆中产生Class对象
- 链接:
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配储存空间并设置类变量初始值
- 解析:JVM将常量池内的符号引用转为直接引用
- 初始化(执行类变量赋值和静态代码块)
Java内存模型(JDK8)
- 概览
线程私有:程序计数器、虚拟机栈、本地方法栈
- 程序计数器
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即”线程私有”
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄漏
- Java虚拟机栈
- Java方法执行的内存模型
- 包含多个栈帧
- 本地方法栈
- 与虚拟机栈相似,主要作用于标注了native的方法
线程共享:MetaSpace、Java堆
元空间(MetaSpace)与永久代(PerGen)的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存
- 常量池-静态常量池
- class文件常量池:字面量(文本字符串,final修饰的常量),符号引用(类的全限定名,字段的名称和描述符,方法的名称和描述符)
- 常量池-运行时常量池
- 当类加载到内存中后,JVM就会将静态常量池中的内容存放到运行时的常量池中,主要是编译期生成的字面量,符号引用等
- 常量池-字符串常量池
- 可以理解为运行常量池中分出来的一部分,类加载到内存的时候,字符串会存到字符串常量池里面
- MetaSpace相比PerGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便Hotspot与其它JVM如Jrockit的集成
Java堆
- 对象实例的分配区域
- GC管理的主要区域
堆和栈的区别
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
GC
- 概览
- 对象被判定为垃圾的标准:没有被其它对象引用
判定对象是否为垃圾的算法
- 引用计数算法
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当作垃圾收集
- 优点:执行效率高,程序执行受影响较小
- 缺点:无法检测出循环引用的情况,导致内存泄漏
- 可达性分析算法
- 通过判断对象的引用链是否可达来决定对象是否可以被回收
- 可以作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
垃圾回收算法
- 标记:从根集合就行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
- 标记-整理算法:
- 标记
- 整理清除:移动所有存活对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
- 复制算法:
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除
- 分代收集算法:按照对象生命周期的不同划分区域以采用不同的垃圾回收算法以提高JVM的回收效率
- 年轻代:尽可能快速地收集掉那些生命周期短的对象
- Eden区
- 两个Survivor区
- 对象晋升到老年代
- 经历一定Minor次数(默认15)依然存活的对象
- Survivor区中存放不下的对象
- 新生成的大对象
- 动态年龄:Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄不小于该年龄的对象直接进入老年代
- 老年代:存放生命周期较长的对象
- 触发Minor GC的条件
- 伊甸园空间不足
- 触发Full GC的条件
- 老年代空间不足
- 永久代空间不足
- Minor GC晋升到老年代的平均大小大于大于老年代的剩余空间
- 调用System.gc()
- 使用RMI来进行RPC或管理的JDK应用,每小时执行一次Full GC
- Safepoint
- 分析过程中对象引用关系不会发生变化的点
- 产生Safepoint的地方:方法调用;循环跳转;异常跳转等
- 安全点的数量得适中
- 常用的调优参数
- -XX:SurvivorTatio:Eden和Survivor的比值(默认8:1)
- -XX:NewRatio:老年代和年轻代内存大小的比例
- -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值
- 年轻代:尽可能快速地收集掉那些生命周期短的对象
- 增量算法:每次只收集一小片区域的内存空间的垃圾,减少系统停顿
垃圾收集器
- STW(Stop The World):全局停顿,Java代码停止运行,native代码继续运行,但不能与JVM进行交互
- STW多半由于垃圾回收导致,也可能由dump线程,死锁检查,dump堆导致
- STW危害:服务停止,没有响应,主从切换,危害生产环境
- 年轻代常见的垃圾收集器:
- Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
- 收集过程全程STW
- ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势
- Parallel Scavenge收集器(-XX:+UseParallGC,复制算法)
- 吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
- Serial收集器(-XX:+UseSerialGC,复制算法)
- 老年代常见的垃圾收集器:
- Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
- Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
- 多线程,吞吐量优先
- CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
- 执行过程概览
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预处理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理对象:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
- Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- G1收集器(-XX:UseG1GC,复制+标记-整理算法)
- 执行过程概览
- Garbage First收集器的特点
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
- 将整个堆内存划分成等多个大小相等的Region
- 年轻代和老年代不再物理隔离
工具
- 概览
监控工具
- jps:用来查看JVM进程状态
- -q:只显示进程号
- -m:显示传递给main方法的参数
- -l:显示应用main class的完整包名应用的jar文件完整路径名
- -v:显示传递给JVM的参数
- -V:禁止输出类名,jar文件名和传递给main方法的参数,仅显示本地JVM标识符的列表
- jstat:用于监控JVM的各种运行状态
故障排查工具
- jinfo:主要用于查看与调整JVM参数
- -flag <name>:打印指定参数的值
- -flag [+|-]<name>:启用/关闭指定参数
- -flag <name>=<value>:将指定的参数设置为指定的值
- -flags:打印VM参数
- -sysprops:打印系统属性
- <no option>:打印VM参数和系统属性
- jmap:用来展示对象内存映射或堆内存详细信息
- -clstats:连接到正在运行的进程,并打印Java堆的类加载统计信息
- -finalizerinfo:连接到正在运行的进程,并打印等待finalization的对象信息
- -histo[:live]:连接到正在运行的进程,并打印Java堆的直方图,如果指定live子选项,则仅统计活动对象
- -dump:dump_options:连接到正在运行的进程,并存储Java堆;dump_options取值为:
- live:指定时,仅dump活动对象,未指定则所有对象
- format=b:以hprof格式dump堆
- file=filename:将堆dump到filename
- jstack:用于打印当前虚拟机的线程快照
- -l:显示有关锁的额外信息
- -e:展示有关线程的额外信息(比如分配了多少内存,定义了多少个类等)
- jhat:用来分析jmap生存的堆dump
- jcmd:用于将诊断命令发送到正在运行的Java虚拟机
- -f:从文件读取并执行命令
- -l:列出本机上所有JVM进程
- jhsdb:Hotspot进程调试器
可视化工具
- jhsdb hsdb –pid <pid>
- jconsole:基于JMX的可视化监控工具,管理工具
- VisualVM:监控及故障处理程序
- JDK Mission Control(JMC):JMX控制台,监控虚拟机MBean提供的数据;可持续收集数据的JFR,并可作为JFR的可视化分析工具
第三方工具
- Memory Analyzer又叫 Memory Analyzer Tool(MAT):Java堆内存分析器
- JITWatch:JIT编译器的日志分析器与可视化工具,用来检查内联决策,热点方法,字节码以及汇编的各种细节,常和HSDIS配合使用
远程连接
- 基于jstatd实现远程
- 基于RMI的服务程序,它用于监控基于HotSpot的JVM资源的创建及销毁,并且提供一个远程接口,从而允许监控工具远程地连接到本地的JVM
- 基于JMX实现远程
- 基于SSH实现远程
JVM参数
- 概览
标准选项
- 用于执行常见操作,例如检查JRE版本,设置类路径,启用详细输出等
- java -help查看支持的参数
附加选项(非标准选项)
- HotSpot虚拟机通用选项,以-X开头
- java -X查看支持的参数
高级选项
- 开发人员使用,用于调整Java HotSpot虚拟机操作特定区域,以-XX开头
- 查看支持的参数
- java -XX:+UnlockExpirementalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsInitial
- java -XX:+UnlockExpirementalVMOptions(实验性参数) -XX:+UnlockDiagnosticVMOptions(诊断性参数) -XX:+PrintFlagsInitial
- 使用jhsdb:jhsdb clhsdb –pid <pid>;然后输入flags查看
- XXFox
JVM三大新能调优参数-Xms -Xmx -Xss的含义
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小(256K)
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
常见问题
- 概览
JVM日志
- jdk8垃圾收集日志打印参数
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -Xloggc:./gclog.log
- jdk11等价写法
-Xlog:gc*=trace:file=./xgclog.log
- jdk8打印类加载信息和偏向锁信息
-XX:+TraceClassLoading -XX:+TraceBiasedLocking
- jdk11等价写法
-Xlog:class+load=debug,biasedlocking=debug:file=./trace.log
CPU过高
- top+jstack
- top:找到占用CPU最高的进程
- top -Hp <pid>:找到占用最高的线程
- printf %x <pid>:10进制到16进制转换
- jstack <pid> > tmp.txt:dump线程
-
cat tmp.txt grep -A 30 2abc(16进制):找到有问题的线程前后30行
- jmc
内存溢出
- 堆内存溢出
- mat
- VisualVM
- 栈内存溢出
- 方法区溢出
- 直接内存溢出
- 直接内存是一块由操作系统直接管理的内存,也叫堆外内存
- 可以使用Unsafe或ByteBuffer分配直接内存
- 可用-XX:MaxDirectMemorySize控制,默认是0,表示不限制
- 代码缓存区满
可视化工具
- GC日志
- 线程