一、内存模型(jdk8)
1、元空间
java8之后使用本地内存来存储类的元信息,包括类的版本、字段、方法、接口以及运行时常量池,基本不会发生OOM异常了。
运行时常量池:字面量和符号引用
2、堆
- 堆是JVM内存中最大的一块,由所有,是的内存区域,主要存放对象实例。
- 通过参数和设定堆最大和最小内存空间,此空间不足以分配jvm运行所需内存时报OOM异常。
- 存放的数据:
- 对象实例:类初始化生成的对象;基本数据类型的数组;
- 字符串常量池:String对象的直接引用;
- 静态变量:static修饰的变量;
- 线程分配缓冲区:提升对象分配的效率;
- 内部划分:
- 老年代:存放大对象和长期存活的对象。对象经过垃圾回收年龄达到15次后依然存活的由年轻代的Survior区移动到老年代。老年代空间不足时会进行Full GC。
- 年轻代:内部又分为一个Eden区和两个Survior区,其内存空间分配比例默认为8:1:1。Minor GC负责的区域。
3、栈
- 在java运行时一个方法对应一个栈帧,线程创建后开始执行方法由此产生一系列栈帧就组成了栈。
- 栈是线程私有的,随线程生灭。
- 栈帧中存放了运行时的局部变量表,操作数栈,动态链接,方法返回地址。
- 方法被执行时入栈,执行完后出栈,整个栈体现了其先进后出的特性。
- 会有两种异常:
- 线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出即栈溢出
- 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出即OOM内存溢出。
- 分类:
- 虚拟机栈:执行的是ava方法
- 本地方法栈:执行的是native方法
4、程序计数器
- 当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过其实现跳转、循环、恢复线程等功能。
- ,每个线程都已自己的程序计数器。
- 在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的。
- 不会抛出OOM异常。
二、JVM类加载机制
- java中的类在JVM运行时由类加载器采用双亲委派的机制对其进行加载,java中的类加载器有
1、启动类加载器 (Bootstrap ClassLoader)
a、由 C++ 实现,无父加载器
b、负责加载存放在 <JAVA_HOME>lib 目录,或者被 -Xbootclasspath 参数所指定的路径中存放的类库。
2、平台类加载器 (Platform ClassLoader)
a、jdk11之后才有,由 Java 实现,父加载器为 Boot ClassLoader;
3、扩展类加载器 (Extension ClassLoader)
a、jdk11之前才有,由 Java 实现,父加载器为 null。
b、 是sun.misc.Launcher 的静态内部类,其负责加载 <JAVA_HOME>libext 目录中或者系统变量 java.ext.dirs 指定的路径下的所有类库。
4、应用程序类加载器 (Application ClassLoader)
a、由 Java 实现,父加载器为 Extension ClassLoader或Platform ClassLoader。
b、 是sun.misc.Launcher 的静态内部类,其负责加载用户类路径 (ClassPath) 上所有的类库
5、自定义类加载器 (Custom ClassLoader)
a、由用户基于 Java 实现,父加载器为 Application ClassLoader。
b、如果想实现自己的类加载逻辑,可以继承 ClassLoader 类,并覆写 findClass() 方法。
- 类加载器之间的父子关系一般不会以继承的关系来实现,而都使用组合关系来复用父加载器的代码。
- 双亲委派机制:
若一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,只有当父加载器反馈无法完成这个加载请求时,子加载器才会尝试自己完成加载。由此所有的加载请求最终都应该传递到最顶层的启动类加载器中。如父加载器为空则默认使用启动类加载器作为父加载器。
- 类加载过程
1、类的生命周期
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
a、加载
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的 对象,作为方法区这些数据的访问入口。
b、验证
- 文件格式验证(Class 文件格式检查)
- 元数据验证(字节码语义检查)
- 字节码验证(程序语义检查)
- 符号引用验证(类的正确性检查)
c、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
d、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行
e、初始化
初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)
f、使用
g、卸载
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
三、垃圾收集器
1、种类
a、部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
b、整堆收集 (Full GC):收集整个 Java 堆和方法区。
2、垃圾对象判断方法:可达性分析算法
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
b、成为GC Root 条件
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
3、垃圾收集算法
- 分代收集算法
- 标记清除算法
- 标记复制算法
- 标记整理算法
4、jdk默认垃圾收集器
- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK22: G1
a、Parallel Scavenge 收集器
使用标记-复制算法的多线程收集器
b、Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法
c、G1 (Garbage-First)
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
四、JVM性能调优
1、调优场景
- Heap内存(老年代)持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过1秒);
- 应用出现OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
2、常见故障
- ①JVM内存泄漏。
- ②JVM内存溢出。
- ③业务线程死锁。
- ④应用程序异常宕机。
- ⑤线程阻塞/响应速度变慢。
- ⑥CPU利用率飙升或100%。
3、调优的基本原则
- 大多数的Java应用不需要进行JVM优化;
- 大多数导致GC问题的原因是代码层面的问题导致的(代码层面);
- 上线之前,应先考虑将机器的JVM参数设置到最优;
- 减少创建对象的数量(代码层面);
- 减少使用全局变量和大对象(代码层面);
- 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
- 分析GC情况优化代码比优化JVM参数更好(代码层面);
4、调优目标
- 延迟:GC低停顿和GC低频率;
- 低内存占用;
- 高吞吐量;
5、调优量化目标
- Heap 内存使用率 <= 70%;
- 老年代内存使用率<= 70%;
- avgpause <= 1秒;
- Full gc 次数0 或 avg pause interval >= 24小时 ;
6、常见参数解析
- -Xmx4g:堆内存最大值为4GB。
- -Xms4g:初始化堆内存大小为4GB。
- -Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
- -Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
- -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
- -XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
- -XX:PermSize=100m:初始化永久代大小为100MB。
- -XX:MaxPermSize=256m:设置持久代大小为256MB。
- -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。
7、可调优参数
- -Xms:初始化堆内存大小,默认为物理内存的1/64(小于1GB)。
- -Xmx:堆内存最大值。默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
- -Xmn:新生代大小,包括Eden区与2个Survivor区。
- -XX:SurvivorRatio=1 Eden区与一个Survivor区比值为1:1。
- -XX:MaxDirectMemorySize=1G:直接内存。
报java.lang.OutOfMemoryError: Direct buffer memory异常可以上调这个值。
- -XX:+DisableExplicitGC:禁止运行期显式地调用System.gc()来触发fulll GC。
- -Dsun.rmi.dgc.server.gcInterval=86400来控制触发的时间。
- -XX:CMSInitiatingOccupancyFraction=60:老年代内存回收阈值,默认值为68。
- -XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
- -XX:ParallelGCThreads=8:新生代并行收集器的线程数。
- -XX:MaxTenuringThreshold=10:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
- -XX:CMSFullGCsBeforeCompaction=4:指定进行多少次fullGC之后,进行tenured区 内存空间压缩。
- -XX:CMSMaxAbortablePrecleanTime=500:当abortable-preclean预清理阶段执行达到这个时间时就会结束。
8、JVM异常排查方向
- 应用程序本身导致的问题
- 程序内部频繁触发GC,造成系统出现长时间停顿,导致客户端堆积大量请求。
- JVM参数配置不合理,导致线上运行失控,如堆内存、各内存区域太小等。
- Java程序代码存在缺陷,导致线上运行出现Bug,如死锁/内存泄漏、溢出等。
- 程序内部资源使用不合理,导致出现问题,如线程/DB连接/网络连接/堆外内存等。
- 上下游内部系统导致的问题
- 上游服务出现并发情况,导致当前程序请求量急剧增加,从而引发问题拖垮系统。
- 下游服务出现问题,导致当前程序堆积大量请求拖垮系统,如Redis宕机/DB阻塞等。
- 程序所部署的机器本身导致的问题
- 服务器机房网络出现问题,导致网络出现阻塞、当前程序假死等故障。
- 服务器中因其他程序原因、硬件问题、环境因素(如断电)等原因导致系统不可用。
- 服务器因遭到入侵导致Java程序受到影响,如木马病毒/矿机、劫持脚本等。
- 第三方的RPC远程调用导致的问题
- 作为被调用者提供给第三方调用,第三方流量突增,导致当前程序负载过重出现问题。
- 作为调用者调用第三方,但因第三方出现问题,引发雪崩问题而造成当前程序崩溃。
9、jvm问题排查常用工具
- jdk自带工具
- Jvisualvm
jdk自带的基于图形化界面、可以查看本地及远程的JAVA GUI监控工具。
- 第三方工具
1、arthas
启动运行
功能列表
2、JProfiler
一个重量级的JVM监控工具,提供对JVM精确监控,其中堆遍历、CPU剖析、线程剖析看成定位当前系统瓶颈的得力工具。可以统计压测过程中JVM的监控数据,定位性能问题。
3、MAT(Memory Analyzer Tool)
一款功能强大的Java堆内存分析器。可以用于查找内存泄露以及查看内存消耗情况。只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
10、JVM问题排查步骤
- 查找问题进程:使用top命令查看内存和cpu占用高的java进程: 命令 top -c -p $(pgrep -d',' -f java)
- 查找问题线程:根据cpu占用高的进程确定是哪些线程导致的:top -Hp pid
- 将线程号转为16进制,如线程号19664转为0x2703
- 查看线程堆栈信息
- 手动导出dump文件
- 使用工具MAT、 JProfiler、Jvisualvm打开hprof文件,分析堆内存快照;
- 查看相关问题的源代码,修复
11、常用命令
- top:显示系统整体资源(CPU、内存)使用情况
- netstat:监控网络使用相关信息
- jmap:查看java进程堆内存使用情况或者导出堆内存快照
- jstack:查看java进程里的存活线程的堆栈信息
- jinfo:查看虚拟机配置信息
- jstat:查看JVM运行时信息
- jps:查看java进程
五、JVM常见异常
-
堆溢出
原因
- 代码中可能存在大对象分配
- 可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
解决方法
- 检查是否存在大对象的分配,最有可能的是大数组分配
- 通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
- 如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
- 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
-
元空间溢出
原因
- 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
- 应用长时间运行,没有重启
解决方法
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类
- 重启JVM
GC overhead limit exceeded
原因
- 堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常
解决方法
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
- dump内存,检查是否存在内存泄露,如果没有,加大内存。
方法栈溢出
原因
创建的了大量的线程导致的,通过jstack出来一共8000多个线程。
解决方法
- 通过 -Xss 降低的每个线程栈大小的容量
- 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制
分配超大数组
原因
不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误
解决方法
检查你的代码中是否有创建超大数组的地方
swap溢出
原因
- swap 分区大小分配不足;
- 其他进程消耗了所有的内存
解决方法
- 其它服务进程可以选择性的拆分出去
- 加大swap分区大小,或者加大机器内存大小
本地方法溢出
原因
- 之前的虚拟机方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处
- 通过操作系统本地工具进行诊断,难度有点大
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/jjc/42100.html