jvm
2025-02-05 20:36:02 0 举报
AI智能生成
jvm总结
作者其他创作
大纲/内容
内存区域
JDK1.7
JDK1.8
程序计数器
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
Java 虚拟机栈
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址
局部变量表
主要存放了编译期可知的各种数据类型,对象引用
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中
动态链接
主要服务一个方法需要调用其他方法的场景
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出
栈中会出现的两种错误
StackOverFlowError
OutOfMemoryError
本地方法栈
和虚拟机栈所发挥的作用非常相似
区别
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
堆
目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
内存分配
对象晋升到老年代的年龄阈值
-XX:MaxTenuringThreshold
只能设置0-15
容易出现错误
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
java.lang.OutOfMemoryError: Java heap space
JDK1.7 字符串常量池和静态变量从方法区永久代移动到了 Java 堆中
方法区
方法区和永久代元空间联系
-XX:MaxMetaspaceSize 标志设置最大元空间大小
-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小
存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
运行时常量池
用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table)
字面量
符号引用
常量池无法再申请到内存时会抛出 OutOfMemoryError
直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的
对象创建
类加载检查
分配内存
指针碰撞
适用场合:堆内存规整(即没有内存碎片)的情况下。
原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表
适用场合:堆内存不规整的情况下。
原理:虚拟机会维护一列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够
大的内存块儿来划分给对象实例,最后更新列表记录
使用该分配方式的GC收集器:CMS
原理:虚拟机会维护一列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够
大的内存块儿来划分给对象实例,最后更新列表记录
使用该分配方式的GC收集器:CMS
内存分配并发问题
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
设置对象头
执行 init 方法
执行 <init> 方法,把对象按照程序员的意愿进行初始化
对象内存布局
对象头(Header)
标记字段(Mark Word)
用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
类型指针(Klass pointer)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
实例数据
程序中所定义的各种类型的字段内容
对齐填充(Padding)
也没有什么特别的含义,仅仅起占位作用
对象访问定位
句柄
直接指针
jvm垃圾回收
内存分配和垃圾回收原则
对象优先在 Eden 区分配
当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,放入Survivor区
无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去
大对象直接进入老年代
长期存活的对象将进入老年代
主要进行GC的区域
部分收集
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集
整堆收集
Full GC,收集整个 Java 堆和方法区
空间分配担保机制
为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间
死亡对象判断方法
引用计数法
每当有一个地方引用它,计数器就加 1;
当引用失效,计数器就减 1;
任何时候计数器为 0 的对象就是不可能再被使用的
当引用失效,计数器就减 1;
任何时候计数器为 0 的对象就是不可能再被使用的
无法解决循环引用的问题,当只有两个对象相互引用,再无其他对象引用时无法判断
可达性分析算法
哪些对象可以作为 GC Roots
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
对象可以被回收,不一定被马上回收
引用类型
强引用
软引用
弱引用
虚引用
判断废弃常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了
判断废弃类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
标记-清除算法
问题
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除后会产生大量不连续的内存碎片
空间问题:标记清除后会产生大量不连续的内存碎片
过程
当一个对象被创建时,给一个标记位,假设为 0 (false);题
在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为 1 (true)
扫描阶段清除的就是标记位为 0 (false)的对象
复制算法
问题
可用内存变小:可用内存缩小为原来的一半。
不适合老年代:如果存活对象数量比较大,复制性能会变得很差
不适合老年代:如果存活对象数量比较大,复制性能会变得很差
标记-整理算法
分代收集算法
垃圾收集器
Serial 收集器
Serial Old 收集器
ParNew 收集器
Serial 收集器的多线程版本
Parallel Scavenge 收集器
新生代采用标记-复制算法,老年代采用标记-整理算法
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法
CMS 收集器
标记-清除算法
过程
初始标记: 短暂停顿,标记直接与 root 相连的对象(根对象);
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫
优点
并发收集、低停顿
缺点
对 CPU 资源敏感;
无法处理浮动垃圾;
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
G1 收集器
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
特点
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念
空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒
步骤
初始标记: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象
并发标记:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量
最终标记: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更
筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度
示意图
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)
类加载
类的生命周期
加载
验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)
使用
卸载
卸载类即该类的 Class 对象被 GC
1.该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
2.该类没有在其他任何地方被引用
3.该类的类加载器的实例已被 GC
2.该类没有在其他任何地方被引用
3.该类的类加载器的实例已被 GC
类加载器
概念
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。每个 Java 类都有一个引用指向加载它的 ClassLoader。数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的
重要的类加载器
BootstrapClassLoader(启动类加载器)
ExtensionClassLoader(扩展类加载器)
AppClassLoader(应用程序类加载器)
双亲委派模型
ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器
执行流程
好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载
打破双亲委派
判断java类是否相同的规则
重要jvm参数
显示指定堆内存
-Xms<heap size>[unit] 最小堆内存
-Xmx<heap size>[unit] 最大堆内存
显示新生代内存
-XX:NewSize=<young size>[unit] :最小新生代
-XX:MaxNewSize=<young size>[unit]:最大新生代
-Xmn<young size>[unit]:新生代分配内存
-XX:NewRatio=<int>:堆内存老年代和新生代的比例
显示指定元空间的大小
-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m)
-XX:MetaspaceSize:Metaspace 由于使用不断扩容到-XX:MetaspaceSize参数指定的量,就会发生 FGC,且之后每次 Metaspace 扩容都会发生 Full GC
垃圾收集器相关
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
GC日志记录
# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
处理oom
-XX:+HeapDumpOnOutOfMemoryError
指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中
-XX:HeapDumpPath=./java_pid<pid>.hprof
表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 <pid> 标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof格式
其他
JDK监控和故障处理工具
jdk命令行工具
jdk可视化分析工具
Jconsole
查看内存信息
线程监控
VisualVM
基于 NetBeans 平台开发,因此他一开始就具备了插件扩展功能的特性,通过插件扩展支持
MAT:内存分析器工具
在遇到 OOM 和 GC 问题的时候,我一般会首选使用 MAT 分析 dump 文件在,这也是该工具应用最多的一个场景
线上问题排查和性能调优
获取dump文件
案例
一次线上 OOM 问题分析
现象:线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。 分析:使用 JDK 自带的jvisualvm分析 dump 文件(MAT 也能分析)。 建议:对于 SQL 语句,如果监测到没有where条件的全表查询应该默认增加一个合适的limit作为限制,防止这种问题拖垮整个系统
生产事故-记一次特殊的 OOM 排查
现象:网络没有问题的情况下,系统某开放接口从 2023 年 3 月 10 日 14 时许开始无法访问和使用。
临时解决办法:紧急回滚至上一稳定版本。分析:使用 MAT (Memory Analyzer Tool)工具分析 dump 文件。
建议:正常情况下,-Xmn参数(控制 Young 区的大小)总是应当小于-Xmx参数(控制堆内存的最大大小),否则就会触发 OOM 错误
临时解决办法:紧急回滚至上一稳定版本。分析:使用 MAT (Memory Analyzer Tool)工具分析 dump 文件。
建议:正常情况下,-Xmn参数(控制 Young 区的大小)总是应当小于-Xmx参数(控制堆内存的最大大小),否则就会触发 OOM 错误
一次大量 JVM Native 内存泄露的排查分析
0 条评论
下一页