这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
JVM已经是Java面试必问的一块内容,对这块知道越多的细节,就越容易让面试官刮目相看。
我结合自身学习和面试经历,总结了JVM的相关面试题,包括JVM基础篇,内存结构篇,GC篇,调优篇。
JVM基础
什么是JVM
JVM全称是Java Virtual Machine ,中文称为Java虚拟机。
JVM是Java程序运行的底层平台,与Java支持库一起构成了Java程序的执行环境。分为JVM规范和JVM实现两个部分。简单来说,Java虚拟机就是指能执行标准Java字节码的虚拟计算机。
请问JDK与JVM有什么区别
现在的JDK、JRE和JVM一般是整套出现的。
JDK = JRE + 开发调试诊断工具
JRE = JVM + Java标准库
什么是Java字节码
Java 中的字节码,是值 Java 源代码编译后的中间代码格式,一般称为字节码文件。
常用的JVM启动参数
# JVM启动参数不换行
# 设置堆内存
‐Xmx4g ‐Xms4g
# 指定GC算法
‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50
# 指定GC并行线程数
‐XX:ParallelGCThreads=4
# 打印GC日志
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps
# 指定GC日志文件
‐Xloggc:gc.log
# 指定Meta区的最大值
‐XX:MaxMetaspaceSize=2g
# 设置单个线程栈的大小
‐Xss1m
# 指定堆内存溢出时自动进行Dump
‐XX:+HeapDumpOnOutOfMemoryError
‐XX:HeapDumpPath=/usr/local/
设置堆内存XMX应该考虑哪些因素
需要根据系统的配置来确定,要给操作系统和JVM本身留下一定的剩余空间。推荐配置系统或容器里可用内存的 70%、80%最好。
比如说系统有 8G 物理内存,系统自己可能会用掉一点,大概还有 7.5G 可以用,那么建议配置‐Xmx6g 。
说明:7.5G*0.8 = 6G ,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。
‐Xmx 设置的值与JVM进程所占用的内存有什么关系?
JVM总内存=栈+堆+非堆+堆外+Native
类加载机制
.java——.class——加载——链接(验证、准备、解析)——初始化——使用——卸载
加载
- 读取类的二进制流
- 转为方法区数据结构,并存放到方法区
- 在Java堆中产生Java.lang.Class对象
验证
- 验证class文件是不是符合规范
- 文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理
- 元数据验证
- 是否有父类
- 是否继承了final类(final类不能被继承,如果继承了就有问题了)
- 非抽象类实现了所有抽象方法
- 字节码验证
- 运行检查
- 栈数据类型和操作码操作参数吻合(比如栈空间只有2字节,但其实却需要大于2字节,此时就认为这个字节码是有问题的)
- 跳转指令指向合理的位置
- 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
- 可使用-Xverify:none关闭验证
准备:
- 为类的静态变量分配内存,初始化为系统的初始值
- final static修饰的变量:直接赋值为用户定义的值,比如private final static int value = 123,直接赋值123
- private static int value = 123,该阶段的值依然是0
解析:
- 作用:符号引用转换成直接引用
初始化:
- 执行clinit方法,clinit方法由编译器梓东收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法
- 初始化的顺序和源文件中的顺序一致
- 子类的clinit被调用前,会先调用父类的clinit
- JVM会保证clinit方法的线程安全性
- 初始化时,如果实例化一个新对象,会调用init方法对实例变量进行初始化,并执行对应的构造方法内的代码
内存结构篇
JVM内存结构划分
线程私有:程序计数器(字节码指令 no OOM),虚拟机栈(Java方法 SOF & OOM),本地方法栈(native方法 SOF & OOM)
所有线程共享:metaspace(类加载信息 OOM),java堆(常量池(字面量和符号引用量OOM),堆(数组和类对象 OOM))
程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即“线程私有”
- 对Java方法计数,如果是native方法则计数器为undefined
- 不会发生内存泄露
java虚拟机栈(Stack)
- Java方法执行的内存模型
- 包含多个栈帧(局部变量表,操作栈,动态连接,返回地址)
局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
- 局部变量表为操作数栈提供必要的数据支撑
每调用一个方法,就会有一个栈帧,方法调用结束后,栈帧就会自动释放,栈的内存不需要通过gc去回收,而会自动释放。
本地方法栈
- 与虚拟机栈相似,主要作用于标注了native的方法
元空间(metaSpace)与永久代(PermGen)的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot与其他JVM如Jrockit的集成
Java堆
- 对象实例的分配区域
- GC管理的主要区域
java内存模型中堆和栈的区别
联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
区别
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
GC篇
Java8默认使用的垃圾收集器是什么?
Java8版本的Hotspot JVM,默认情况下使用的是并行垃圾收集器(Parallel GC)。其他厂商提供的JDK8基本上也默认使用并行垃圾收集器。
Java11的默认垃圾收集器是什么?
Java9之后,官方JDK默认使用的垃圾收集器是G1。
什么是年轻代
年轻代是分来垃圾收集算法中的一个概念,相对于老年代而言,年轻代一般包括:
- 新生代,Eden区。
- 存活区,执行年轻代GC时,用存活区来保存活下来的对象。 存活区也是年轻代的一部分,但一般有2个存活区,所以可以来回倒腾。
什么是GC停顿(GC pause)
因为GC过程中,有一部分操作需要等所有应用线程都到达安全点,暂停之后才能执行,这时候就叫做GC停顿,或者叫做GC暂停。
对象被判定位垃圾的标准
没有被其他对象引用
判断对象的引用数量
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当做垃圾收集
引用计数算法
- 优点:执行效率高,程序执行受影响较小
- 缺点:无法检测出循环引用的情况,导致内存泄露
可达性分析算法
- 通过判断对象的引用链是否可达来决定对象是否可以被回收
可以作为gc root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈找那个JNI(Native方法)的引用对象
- 活跃线程的引用对象
常用的垃圾回收算法
标记-清除算法(mark and sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
- 碎片化,有不连续的碎片块,可能导致后面有对象的时候无法分配内存
复制算法(copying)
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
- 应对对象存活率较高的时候就力不从心了,要进行较多的复制操作
标记-整理算法(compacting)(适用于老年代)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景
分代收集算法(Generation Collector)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
- 目的:提供JVM的回收效率
触发FULL GC的条件
- 老年代空间不足
- cms gc时出现promotion failed,concurrent mode failure
- minor gc晋升到老年代的平均大小大于老年代的剩余空间
- system.gc()
- 使用RMI来进行RPC或管理的jdk应用,每小时执行1次full gc
什么是STW
stop-the-world
- JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
- 多数GC优化通过减少stw发生的时间来提高程序性能
常见的垃圾收集器有哪些?
垃圾收集算法:为实现垃圾回收提供理论支持
垃圾收集器:利用垃圾收集算法,实现垃圾回收的实践落地
常见的垃圾收集器包括:
- 串行垃圾收集器: ‐XX:+UseSerialGC
- 并行垃圾收集器: ‐XX:+UseParallelGC
- CMS垃圾收集器: ‐XX:+UseConcMarkSweepGC
- G1垃圾收集器: ‐XX:+UseG1GC
术语:Stop the world
- 简写为STW,也叫全局停顿,Java代码停止运行,native代码继续运行,但不能与JVM进行交互
- 原因:多半由于垃圾回收导致;也可能由dump线程、死锁检查、dump堆等导致
- 危害:服务停止、没有相应;主从切换,危害生产环境
并行收集 VS 并发收集
- 并行收集:指多个垃圾收集线程并行工作,但是收集的过程中,用户线程(你的业务线程)还是处于等待状态的
- 并发收集:指用户线程与垃圾收集线程同时工作
吞吐量
- CPU用于运行用户代码的时间与CPU总消耗时间的比值
- 公式:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
年轻代常见的垃圾收集器
serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾回收时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
- 适用场景:
- 客户端程序,应用以-client模式运行时,默认适用的就是Serial
- 单核机器
ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势
- 适用场景
- 主要用来和CMS收集器配合使用
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 多线程
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
- 特点:
- 可以达到一个可控制的吞吐量
- -XX:MaxGCPauseMillis:控制最大的垃圾收集停顿时间(尽力)
- -XX:GCTimeRatio:设置吞吐量的大小,取值0-100,系统花费不超过1/(1+n)的世界用于垃圾收集
- 自适应GC策略:可用-XX:+UseAdptiveSizePolicy打开
- 打开自适应策略后,无需手动设置新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)等参数
- 虚拟机会自动根据系统的运行状况收集性能监控信息,动态地调整这些参数,从而达到最优的停顿时间以及最高的吞吐量
- 可以达到一个可控制的吞吐量
- 适用场景
- 注重吞吐量的场景
老年代常见的垃圾收集器
serial old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- Serial收集器的老年代版
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
- 可以和Serial/ParNew/Parallel Scavenge这三个新生代的垃圾收集器配合使用
- CMS收集器出现故障的时候,会用Serial Old作为后备
Parallel old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- 多线程,吞吐量优先
- Parallel Scavenge收集器的老年代版本
- 特点
- 只能和Parallel Scavenge配合使用
- 关注吞吐量的场景
CMS收集器(-XX:+UseConcMarkSweepGC 标记-清除算法)
- CMS:Concurrent Mark Sweep
- 并发收集器(前面都是并行的)
- 执行过程
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
- 特点:
- STW的时间比较短
- 大多过程并发执行
- 缺点:
- CPU资源比较敏感
- 并发阶段可能导致应用吞吐量的降低
- 无法处理浮动垃圾
- 不能等到老年代几乎满了才开始收集
- 预留的内存不够——concurrent Mode Failure——Serial Old作为后备
- 可使用CMSInitiatingOccupancyFraction设置老年代占比达到多少就触发垃圾收集,默认68%
- 内存碎片
- 标记-清除 导致碎片的产生
- UseCMSCompactAtFullCollection:在完成Full GC后是否要进行内存碎片整理,默认开启
- CMSFullGCsBeforeCompaction:进行几次Full GC后就进行一次内存碎片整理,默认0
- CPU资源比较敏感
- 适用场景:
- 希望系统停顿时间短,响应速度快的场景,比如各种服务器应用程序
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
- Garbage First
- 面向服务器端应用的垃圾收集器
- 内存布局1-32M
- E,S,O,H
- Humongous:存储大对象,存不下就分配两个H
- 将整个Java堆内存划分成多个大小相等的Region
- 年轻代和老年代不再物理隔离
- 设计思想:
- 内存分块(Region)
- 跟踪每个Region里面的垃圾堆积的价值大小
- 构建一个优先列表,根据允许的收集时间,优先回收价值高的Region
- 特点
- 可以作用在整个堆
- 可控的停顿(MaxGCPauseMillis=200)
- 无内存碎片
- 适用场景
- 占用内存较大的应用(6G以上)
- 替换CMS垃圾收集器
回收策略总览
回收策略总览
- 年轻代——serial,parNew,Parallel Scavenge,G1
- 老年代——CMS,Serial Old,Parallel Old,G1
如何选择垃圾收集器?
关注的主要矛盾点是什么?
- 数据分析类,尽快获得执行结果,吞吐量就是考虑的点,考虑parallel scavenge
- web应用,低延迟是考虑的点,所以是CMS,G1
基础设施
- 单核嵌入式机器,并行和并发都不合适
调优篇
性能调优参数-Xms,-Xmx,-Xss的含义
- Xss:规定了每个线程虚拟机栈(堆栈)的大小(通常256k)
- Xms:堆的初始值
- Xmx:堆能达到的最大值
如果CPU使用率突然飙升,你会怎么排查
针对当前问题,往往需要使用不同的工具来收集信息,例如:
- 收集不同的指标(CPU,内存,磁盘IO,网络等等)
- 分析应用日志
- 分析GC日志
- 获取线程转储并分析
- 获取堆转储来进行分析
系统性能一般怎么衡量
可量化的3个性能指标:
- 系统容量:比如硬件配置,设计容量;
- 吞吐量:最直观的指标是TPS;
- 响应时间:也就是系统延迟,包括服务端延时和网络延迟。
这些指标。可以具体拓展到单机并发,总体并发,数据量,用户数,预算成本等等。
查看JVM进程号的命令是什么?
可以使用 ps ‐ef 和 jps ‐v 等等。
怎么查看剩余内存?
比如: free ‐m , free ‐h , top 命令等等。
查看线程栈的工具是什么?
一般先使用 jps命令, 再使用 jstack ‐l
用什么工具来获取堆内存转储?
一般使用 jmap 工具来获取堆内存快照。
JVM的优化本质上是做调参
- 扩大内存可以更少的触发gc
- 内存太大触发gc时候的停顿时间会长
因此要根据你实际的业务场景设置一个合适的值,并配合压测和线上环境的实际情况做不断的调优 吞吐量=花费在非GC停顿上的工作时间/总时间 至少需要优化到95%
-Xms 启动JVM时堆内存的大小 -Xmx 堆内存最大限制 两者需要设置的一样防止扩缩容(CPU又要关注业务,又要处理扩容,会对cpu产生干扰)
-XX:NewSize 年轻代大小 -XX:MaxNewSize 最大年轻代大小 两者需要设置的一样防止扩缩容 (互联网应用,年轻代的通常的设置的大一些,慢一点进入老年代,减少老年代的gc)
-XX:SurvivorRatio eden survivor占比,默认为8(eden区要比survivor区设置的至少大一倍,否则没有意义) Eden需要比survivor尽可能的大,防止多次触发young gc导致年龄快速增长到可以进入老年代的case
-XX:MetaspaceSize 元空间初始空间大小 -XX:MaxMetaspaceSize=512m 元空间最大空间,默认是没有限制的 这两个参数不建议设置 元数据区是放在了本地内存空间
通用JVM工具整理
- jps:虚拟机进程状态工具
- jps -v | grep 6783
- jinfo:jvm参数信息工具
- jinfo -flags pid
- jstat:查看虚拟机各种运算状态
- 示例:jstat -gcutil pid
- S0:新生代中s0区已使用空间的百分比
- S1:新生代中S1区已使用空间的百分比
- E:新生代已使用空间的百分比
- O:老年代已使用空间的百分比
- M:元数据区已使用空间的百分比
- CCS:压缩类空间利用率百分比
- YGC:从应用程序启动到当前,发生Young GC的次数
- YGCT:从应用程序启动到当前,Young GC所用的时间【单位:秒】
- FGC:从应用程序启动到当前,发生Full GC的次数
- FGCT:从应用程序启动到当前,Full GC所用的时间【单位:秒】
- GCT:从应用程序启动到当前,用于垃圾回收的总时间【单位:秒】
- 示例:jstat -gcutil pid
- jstack: 线程快照工具
- 示例:jstack -l pid(可排查锁)
- jmap:HeapDump工具
- 示例:
- jmap -heap pid 查看堆信息
- jamp -dump:format=b,file=heapDUmp.hprof pid 导出堆文件并用jhat查看
- jhat -port 8899 heapDump.hprof 浏览器访问
- 示例:
- 线上OOM问题排查
- java -Xms48m -Xmx48m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof -jar mianshi.jar
- 使用jprofiler查看dump文件及call tree分析
- jprofiler也可以attach到本地的java应用
可能导致CPU占用率过高的场景与解决方案
- 无限while循环
- 尽量无限循环
- 让循环执行得慢一点
- 频繁GC
- 降低GC频率
- 频繁创建新对象
- 合理使用单例
- 序列化和反序列化
- 使用合理的API实现功能
- 选择好用的序列化/反序列化类库
- 正则表达式
- 减少字符串匹配期间执行的回溯
- 频繁的线程上下文切换
- 降低切换的频率
内存溢出场景
- 堆内存溢出
- 栈内存溢出
- 方法区溢出
- 直接内存溢出
堆内存溢出的场景
- 内存泄漏(mat)
- 非内存泄漏
举例: 如果列表页查询,用户传过来的pageSize很大,就会造成堆溢出
栈内存溢出
- hotspot虚拟机的栈内存不可扩展
- 统一用Xss设置栈的大小
方法区溢出
- 方法区是一个逻辑上的概念,一部分在堆内存,一部分在元空间
- 常量池里对象太大
- 加载的类的种类太多
- 动态代理的操作库生成了大量的动态类
- JSP项目
- 脚本语言动态类加载
如何避免方法区溢出
- 设置xmx
- 留空元空间相关的配置,或设置合理大小的元空间
直接内存和直接内存溢出
- 直接内存是一块由操作系统直接管理的内存,也叫堆外内存
- 可以使用Unsafe或ByteBuffer分配直接内存
- 可用-XX:MaxDirectMemorySize控制,默认是0,表示不限制
为什么要有直接内存
- 性能优势
直接内存使用场景
- 有很大的数据需要存储,生命周期很长
- 频繁的IO操作,比如并发网络通信
直接内存的经验之谈
- 堆dump文件看不出问题或者比较小,可考虑直接内存溢出问题
- 配置内存时,应给直接内存预留足够的空间
直接内存总结
- 直接内存也叫堆外内存,IO效率较高
- 可以用Unsafe类或ByteBuffer来分配
如何定位并解决项目越跑越慢的问题
- STW过长
- 项目依赖的资源导致变慢
- 数据库
- 网络
- code cache满了
- 线程争抢过于激烈
- perfma
- fastThread
- 服务器问题
- 操作系统问题
- 其他进程争抢资源