JVM
# JVM
# 类加载机制
# hotspot

# 类加载过程

- 加载、验证、准备、解析、初始化、使用、卸载
- 加载
- 在硬盘上查找并通过IO读入字节码文件,使用到的类才会加载,调用类main(),new对象等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,对应方法区这个类的各种数据入口
- 主类在运行过程如果用到其他类,会逐步加载这类,jar或war不是一次性加载全部,是使用到才加载
- 加载到方法区包括
- 运行时常量池
- 类型信息
- 字段信息
- 方法信息
- 类加载器的引用----这个类到类加载器实例的引用
- 对应class实例的引用----类加载器加载类信息到方法区后,会创建一个Class类型的对象放到Heap中,作为开发人员访问方法区中类定义的入口和切入点
- 链接
- 验证
- 校验字节码文件的正确性
- 准备
- 给类的静态变量分配内存,并赋予默认值,例如static int = 0,static boolean = false,Object = null
- 解析
- 将符号引用替换为直接引用(内存/物理地址),该阶段会把一些静态方法(符号引用,例如main())替换为数据所存指针或句柄(直接引用),静态链接过程,动态链接指在程序运行期间完成的将符号引用替换为直接引用的过程
- 验证
- 初始化
- 对类的静态变量初始化指定的值,执行静态代码块
- 使用
- 卸载
- 加载
# 双亲委派原则
- 每个类加载器都有自己的父类加载器,当一个类加载器需要加载某个类时,首先会委托给它的父类加载,即先找父亲加载,如果父亲不能加载,子类才会尝试自己去加载
# 打破双亲委派
自定义类加载器,重写findClass,核心类由appClassLoader加载,自定义的类自己加载,不在委派给双亲加载
# 加载器类型
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库
- rt.jar
- charsets.jar
- boostrapLoader(C++实现)
- 扩展类加载器:负责加载JRE的ext目录下的jar包
- extCloassLoader
- 应用程序类加载器:负责加载classPath路径下的类包,主要就是加载自己写的类
- appClassLoader
- 自定义加载器:负责加载用户自定义路径下的类包
# 为什么设计
- 沙箱安全:自己写的java.lang.String类不会被加载,防止核心API被随意篡改
- 避免重复加载:当父亲已经加载了该类,就没有必要子ClassLoader再加载一次,保证加载类的唯一性
# JVM内存模型

# 内存模型
- 程序计数器:记录当前线程执行位置,以便cpu时间片执行
- 虚拟机栈:方法执行时,每个方法都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口
- 本地方法栈:和虚拟机栈类似,但是虚拟机栈用于Java方法的执行,而本地方法栈用于Native方法(C++)
- 堆:所有对象实例和数组都放在这里
- 方法区:存放类信息、常量、静态变量
- 直接内存:NIO,堆外内存
- 运行时常量池:存放编译期生成的各种字面量和符号引用
# 参数设置

- 堆
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -Xmn:新生代大小
- -XX:NewRatio:新生代和老年代比值
- -XX:SurvivorRatio:新生代中eden和s0的比例,默认-XX:SurvivorRatio=8,eden:s0=8:2
- 栈
- Xss:每个线程的栈大小
- 方法区(元空间)
- -XX:MetaspaceSize:最小元空间大小,指定原空间触发FullGC阈值,以字节为单位,默认是21M,建议对于8G机器,设置-XX:MetaspaceSize=256m,与-XX:MaxMetaspaceSize=256m,防止元空间动态扩展
- -XX:MaxMetaspaceSize:最大元空间大小,默认-1,表示无限制
# 垃圾回收(GC)
# 垃圾回收机制(算法)
- 分代回收
- 新生代:复制算法,新生代对象存活时间短,老年代对象存活时间长
- 老年代:标记-整理算法,老年代对象存活时间长
- 永久代:方法区,存储类信息、常量、静态变量等
- 引用计数法:给对象添加一个引用计数器,有对象引用时计数器+1,引用失效时计数器-1,没办法解决循环引用问题
- 复制算法:
- 流程:将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一块去,然后把使用过的空间一次清理掉
- 标记从gc root做可达性分析,标记所有可达对象
- 从s0复制存活对象到s1
- 清理s0
- 重复以上步骤,在s1和s0之间来回拷贝对象
- 适用场景
- 存活对象少,比较高效
- 适合新生代,因为新生代对象存活时间短
- 优点:快速回收内存,不会产生内存碎片
- 缺点:需要两块一样的空闲空间,需要复制移动对象
- 流程:将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一块去,然后把使用过的空间一次清理掉
- 标记-清除算法:
- 流程:分为标记和清除两个阶段,先标记出所有需要回收的对象,在回收没被标记的对象,标记清除算法需要遍历整个堆空间,效率低
- 标记存活对象
- 清除没有标记的对象
- 适用场景
- 对象存活比较多
- 老年代
- 优点:实现简单,只需要标记存活对象和清理未标记对象,可以解决循环引用,只要对象不可达即可回收
- 缺点:会产生碎片,以及STW
- 流程:分为标记和清除两个阶段,先标记出所有需要回收的对象,在回收没被标记的对象,标记清除算法需要遍历整个堆空间,效率低
- 标记-整理算法:
- 流程:标记过程和标记-清除类似,但后续步骤不是直接清理对象,而是把存活的对象移动到内存的一端
- 标记阶段:遍历整个内存空间,标记被gc root引用的对象
- 整理阶段:移动那些仍然在使用中的对象,将他们整理到内存的另一端,释放连续的内存空间,不需要额外的空间
- 适用场景:
- 对象存活较多
- 老年代
- 优点:与标记复制算法相比,标记-整理算法可以节约内存空间;与标记清除算法相比,避免碎片
- 缺点:整理过程中,由于对象位置变动,需要调整虚拟机栈中的引用地址,增加算法复杂性,会STW
- 流程:标记过程和标记-清除类似,但后续步骤不是直接清理对象,而是把存活的对象移动到内存的一端
# 垃圾回收器
- Serial(串行)
- 使用:-XX:+UseSerialGC,-XX:+UseSerialOldGC
- 单线程,且回收的时候会STW
- 新生代采用复制算法,老年代采用标记-整理算法
- 优点:简单高效,与其他收集器的单线程相比

