jvm
JVM
jvm内存模型
- 堆:java堆是线程共享的一块内存,几乎所有对象实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作。
堆在逻辑上划分为新生代(EdenSpace 区、ServivorFrom区、ServivorTo区)和老年代;
jdk1.8,字符串常量池和静态变量在堆中
- 栈:线程私有,每个方法的执行都会产生一个栈帧,用于存储局部变量表,操作数栈,动态链接和方法出口地址
- 本地方法栈:线程私有,用于管理本地方法的调用,由native修饰
- 程序计数器:线程私有,是一块很小的内存空间,用于记录虚拟机正在执行的线程指令地址
- 方法区:用来存放已经被虚拟机加载的类信息,常量,静态变量等数据。jdk1.7,叫永久代,在堆内存中;jdk1.8之后叫元空间,在本地内存当中
类加载完成后,在java堆中为对象分配内存:
类加载过程及类加载器
1、加载阶段:jvm将字节码文件转为二进制流加载到内存
2、连接
- 验证:验证字节码的正确性
- 准备:对静态变量分配内存并初始化值
- 解析:将符号引用替换为直接引用
3、初始化:开始执行类中的java代码,调用类构造器;给类的静态变量初始化指定的值,执行静态代码块
三种类加载器:
- 启动类加载器(Bootstrap Class Loader):主要加载核心类库
- 扩展类加载器(Extension Class Loader):用来加载java的扩展库
- 应用程序类加载器(Application Class Loader):根据java类的路径来加载类
双亲委派机制
Java中类加载器之间的关系通过双亲委派机制来保证
当一个类加载器接收到类加载的请求,它首先不会尝试自己去加载,而是会将整个请求委派给父类加载器去加载,只有当父类加载器在自己的查找范围内找不到该类时,子类加载器才会去加载整个类。
简单来说就是父加载器优先加载,父加载器无法加载再由子加载器加载。
可以避免重复加载,实现了加载的唯一性
JMM内存模型
本身是一种抽象的概念,是一种规则。并不存在
由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
JMM规则:
- 所有的共享变量(实例变量和类变量)都存在主内存中,局部变量是每个线程私有的
- 每一个线程都存在自己的工作内存,里面存有共享变量的副本
- 线程对变量的读写都必须在工作内存中完成,不能直接读写主内存的变量
变量不可见性的原因:
- 每个线程都有自己的工作内存,线程都是从主内存中拷贝共享变量的副本值
- 每个线程都是在自己的工作内存中操作共享变量
解决方法:
1、加同步锁( synchronized )
2、对共享的变量进行volatile关键字修饰(相当于把这个变量监听起来了,只要发生改变,其它的线程可以立刻知道,每次访问变量时候都会进行一次刷新)
某个线程进入到synchronized代码块前后,执行的过程:
- 线程获得锁
- 清空工作内存
- 从主内存拷贝共享变量最新的值到工作内存
- 执行代码,并将修改后的共享变量副本值刷回主内存
- 线程释放锁
volatile禁止指令重排:
在理论层面,可以把基本的CPU内存屏障分成四种:
LoadLoad:禁止读和读的重排序。
StoreStore:禁止写和写的重排序。
LoadStore:禁止读和写的重排序。
StoreLoad:禁止写和读的重排序。
Unsafe中的方法:
loadFence=LoadLoad+LoadStore
storeFence=StoreStore+LoadStore
fullFence=loadFence+storeFence+StoreLoad
GC
如何判断一个对象是否存活
有两种算法可以判断
- 引用计数法:给对象设置一个引用计数器,当对象被引用时,计数器加1,引用失效时,计数器减1,当引用计数器为0的时候,说明这个对象没有被引用,也就是垃圾对象,等待回收;
缺点:无法解决循环引用的问题,当A引用B,B引用A的时候,此时计数器都不为0,无法垃圾回收
- 可达性分析法
从GC Roots对象开始向下搜索,如果一个对象到GC Roots没有任何的引用链时,说明该对象不可用。
GC Roots有以下几种:线程栈的本地变量、静态变量和本地方法栈的变量等。
当一个对象满足上述条件时,不会马上回收,还要进行两次标记;
- 第一次标记:判断该对象是否有finalize方法并且该方法没有被执行过,如果没有该方法就标记为垃圾对象,等待回收;
- 如果有finalize方法,则进行第二次标记:把该对象放入队列,并且生成一个线程去执行该方法,如果执行了该方法仍然没有和GC Roots的引用,则该对象会被回收
强引用:就是普通对象的引用关系(new的对象),只要强引用还在,垃圾回收器就不会回收被引用的对象
软引用:一些有用但是不是必须的对象。当jvm内存不足时,才会回收软引用指向的对象,jvm会确保抛出OOM异常之前回收调用被软引用指向的对象。软引用通常来实现内存敏感的缓存数据,如果还有空闲内存,就可以暂时保留缓存,如果内存不足时,清理掉缓存,这样就 保证了使用缓存的同时不会耗尽内存。使用SoftReference类来实现软引用。
弱引用:不是必须的对象,比软引用更弱一些。无论当前对象是否充足,都会回收调弱引用关联的对象。使用WeakReference类来实现弱引用。
虚引用:是一种形同虚设的引用,主要用来跟踪对象被垃圾回收的活动。PhantomReference实现
四种垃圾回收算法
标记清除法:
-
标记:可达性算法遍历内存区域,对需要回收的对象打上标记。
-
清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
- 效率低,要遍历两次内存
- 产生大量的不连续的空间碎片,如果需要一块大的内存时,无法找到满足的内存,则会再一次进行GC
复制算法:
- 将内存划分为相等大小的AB两块,每次只使用其中的一块。 当A块使用完了,触发GC时,将A块中还存活的对象复制到B块,然后一次性清理掉A块的内存;下次触发GC时,就将B块存活的对象又复制到A块,然后清理B块;如此循环
优点:
- 相对于标记–清理算法解决了内存的碎片化问题。
- 效率更高(清理内存时,记住首尾地址,一次性抹掉)
缺点:
- 内存利用率不高,每次只能使用一半内存。
改进:(新生代内存分配:Eden(8)幸存区0(1)幸存区1(1),首次GC时,只需要将Eden区存活的对象复制到To,再次GC,将Eden和To复制到From,这样内存的利用率就可以达到90%,大大提高内存利用率
(老年代:存放新生代中经过多次回收仍然存活的对象(默认15次))
标记整理法
:针对老年代(因为新生代存活的对象很多)
- 标记:对需要回收的对象进行标记
- 整理:让存活的对象,向内存的一端移动,然后直接清理没有用的内存
分代收集算法
:当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。
- 新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。
- 老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。
MinorGC和FullGC
MinorGC:发生在新生代的垃圾回收,因为新生代的特点,MinorGC非常频繁,且回收速度比较快,每次回收的量也很大。
-
在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。
-
当Eden区满了的时候,minor garbage 被触发 。
-
经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁。
-
当发生 Minor GC时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制(此处采用标记 - 复制算法)到 to 指向的 Survivor区中,然后交换 from 和 to指针,以保证下一次 Minor GC时,to 指向的 Survivor区还是空的。
-
再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold默认是15),就会被从年轻代Promotion到老年代。
FullGC:发生在老年代的垃圾回收,也称MajorGC,速度比较慢,相对于MinorGc慢10倍左右。进行一次FullGC通常会伴有多次多次MinorGC。
- Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
垃圾回收器
STW: Stop-The-World
: 是在垃圾回收算法执⾏过程当中,将JVM内存冻结丶应用程序停顿的⼀种状态
。
- 在STW 状态下,JAVA的所有线程都是停⽌执⾏的 -> GC线程除外
- 一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
- STW是不可避免的,垃圾回收算法执⾏一定会出现STW,我们要做的只是减少停顿的时间
- GC各种算法优化的重点,就是减少STW(暂停),同时这也是JVM调优的重点。
为什么一定要STW停顿的原因?
-
分析工作必须在一个能确保一致性的快照中进行
-
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
-
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
-
被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉卡顿
-
所以我们要减少STW的发生,也就相当于要想办法降低GC垃圾回收的频率
-
STW状态和采用哪款GC收集器无关,所有的GC收集器都有这个状态,因为要保证一致性。
减少STW(暂停)和降低GC垃圾回收的频率是调优的重点
如果系统卡顿很明显,大概率就是频繁执行GC垃圾回收,频繁进入STW状态产生停顿的缘故
- Serial 单线程新生代复制算法的垃圾回收器;
- SerialOld 垃圾回收器,是一种单线程老年代标记整理算法;
- ParNew 垃圾回收器,是 Serial 的多线程实现,采用复制算法实现;
- Parallel Scavenge 垃圾回收器,是一种高效的多线程复制算法;
- ParallelOld 垃圾回收器,是 Parallel Scavenge 的一种老年代的多线程标记整理算法;
- CMS 垃圾回收器,是一种多线程标记清除算法
- G1 垃圾回收器,是一种高吞吐量的垃圾回收器。
CMS 垃圾回收器
CMS(Concurrent Mark Sweep ) 是一种以获取最短停顿时间为目的的垃圾回收器;采用标记-清除算法
- 初始标记(单线程):暂停所有线程,从来可达性分析来标记对象,这也是 CMS 垃圾回收器第一个 STW 的时候;
- 并发标记:并发标记的时候 GC 线程和用户线程是同时存在的,这里的并发标记是一边产生垃圾一边进行跟着标记,这个过程很难完成,也最耗费时间;
- 并发预清理:这个阶段用户线程和 GC 线程同时运行,GC 线程会进行一下预清理的动作;
- 重新标记(多线程):重新标记这个阶段会暂停用户线程,将上一步并发标记过程中用户线程引起的更新进行修正,这个时间会比初始标记时间长,但是会比并发标记时间短一点;
- 并发清除:在所有需要清理的对象都被标记完过后就会执行最后一步清理的动作。清理的时候用户线程是可以继续运行的,GC 线程只清理标记的区域。在并发清理的过程中也会产生新的垃圾,这个垃圾叫做浮动垃圾,这个浮动垃圾就要等下一次CMS来清理了
缺点:会产生大量的空间碎片;会产生浮动垃圾
当老年代空间不够时,会触发CMS
G1 垃圾回收器
标记-整理算法实现
是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
在JDK1.7版本正式启用,是JDK 9以后的默认垃圾收集器,取代了CMS 回收器。
G1是一个并行回收器,它把堆内存分割为很多不相关的区域(region物理上不连续),把堆分为2048个区域,每一个region的大小是1 - 32M不等,必须是2的整数次幂。使用不同的region可以来表示Eden、幸存者0区、幸存者1区、老年代等。
G1的内存划分
- 所有的Region大小相同,且在JVM生命周期内不会被改变。个region只可能属于一个角色
- 在划分的区域中H区(Humongous)相当于老年代,这表示这些Region存储的是巨大对象(humongous object,H-obj),超过0.5个region, 就放到H。如果一个H装不下,G1会找连续的H位置存储(有时候为了找到,会启动FullGC);
G1的特点:
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,
- 并行性:G1在回收期间,可以有多个GC线程工作;此时用户线程STW
- 空间整合:G1将内存划分为一个个的region。 内存的回收是以region作为基本单位的。复制算法实现
- 可预测的停顿时间模型(即:软实时soft real一time)
G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒、可以通过参数-XX:MaxGCPauseMillis进行设置)
G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1 垃圾回收器的整个实现过程分为四个步骤:
- 初始标记(会SWT):通过可达性分析标记 GC Roots 的直接关联对象
- 并发标记:并发标记是通过 GC Roots 找到存活的对象,这个阶段 GC 线程是与用户线程同时运行的,并且这个阶段的时间比初始标记长;
- 最终标记(会STW):跟CMS的重新标记一样,会短暂暂停用户线程,处理并发标记阶段结束后仍有引用变动的对象
- 清理阶段(会STW):对每个 Region 的回收成本进行排序,根据用户期望的停顿时间来制定收回计划,这也就是可预测的停顿时间模型的体现之处,把决定回收的Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间;
jvm调优
何时进行JVM调优
-
Heap内存(老年代)持续上涨达到设置的最大内存值;
-
Full GC 次数频繁;
-
GC 停顿时间过长(超过1秒);
-
应用出现OutOfMemory 等内存异常;
-
应用中有使用本地缓存且占用大量内存空间;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现