JVM垃圾回收
2025-03-26 23:21:47 0 举报
AI智能生成
JVM(Java虚拟机)垃圾回收(Garbage Collection, GC)是自动管理内存的过程,用于识别和清除不再被程序引用的对象,释放这些对象占用的内存空间,以供其他对象使用。垃圾回收机制的核心目标是回收堆内存中无用的内存空间。它采用多种算法和策略,如标记-清除(Mark-Sweep)、复制(Copying)、标记-整理(Mark-Compact)和分代收集(Generational Collection)等。垃圾回收器的执行可以是同步的,也可以是并发的,这影响了它对应用程序性能的影响。为了优化垃圾回收的效率,Java虚拟机还包括了对内存堆的多区域划分,如年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,Java 8 之后被元空间 Metaspace 替代)。在JVM垃圾回收过程中,程序员通常可以根据应用程序的特点和需求,对垃圾回收算法进行选择和调整,以便更好地控制内存管理。
作者其他创作
大纲/内容
堆内存
可达性算法
GC roots
类的静态变量引用
方法的局部变量引用
新生代
Eden
S1
S2
老年代
对象迁入规则
新生对象默认进入Eden区
通过JVM设置,大对象可直接进入老年代
Eden区满,即触发Minor GC
触发前检查
新生代对象大小是否大于老年代可分配空间大小
是
空间担保机制未开启
直接触发Full GC
空间担保机制已开启
每次Minor GC后进入老年代的平均值是否大于老年代可分配空间大小
是
直接触发Full GC
否
尝试进行Minor GC
否
尝试进行Minor GC
GC后的三种情况
1.存活对象 < S区大小
直接进入S区
动态年龄判断规则
1n + 2n + 3n > 50%S区大小,3n以上对象迁入老年代
15次Minor GC后依旧存活的对象迁入老年代
2.S区大小<=存活对象大小<=老年代可分配大小
进入老年代
3.存活对象大小 > 老年代可分配大小
内存溢出 OOM
JVM参数含义
-XX:InitialHeapSize
初始堆大小
-XX:MaxHeapSize
最大堆大小
-XX:NewSize
初始新生代大小
-XX:MaxNewSize
最大新生代大小
-XX:PermSize
元数据大小(即永久代)
-XX:MaxPermSize
元数据最大大小
-XX:MaxTenuringThreshold
最大存活年龄
默认躲过15次Young GC的对象升入老年代
-XX:PretenureSizeThreshold=10485760
大对象阈值
-XX:SurvivorRatio=2
Eden:Survivor:Survivor的比例为2:1:1
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
每次Full GC后都整理一下内存碎片。
-XX:+CMSParallelInitialMarkEnabled
在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
-XX:+CMSScavengeBeforeRemark
在CMS的重新标记阶段之前,先尽量执行一次Young GC
-XX:MaxGCPauseMills
G1执行GC的时候最多可以让系统停顿多长时间
默认200ms
-XX:G1NewSizePercent
设置新生代初始占比的
默认5%
-XX:G1MaxNewSizePercent
设置新生代最大占比的
默认60%
-XX:InitiatingHeapOccupancyPercent
老年代占比达到以后触发Mix GC
默认45%
-XX:G1MixedGCCountTarget
混合回收中,最后一个阶段执行几次回收
默认8次
-XX:G1HeapWastePercent
执行回收时,空闲Region达到一定程度,即停止垃圾回收
默认5%
-XX:G1MixedGCLiveThresholdPercent
回收Region时,这个Region中必须存活对象低于一定程度时才回收
默认85%
-XX:+DisableExplicitGC
禁止显示执行GC,不允许通过代码触发GC
如System.gc()
-XX:+HeapDumpOnOutOfMemoryError
OOM时dump一份内存快照
-XX:HeapDumpPath=/usr/local/app/oom
OOM时生成的内存快照的存放位置
-XX:+PrintGCDetils
打印详细的gc日志
-XX:TraceClassLoading
-XX:TraceClassUnloading
追踪类的加载和卸载的情况,通过日志打印JVM中加载了哪些类,卸载了哪些类
Tomcat的catalina.out日志文件中查看
-XX:+PrintGCTimeStamps
打印出来每次GC发生的时间
-Xloggc:gc.log
设置将gc日志写入一个磁盘文件
GC
Minor GC/ Young GC
算法
复制算法
触发时机
1.Eden分区占满
2.Full GC后附带执行一次Minor GC
Full GC / Old GC
算法
标记清除算法 + 整理
整理
整理时stop the world
JVM参数设置整理时机,默认Full GC后马上整理,避免内存碎片问题
JVM参数设置何时整理,默认每次Full GC都进行整理
触发时机
1.老年代分区占满/达到预设的92%
2.空间担保未开启时,Minor GC执行前,新生代的对象大小大于老年代剩余可分配大小时
3.空间担保开启时,Minor GC执行前,老年代剩余可分配空间小于平均每次Minor GC后升入老年代的对象大小
Mix GC
垃圾回收器
新生代
Serial
ParNew
Parallel Scavenge
老年代
Serial Old
CMS
Parallel Old
G1垃圾回收器
Serial收集器
**模式**:单线程STW(Stop-The-World)
**场景**:客户端应用/单CPU环境
ParNew收集器
**特性**:多线程版Serial收集器
**搭配**:常与CMS收集器协同工作
Parallel Scavenge
**目标**:吞吐量优先(用户代码时间占比)
**适用**:后台计算型应用
CMS收集器
**目标**:最小停顿时间
**四阶段**:
1. 初始标记(STW)
2. 并发标记
3. 重新标记(STW)
4. 并发清除
**缺陷**:内存碎片/CPU敏感
G1收集器
**革新**:Region分区管理
**特点**:
可预测停顿时间模型
混合回收(新生代+老年代)
**四阶段**:
1. 初始标记(STW)
2. 并发标记
3. 最终标记(STW)
4. 筛选回收
其他收集器
**Serial Old**:Serial的老年代版
**Parallel Old**:Parallel Scavenge的老年代搭档
JVM调试工具
jstat
jstat -gc
S0C
这是From Survivor区的大小
S1C
这是To Survivor区的大小
S0U
这是From Survivor区当前使用的内存大小
S1U
这是To Survivor区当前使用的内存大小
EC
这是Eden区的大小
EU
这是Eden区当前使用的内存大小
OC
这是老年代的大小
OU
这是老年代当前使用的内存大小
MC
这是方法区(永久代、元数据区)的大小
MU
这是方法区(永久代、元数据区)的当前使用的内存大小
YGC
这是系统运行迄今为止的Young GC次数
YGCT
这是Young GC的耗时
FGC
这是系统运行迄今为止的Full GC次数
FGCT
这是Full GC的耗时
GCT
这是所有GC的总耗时
jstat -gccapacity PID
堆内存分析
jstat -gcnew PID
年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity PID
年轻代内存分析
jstat -gcold PID
老年代GC分析
jstat -gcoldcapacity PID
老年代内存分析
jstat -gcmetacapacity PID
元数据区内存分析
jmap
jmap -histo PID
按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。
jmap -dump:live,format=b,file=dump.hprof PID
命令生成一个堆内存快照放到一个二进制文件里去
jhat
jhat dump.hprof -port 7000
jhat内置了web服务器,他会支持你通过浏览器来以图形化的方式分析堆转储快照
浏览器上访问当前这台机器的7000端口号,就可以通过图形化的方式去分析堆内存里的对象分布情况了
线上JVM监控
low
高峰期和低峰期都用jmap、jhat做下检查
定时每天、每周去看
监控系统
Zabbix
OpenFalcon
Ganglia
一份JVM参数模板
-Xms4096M
-Xmx4096M
-Xmn3072M
-Xss1M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFaction=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
-XX:+PrintGCDetails
-Xloggc:gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
JVM和性能优化
1、Java内存区域
虚拟机的历史
未来的Java技术一览
运行时数据区域
各个区域的作用
程序计数器
栈
堆
方法区
运行时常量池
各个版本内存区域的变化
1.6
1.7
1.8
直接内存
站在线程角度来看堆和栈
深入辨析堆和栈
方法的出入栈
栈桢
栈上分配
虚拟机中的对象
分配过程
内存布局
对象的访问定位
堆参数设置和内存溢出实战
Java堆溢出
新生代配置
方法区和运行时常量池溢出
虚拟机栈和本地方法栈溢出
本机直接内存溢出
2、垃圾回收器和内存分配策略
GC概述
判断对象的存活
辨析强、弱等各种引用
GC算法
标记-清除算法
复制算法
标记-整理算法
分代收集
Stop The World现象
GC日志解读
内存分配与回收策略
内存泄漏和内存溢出辨析
JDK为我们提供的工具
了解MAT
垃圾回收器
垃圾回收器概览
垃圾回收器工作详解
G1详解
未来的垃圾回收
3、JVM的执行子系统
Class类文件本质
Class文件格式
格式详解
字节码指令
类加载机制
加载过程详解
类加载器
自定义类加载对类进行加密和解密
系统的类加载器
双亲委派模型
Tomcat类加载机制
栈桢详解
方法调用详解
基于栈的字节码解释执行引擎
基于栈的指令集与基于寄存器的指令集
分析代码在虚拟机中的执行情况
5、深入了解性能优化
常用的性能评价/测试指标
响应时间
并发数
吞吐量
相互之间的关系
常用的性能优化手段
总原则
前端优化手段
应用服务性能优化
存储性能优化
详细了解应用服务性能优化
缓存
缓存的基本原理和本质
合理使用缓冲的准则
分布式缓存与一致性哈希
集群
异步
同步和异步,阻塞和非阻塞
常见异步的手段
应用相关
代码级别
并发编程
资源的复用
JVM
与JIT编译器相关的优化
GC调优
JVM调优实战
存储性能优化
4、编写高效优雅Java程序
构造器参数太多怎么办?
不需要实例化的类应该构造器私有
不要创建不必要的对象
避免使用终结方法
使类和成员的可访问性最小化
使可变性最小化
优先使用复合
接口优于抽象类
可变参数要谨慎使用
返回零长度的数组或集合,不要返回null
优先使用标准的异常
用枚举代替int常量
将局部变量的作用域最小化
精确计算,避免使用float和double
当心字符串连接的性能
对象
对象的创建
根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出ClassNotFoundException;
检查符号引用对应的类是否加载过
如果没有,则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
从堆中划分一块对应大小的内存空间给该对象
指针碰撞
java堆内存空间规整的情况下使用
Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
空闲列表
java堆空间不规整的情况下使用
对象中的成员变量赋上初始值
设置对象头信息
调用对象的构造函数进行初始化
对象的内存布局
对象头
Mark Word
对象的hashCode
CG年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC标志
Class Metadata Address
指向对象实例的指针
对象的实例数据
对齐填充
对象的访问方式
指针
reference中存储的直接就是对象地址
句柄
java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息
两种方式的比较
使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot虚拟机使用的是直接指针访问的方式。
class文件结构
魔数
唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
版本号
常量池
字面量
符号引用
访问标志
用于识别一些类或接口层次的访问信息
是否final
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
类索引,父类索引,接口索引集合
这三项数据主要用于确定这个类的继承关系。
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类父类的全限定名
接口索引
描述这个类实现了哪些接口
字段表集合
表结构
访问标志
名称索引
描述符索引
属性表集合
字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
简单来说,字段表集合存储字段的修饰符+名称
变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
方法表集合
访问标志
名称索引
描述符索引
属性表集合
Java代码经过编译器编译为字节码之后,存储在方法属性表集合中一个名叫“Code”的属性中
属性表集合
在 Class 文件、字段表、方法表都可以携带子机的属性表集合,以用于描述某些场景专有的信息。
类加载机制
类生命周期
加载
验证
准备
解析
初始化
使用
卸载
类加载器
启动类加载器
C++ 实现,是虚拟机自身的一部分;
负责将存放在 \lib 目录中的类库加载到虚拟机内存中
其他加载器
由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
分类
自定义类加载器
用户根据需求自己定义的。也需要继承自ClassLoader.
应用程序类加载器
它负责加载用户类路径(ClassPath)上所指定的类库
扩展类加载器
它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中
双亲委派模型
内容
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
实现
首先检查类是否被加载;
若未加载,则调用父类加载器的loadClass方法;
若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当前类加载器调用findClass加载类;
若父类加载器可以加载,则直接返回Class对象;
好处
保证java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题。
破坏
基础类需要调用用户的代码
解决方式
线程上下文类加载器
也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原
实现方法
重写ClassLoader类的loadClass()
示例:
JDBC
原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类
JNDI服务需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码
重写loadClass()方法
双亲委派模型的具体实现就在loadClass()方法中
用户对程序的动态性的追求
例如OSGi(面向Java的动态模型系统)的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
代码热替换、模块热部署
类加载过程
加载
将编译后的.Class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到
验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
解析
将class文件的常量池的符号引用替换为直接引用的过程(是静态链接)。
可能发生在始化阶段之前,也可能发生在初始化阶段之后,后者是为了支持 Java 的动态绑定。
初始化
为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序(执行()方法)。
类加载方式
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
类加载时机
遇到new,getStatic,putStatic,invokeStatic这四条指令
new一个对象时
set或者get一个类的静态成员变量(除去那种被final修饰放入常量池的静态字段)
调用一个类的静态方法
直接访问 static 数据时
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
垃圾收集算法
标记-清楚算法
复制算法
标记-整理算法
分代收集算法
对象已死吗?
引用计数算法
可达性分析算法,GCRoots对象包括:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。
强引用,软引用,弱引用,虚引用
方法区的垃圾回收,判断‘无用的类’:该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading(可在Product版虚拟机使用)、-XXTraceClassUnLoading(需要FastDebug版虚拟机支持)查看类加载和卸载信息。
OOM分析
Metaspace内存溢出
原因
1.Metaspace内存大小直接用的默认或者设置的太小
2.cglib等技术动态生成类,控制不良,导致生成类过多,导致Metaspace塞满
定位
GC日志 - > MAT内存快照分析
虚拟机栈内存溢出
原因
一个线程里不停的调用方法,入栈-出栈,不停地消耗栈内存(一般设置1M)
方法递归
public static void sayHello(String name){
sayHello(name);
}
定位
系统日志打印异常信息
java.lang.StackOverflowError
堆内存溢出
原因
1.系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,继续放入新对象实在放不下了,即引发OOM导致系统崩溃
2.系统有内存泄漏的问题,就是莫名其妙弄了很多对象,结果都是存活的,没有及时取消他们的引用,导致GC无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了
即要么负载太高,要么有内存泄漏问题
定位
JVM设置OOM溢出时导出快照
日志文件错误信息捕捉
MAT分析快照
先看占用内存最多的是什么对象
然后分析线程的调用栈
然后看是哪个方法引起的,接着优化代码即可
堆外内存溢出
原因
堆外内存申请使用,长时间未释放
进入了老年代,但是一直没触发Full GC导致堆外内存一直不能释放
定位
日志分析
java.lang.OutOfMemoryError: Direct buffer memory
MAT分析快照
奇葩问题
Tomcat参数设置导致OOM
max-http-header-size
请求头大小
设置了为10M,每个Tomcat线程运行时生成两个10M的byte数组,每个线程运行4s,直接导致OOM
Tomcat源码中定义了,每次调用会创建两个byte[]保存请求和响应
堆外内存溢出
Jetty通过NIO使用堆外内存,系统JVM参数设置新生代过小老年代很大,导致堆外内存引用进入老年底,长时间未触发Full GC导致一直无法释放,导致堆外内存OOM
NIO设计时考虑了GC问题,在NIO执行结束后会调用System.gc()方法,显示释放内存,不过我们一般的JVM参数设置会禁止System.gc()这种方法调用触发GC,导致无法释放触发OOM
自研RPC框架序列化问题导致OOM
通信双方定义传输参数格式,然后接到参数后进行反序列化(转换成byte[])。进行后续逻辑处理
服务A修改参数格式未通知服务B,导致解析时一直失败,这里RPC框架设计的一个机制是
解析失败的话会把解析失败的这个byte[]放到内存中,等待后续分析
想法是好的,但是这个内存大小设置为了4GB,这就导致了在服务B不感知参数格式变化的情况下,A不停调用,B不停解析失败放到内存中
直至占用打到4GB后直接触发OOM
查看日志是有效判断OOM种类的方法
元数据溢出:java.lang.OutOfMemoryError: Metaspace
栈内存溢出: java.lang.StackOverflowError
堆内存溢出:java.lang.OutOfMemoryError: Java heap space
堆外内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
性能调优
根据应用类型选择收集器组合
平衡吞吐量/停顿时间/内存占用
监控GC日志分析回收效率
内存异常
`OutOfMemoryError`:堆/方法区溢出
`StackOverflowError`:栈深度超标
垃圾回收算法
引用计数法
**原理**:通过引用计数器跟踪对象引用
**缺陷**:无法处理循环引用问题
标记-清除算法
**两阶段**:标记存活对象 → 清除未标记对象
**问题**:内存碎片化严重
标记-整理算法
**改进策略**:标记后移动存活对象到内存一端
**优势**:避免内存碎片
**代价**:对象移动时间开销
复制算法
**内存划分**:等分为两个半区
**特点**:空间换时间,内存利用率50%
运行时数据区
Java堆(Java Heap)
是JVM所管理的内存中最大的一块,被所有线程共享
几乎所有对象实例都在这里分配内存
内存不足时抛出`OutOfMemoryError`异常
方法区(Method Area)
存储类信息、常量、静态变量、即时编译代码
包含运行时常量池(字面量和符号引用)
虚拟机栈(JVM Stack)
线程私有,存储栈帧(局部变量表/操作数栈/动态链接/方法出口)
可能抛出`StackOverflowError`和`OutOfMemoryError`
本地方法栈
为Native方法服务
异常类型与虚拟机栈相同
程序计数器
线程私有,指示下一条字节码指令地址
执行native方法时值为undefined
0 条评论
下一页