- Parallel(并行)
- 使用:-X:+UseParallelGC,-XX:+UseParallelOldGC
- Serial的多线程版本,收集器线程数与CPU数量相同,也可以指定-XX:ParallelGCThreads=N
- 新生代采用复制,老年代采用标记-整理

- ParNew
- 使用:-XX:+UseParNewGC
- 跟Parallel类似,但只用于新生代,可以配合CMS使用
- 新生代采用复制算法

- CMS(并发标记清除)
- 步骤
- 初始标记:STW,标记GC Roots能直接关联到的对象
- 并发标记:从GC Roots开始遍历整个堆,标记所有存活对象
- 重新标记:STW,修正并发标记期间,因用户程序继续运行而导致的标记变动的对象
- 并发清除:清理未标记对象
- 以获取最短停顿时间为目标,几乎可以让收集线程与用户线程同时工作
- 优点:并发收集、低停顿
- 缺点:对cpu资源敏感,无法处理浮动垃圾,大量空间碎片

- 步骤
- G1
- 分区概念,弱化分代
- 标记整理算法,不会产生空间碎片,分配大对象不会提前fullgc
- 可以设置停顿时间
- 充分利用cpu多核条件下缩短stw
- 步骤
- 初始标记:STW,标记GC Roots能直接关联到的对象
- 并发标记:从GC Roots开始遍历整个堆,标记所有存活对象
- 最终标记:STW,修正并发标记期间,因用户程序继续运行而导致的标记变动的对象
- 筛选回收:对各个分区进行回收价值和成本排序,根据用户期望的停顿时间选择要回收的分区的对象
- ZGC
- jdk11
# 可达性分析
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 活跃线程引用的对象
# 强软弱虚
- 强引用:new出来的对象,只要强引用存在,垃圾回收器就永远不会回收
- 软引用:内存空间足够,垃圾回收器不会回收;内存空间不足,垃圾回收器会回收
- 弱引用:只要垃圾回收器工作,不管内存是否足够,都会被回收
- 虚引用:不会对对象生存时间产生影响,也无法通过虚引用来获取对象
# OOM的种类
- 堆溢出:java.lang.OutOfMemoryError: Java heap space
- 栈溢出:java.lang.StackOverflowError
- 直接内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
- 永久代溢出:java.lang.OutOfMemoryError: PermGen space
- 元空间溢出:java.lang.OutOfMemoryError: Metaspace
- 无法创建新线程:java.lang.OutOfMemoryError: unable to create new native thread
# JVM性能检测工具
- jps:查看进程
- jstat:查看堆内存使用情况
- jmap:查看堆内存使用情况,jmap -dump:file=xxx.dump,format=b
- jstack:查看线程堆栈
- jconsole:图形化界面
- jvisualvm:图形化界面
- jinfo:查看java配置信息
- jdb:调试工具
- Arths:Arthas
# CPU100%排查
# 原因
- 死循环
- 大量GC
- 大量密集型任务
- 死锁
https://mp.weixin.qq.com/s/GyFl-rxQkCZM4q65dl5o1g https://mp.weixin.qq.com/s/TcprDRjzQCXQ7OQmJ0E5XQ
# jstack
- 获取进程id
- top
- 查看进程内的线程id
- top -Hp 3030,找到最消耗cpu的线程id3051,H表示显示线程级别信息,p表示指定进程id
- 将线程id转换为16进制
- printf "%x\n" 3051 # beb
- 通过自带的jstack导出堆栈,查看代码
- jstack 3051 > grep beb(转换后的十六进制代码)
# show-busy-threads 脚本
# arthas
下载
- curl -O https://arthas.aliyun.com/arthas-boot.jar
启动
- java -jar arthas-boot.jar
执行thread命令
- thread
找到堆栈信息
- thread 18
堆栈+线程+锁+内存+代码+对象+对象引用:jstack -m -l
> jstack.log
# 总结
生产环境如果cpu已经被打满了,不要一上来就说什么top,jstack,记住,真实的生产环境如果CPU已经要被打爆了的话 第一选择肯定是重启,并且如果你近段时间有发布的话,还要考虑是否可以回滚,保障生产环境的稳定性是最重要的 还有就是,如果CPU已经被打爆了,不管arthas还是jstack大概率也是执行不了的,jvm无法响应
参考答案: 之前碰到过cpu被打满的情况,线上第一时间进行了重启,重启过程中查了服务那段时间日志,链路,指标,没有发现特殊异常。 当重启完成后,开始排查具体的原因,通过定期执行top命令,发现java进程的cpu使用率确实在增加。 接着通过 top -Hp pid命令,以及jstack命令拿到了应用里cpu使用率最高的线程的堆栈,通过分析堆栈定位到具体的代码,是因为代码触发了一个临界值,进入了死循环。
编辑 (opens new window)
上次更新: 2024/11/05, 15:11:10