jvm总结
2023-03-25 22:04:03 2 举报
AI智能生成
jvm相关技术总结
作者其他创作
大纲/内容
堆内存剩余空间小于该对象需要分配的空间
内存溢出
对象一直没有被垃圾回收,造成可用的堆内存越来越少
内存泄露
堆内存溢出
1.栈内存中栈帧过多,栈帧的总内存和超过了当前线程的栈内存大小
2.线程分配过度,导致总的栈内存不足以给下一个线程分配栈内存;
栈内存溢出
存储的字节码文件大小超过了方法区内存大小
动态创建了大量java类,这些类需要被存储到方法区,导致方法区内存不够
1.方法区
程序中动态的创建了大量的基础数据类型和字符串,导致常量池不够分配新创建的常量
2.常量池内存溢出
方法区+运行时常量池内存溢出
频繁调用本地方法,创建对象导致本机直接内存不够分配
本机内存溢出
对象优先在eden区域分配内存
eden区域内存满了,再存放from suvivor区域
对象优先在eden区域分配
新生代对象经历过一定的垃圾回收次数还存活,就会被放到老年代内存中去;
长期存活对象进入老年代
内存大小超过一定大小的对象直接放入老年代
相同年纪大小的对象的内存综合超过survivor区域内存一半,那么大于这个年纪的对象都会放在老年代
大对象直接进入老年代
新生代每一次gc之后,都有可能把存储不下的对象放在老年代;所以老年代会留出一些空间给这些新生代
老年代每次测量进入老年代对象的年纪大小,去做评估预测,老年代剩余空间是否能足够装下下次进入老年代的新生代对象,如果不够,就会做老年代的垃圾回收
内存分配担保原则
对象内存分配原则
java是跨平台的,需要同一的内存模型来兼容不同不同的操作系统的差异,硬件差异等等。不能因为操作系统硬件的差异导致相同的程序出现不一样的结果
内存模型概述
线程都有自己的工作内存;每个线程的工作内存之间都是相互屏蔽;
线程操作变量,都是先通过工作内存,然后复制到主内存;其他线程才能再主内存中访问这个变量;
主内存,是可以被所有的线程访问。
主内存+工作内存概述
1.线程的工作内存去访问主内存,获取变量值
2.内存之间的基本操作都是原子操作,不可再分割
1.交互概述
8大原子操作
所有的操作必须符合前后逻辑关系
操作之前必须要满足前后依赖关系
配对操作之前含有其他操作关系,但是必须保证操作前后逻辑关系
变量被加锁多少次,就要被解锁多少次
交互原则
主内存+工作内存之间数据交互
线程内部:线程一定会按照串行的方式去执行指令
线程之间:由于cpu的执行权问题,多线程之间执行的任何代码都可能是交叉进行的,除了volatile,synchronized
理解
有序性
主内存和工作内存之间的基本操作都是原子操作
原子性
共享变量被一个线程操作,操作后的记过能被其他线程直到
volatile修饰的变量,修改的之后会立即从工作内存同步到主内存之中;实现其他线程对该变量的可见性
volatile
解锁之前必须把变量的值从工作内存传递到主内存
synchronized
对象的引用是不变的,所以说对所有线程来说都是可见的
final
java可见性实现
可见性
内存模型三大特点
并发情况下,两个操作是否存在冲突的情况;判断数据是否存在并发问题,以及线程是否安全的重要依据
解决问题
1.锁定规则:同一个锁,只有被释放之后才能被另外一个线程再次占用;
2.读写原则:读写是一对操作,下一的读操作必定在写操作之后;
3.对象终结原则:对象被回收之前必须先要被初始化
4.传递性:a操作优先于b操作,b操作优先于c操作,那么a操作也有限与b操作
具体原则
java先行发生原则
java内存模型
java的多线程是通过计算机内核线程来回相互切换的,java的线程执行到了某一步,cpu执行权被切换到其他线程上时候,这个程序计数器的作用来了, 就是去记录它所属的线程执行到了哪一步,哪一个指令,执行权再次切换回来的时候,这个计数器就会帮助线程准确无误的接着切换之前的代码接着执行。
作用
1.是线程所独有的
2.生命周期和线程周期相同
不存在内存异常的情况
3.永远都不会有异常
特点
程序计数器
native方法运行的时候用到的内存空间就是本地方法栈
为java语言调用本地方法,也就是调用native修饰的方法服务的。
本地方法栈
方法区被各个线程所共享
也称作永久代,存储的都是,经过虚拟机加载之后的字节码文件,类的信息,常量池,静态变量
运行时常量池,存储了编译期的各种字面量(字面量都是常量池的一部分)
存储内容
方法区
栈帧
线程所独有,存储的都是临时数据
和线程生命周期一样
3.方法在执行的时候栈内存都会去创建这个方法对应的栈帧,栈栈中存储了这个方法的局部变量表,方法返回值,方法出口等等。我们在调用方法的时候通过方法当中嵌套方法,那么栈内存,同样会为这些方法都去创建对应的栈帧,线程去执行这个栈帧(方法),执行完一个栈帧,这个栈帧对应的内存就会被回收,这就是所谓的弹栈。线程永远都只会在栈内存中最上层的栈帧上执行。并且由于前后调用的方法之间存在着返回值的原因,对应的栈内存中的上下两个栈帧之间也并不是完全割裂的,他们需要返回值的传递。每个栈内存最多可以存储1000-2000个栈帧。所以说。
给栈内存分配128kb
-Xss128k
栈内存大小设置
操作系统会限制线程的数量,从而达到限制总的栈内存大小
JVM没有设置总的栈内存大小
栈内存
eden
from suvivor
to suvivor
新生代
老年代
默认的新生代:老年代=1:2
默认eden:from suvivor:to suvivor=8:1:1
比例大小可以通过jvm参数调整
内存比例
堆内存区域划分
java存储对象的主要区域
堆内存作用
堆内存所说的永久代,只是在jdk1.8版本之前有这个概念,1.8就完全摒弃了这个概念,采用本地硬盘的方式来存储这些数据,有效防止了java这个永久代内存溢出。
永久代也是属于内存,必须制定大小,大小受限制与所分配的内存大小
jdk1.7
存放在磁盘,可以不指定大小,大小受限制与磁盘
jdk1.8
永久代说明
堆内存
运行时数据区域
对象的hashCode值
分代年纪
锁的标记位
是否偏向
无锁
偏向线程的id
偏向锁时间戳
偏向锁
指向锁的指针
轻量级锁,重量级锁
标记位
Gc标记
记录内容和锁的状态有关
Mark Word
指向类的指针
数组长度
对象头
实例数据
对齐填充字节
分为三部
Java对象在内存中存储
基础数据类型拷贝值, 非基础数据类型,新创建对象,并用老对象给新对象字段赋值;
深拷贝
基础数据类型拷贝值,非基础数据类型拷贝对应的引用
浅拷贝
拷贝
内存
GcRoot对象作为起点,向下搜索,搜索走过的路径称为引用链;一个对象到GcRoot没有任何引用链,那么这个对象就是不可达的。
可达性算法过程
new出来的对象
栈内存中栈帧引用的对象
方法区中引用的对象
本地方法区中引用的对象
软引用,弱引用,虚引用
GcRoot对象
可达性算法
给对象添加一个引用计数器,当程序有地方用到这个对象,计数器+1;引用失效就会-1,任何时候如果引用计数器为0,那么就表示该对象要被回收了
概述
循环依赖,造成内存泄露,最后导致内存溢出
问题
引用计数算法(被废弃了)
判断对象是否存活算法
根据新生代,老年代情况的不同,针对新生代,老年代会有不同的垃圾回收算法;
分代收集算法
eden区域和一块存有对象的survivor的区域中还存活的对象,会复制到另外一块空闲的survivor区域;如果这块survivor区域内存大小不够,那么还会放在老年代当中;
注意:由于老年代中可以存放新生代的对象,如果此时老年代内存也不够,就会触发老年代的fullgc
新生代使用复制算法原因:新生代的对象存活率比较低,复制起来成本低
复制算法(新生代)
回收前
回收后
复制算法示意图
老年代不适用复制算法原因:老年代的对象存活率高,如果使用复制算法成本太高
根据可达性算法,把存活的对象会向内存区域的一边做迁移跃动,最后会有一个迁移末端;末端之外的对象就是需要被回收的对象;
标记-整理算法(老年代)
标记-整理示意图
根据可达性算法需要被回收的对象会被标记,然后堆标记的对象的存错回收;
1.标记-清除的效率都不高
2.清除会造成很多内存碎片,有可能导致二级gc;
缺点:
标记-清除算法(老年代)
标记-清除算法示意图
三色标记算法是一种垃圾回收的标记算法
让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的
JVM中的CMS、G1垃圾回收器 所使用垃圾回收算法即为三色标记法。
使用范围
黑色:代表该对象以及该对象下的属性全部被标记过了。(程序需要用到的对象,不应该被回收)
灰色:对象被标记了,但是该对象下的属性未被完全标记。(需要在该对象中寻找垃圾)
白色:对象未被标记(需要被清除的垃圾)
三色标记法过程
并发标记的时候,存在漏标的情况
三色标记存在问题
三色标记算法
综合概述:老年代垃圾回收算法还是选择标记-整理算法
垃圾回收算法
垃圾收集算法
单线程回收,用户线程需要停止
serial
多线程回收,用户线程需要停止
PN
以高吞吐量标准设计的:用户线程时间/(用户线程时间+垃圾回收时间)
PS
新生代垃圾回收器
老年代的单线程垃圾回收,用户线程需停止
serial-old
老年代多线程回收,用户线程需要停止
以高吞吐量为原则设计:
PS-old
多线程回收,以用户线程暂停时间最短为设计标准
回收的时候,用户线程有一段时间不需要停止
CMS
示意图
CMS垃圾回收器工作流程图
老年代垃圾回收器
垃圾回收的时候几乎没有stop the world 时间
新生代,老年代都可以回收
可将内存分成很多歌大小相同的region区域,根据区域之间使用标记-整理算法,区域内部使用标记复制算法;
很重要的一个特点
可预测垃圾回收时间
对region区域做选择性回收,回收价值高的region区域
1.初始标记:停顿所有的用户线程,标记各个region区域中能被gcroot关联到的对象。
2.并发标记:用户线程和gc线程并行,gc线程根据可达性算法找出存活的对象。
3.最终标记:停顿用户线程,并发执行gc线程去标记刚才用户线程操作引用对象的那部分内存。
4.筛选标记:根据可停顿时间,计算出最优的region区域,并发的对最优的区域进行回收(这个时候用户线程和gc线程是可以并发的)。
垃圾回收过程
垃圾回收过程示意图
将java的堆内存分成2048个大小相同的region块
region 区域大小在jvm运行期间都是不会被改变
每个region区域的大小都是相等的
region大小特点
每一个region区域只会属于Eden, Survivo,old其中的一种
Eden, Survivo,老年代的区域并不是连续;
新增一种新的内存区域,Humongous内存区域,超过0.5个region对象就会被放Humongous区域
存储特点
region特点
分区region
Young gc
mixed gc
FGc
三个过程
最早出现在jdk7;jdk9以后就是默认的垃圾回收器
G1回收器历史
G1回收器缺点
设定region 区域大小
-XX :G1HeapRegionSize
手动设置G1垃圾回收器;JDK9默认就是G1
-XX:+UseG1GC
-XX:MaxGCPauseMillis
相关JVM参数
G1回收器
停顿时间不超过10ms;
垃圾回收器停顿时间,不会随着活跃对象增加而增加
支持8MB-16TB堆内存
ZGC设计目标
jdk11引入
JDK15正式发布
ZGC历史
已经没有分代收集的概念了
只能运行在64位操作系统上
ZGC相对以往垃圾回收器特点
也会像之前的回收器一样,将内存划分成很多个小的块
固定为2MB
用于存放256KB以下的小对象
小页面
固定位32MB
用于存放256KB以上,4MB以下的对象
中页面
大小不固定,但必须是2MB的2次幂的大小
只会存放大于或者等于4MB的大对象,每一个大页面,只会存储一个大对象
大页面
具体划分
ZGC内存空间划分
统一内存访问
cpu都整合到同一个芯片上,各个cpu对于内存的访问会出现争抢,会导致内存访问瓶颈;为了解决这个,将内存和cpu集成在一个单元上,这个就是非统一内存访问
支持非统一内存访问
内存访问方式
在NUMA内存访问模式下,cpu访问本地存储器比访问非本地存储器速度快一些;
分配速度很快,收回也很快
ZGC支持NUMA,会优先将小页面分配在本地内存,本地内存不够,然后再从远端的内存进行分配。
中页面,大页面分配不会分配在cpu的本地内存
ZGC支持NUMA
标记阶段
回收阶段
重定位
处理过程
染色指针?
小页面优先回收,中页面,大页面尽量不回收
回收策略
回收过程示意图
1、多线程并发标记
2、多线程再次并发标记,处理中间并发阶段(用户线程GC线程同时运行)时候,遗漏的对象
1、将原来活跃的对象复制到新的内存空间上,并将原来的内存空间回收;如果发现某一个页全部是垃圾对象,直接全部回收该区域
2、重定位,新的地址值换到原来的对象上面
转移阶段
具体过程
回收过程
ZGC垃圾回收
当对象的内存地址被转移的时候,刚号在并发阶段应用程序有需要访问这个对象
使用场景
JVM向应用代码插入一小段代码的技术
相当于有两个操作要一起做的原子操作
解释
1、对已经转移但是还没重定位的对象进行对象的重定位
2、删除对应对象再转发表中的记录的指针的新旧关系
过程
读屏障
jvm启动预热,如果从未发生过GC,那么就会在堆内存超过10%,20%,30%的时候触发一次GC,来收集GC的数据
预热规则
ZGC根据近期对象的分配速度以及GC时间,计算当内存占用达到什么样的阈值的时候出发下一次GC
基于分配速率的自适应算法
流量平稳的时候,自适应需要堆内存占用达到百分之95的时候才会触发
基于固定时间间隔
和固定时间规则类似。
主动触发
内存已经无法再给新的对象分配内存触发
内存分配不够
代码中直接System.gc()触发
外部触发
元数据区不足时导致触发
元数据分配触发
ZGC的GC时机
估算当前的堆内存分配速率,速度估计越快,GC来的越早,速度估算越慢,GC来的越迟
ZAllocationSpikeTolerance
定 GC 发生的间隔,以秒为单位触发 GC
ZCollectionInterval
GC触发时机
Stw阶段,GC线程的数量
ParallelGCThreads
并发阶段,cpu的数量
ConcGCThreads
GC线程
ZGC调优
ZGC
将垃圾回收的停顿时间控制在10ms以内
与G1回收器的设计目标一直:低延时为主要目标
设计目标
jdk12版本以及以后版本
由redhat公司开发
Shenandoah开发使用了很多G1回收器的代码
历史
基于region的内存布局
标记阶段都是并发标记
Shenandoah与G1相同点
在最终的回收阶段,采用的是并发整理,由于和用户线程并发执行,因此这一过程不会造成STW,这大大缩短了整个垃圾回收过程中系统暂停的时间
默认情况下不使用分代收集,也就是Shenandoah不会专门设计新生代和老年代,因为Shenandoah认为对对象分代的优先级并不高,不是非常有必要实现
采用“连接矩阵”代替记忆集。在G1以及其他经典垃圾回收器中均采用了记忆集来实现跨分区或者跨代引用的问题,每个Region中都维护了一个记忆集,浪费了很多内存,且导致系统负载也更重,「因此在Shenandoah中摒弃了这种实现方式,而是采用连接矩阵来解决跨分区引用的问题」
Shenandoah与G1不同点
Shenandoah
JDK11正式
只负责内存分配,不负责内存回收
工作
性能测试
内存压力测试
虚拟机接口测试
极短寿命的工作
极端延迟敏感的应用
-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC
参数配置
Epsilon GC
old serial
serial +old serial
par new
par new + CMS
PO
PS +PO
垃圾回收器搭配使用
几十兆
几个G
20G
上百G
G1
4T
垃圾回收器最佳回收内存大小
XX:+UseSerialGc
XX:+SurvivorRatio
XX:+PreTenureSizeThreshold
XX:+MaxTenuingThreshold
并行回收垃圾线程数量
XX:+ParallelGCThreads
自动选择各区大小比例
-XX:+UseApaptiveSizePolicy
Parallel
使用标记清除算法
XX:+UseConcMarkSweepGC
-XX:ParallelCMSThreads=n
-XX:CMSInitialingOccupancyFraction
-XX:+UseCMSCompactAtFullCollection
设置几次FullGC后进行一次碎片整理
-XX:CMSFullGCsBeforeCompaction=n
允许对类 元数据进行回收
span style=\
-XX:CMSInitiatingPermOccupancyFraction=n
-XX:+UseCMSinitiatingOccupancyOnly
gc时间占用运行时间比例
-XX:GCTimeRatio
停顿时间,是一个建议时间,GC会尝试使用各种手段达到这个时间,比如减小年轻代
-XX:MaxGcPauseMills
新生代调整young区域的块的个数达到这个值
新生代回收时间大小
-XX:+MaxGCPauseMillis
GC间隔时间
-XX:+MaxGCPauseIntervalMillis
分区大小,size越大,垃圾回收时间越长,GC间隔时间长,也会导致每次GC时间长
-XX:+G1HeapRegionSize
新生代最小比例,默认5%
-XX:G1NewSizePercent
新生代最大比例,默认60%
-XX:G1MaxNewSizePercent
GC时间的建议比例,G1会根据这个值调整整个堆内存大小
线程数量
-XX:ConcGcThreads
启动G1的堆内存占用比例
-XX:InitiatingHeapOccupancyPercent
使用tlab
-XX:UseTLAB
打印tlab情况
-XX:PrintTLAB
设置tlab大小
-XX:TLABSize
通用常数
垃圾回收器常用参数
垃圾收集器
线程运行到安全点,安全区域会进行gc信号检查,也就是是否需要做垃圾回收操作;如果需要做,那么线程都会停止在安全点或者安全区域,等待垃圾回收完成;如果不需要垃圾回收,那么就不会停留在安全点,或者安全区域
设置在一些执行时间长的指令上
安全点设置
相对于安全点来说,就是指令跨度大的安全点;
安全区域设置
垃圾回收时间点
当Eden区或者S区不够用了
当老年代空间不够用了
当方法区不够用了
子主题
jdk8及以前版本
根据垃圾回收器的不同,会有所不同
System.gc()(通知jvm进行一次垃圾回收,具体执行还要看JVM,另外在代码中尽量不要用,毕竟GC一次还是很消耗资源的)
jdk9开始
垃圾回收时间
GC日志分析
垃圾回收
jdk1.7 默认垃圾收集器PS(新生代)+PO(老年代)
jdk1.8 默认垃圾收集器PS(新生代)+PO(老年代)
PN + CMS
2、常见的HotSpot垃圾回收器组合有哪些?
1、提高用户线程吞吐量
2、提高用户线程相应时间
3、所谓调优,到底是在调什么?
扩大JVM内存
加大新生代区间比例
提高去Survivor区域比例
提高新生代到老年代的年纪
调整内存比例
避免代码内存泄露
4、PN +CMS 怎样才能让系统基本不产生FGC
5、PS +PO 怎样才能让系统基本不产生FGC
不分代收集,在G1回收器概念里面,已经没有新生代,老年代的概念,所有的堆内存区域划分为不同的区域
会发生FGC
6、G1回收器是否分代?G1回收器会产生FGC吗?
1、扩大内存
2、提高CPU性能(可以提高回收效率)
3、降低MixedGC 触发的阈值,让MixedGc提早发生
7、如果G1回收器发生FGC?
小的堆内存影响不大;大的堆内存会有服务器卡顿
8、生产环境可以随随便便dump吗?
栈,堆,方法区直接内存溢出
9、常见OOM问题有哪些?
调优常见问题
top 命令
找出哪个进程的CPU高
top -hp命令
找出该进程中哪个线程cpu高
jstack
导出该线程的堆栈信息
线程线程占比和垃圾回收线程的占比对比
解决步骤
案例1、
1、找到java的线程,看有哪些线程正在运行,然后拷贝出来运行的线程的唯一标记
2、再去等待线程中,找这些线程等到的是哪个线程,拿出这些线程id和运行的线程对比,,可以找到持有锁的线程;
假如100个线程很多线程都在等待,找到持有这把锁的线程
案例2、
线程
文档加载斤内存中行程的java对象导致内存不足,频繁GC,导致stw时间过长
1、为啥原网站很慢
从PS垃圾回收器,设置成PN+CMS 或者G1回收器
2、内存加大之后更卡顿
案例1
线程池使用不当,导致OOM
案例3、
jvm调优实战
1、吞吐量
1、减少垃圾回收时候的stop the world的时间
2、响应时间
调优前需要明确是以高吞吐量为要求,还是以响应时间低为要求;还是说满足一定的相应时间的情况下,达到多少的吞吐量
PS+PO回收期
数据计算
数据挖掘
场景
吞吐量优先
网站
API
追求响应时间
垃圾回收器选择
JVM调优的目的
jvm性能调优
token流
词法解析
抽象语法树
语法解析
经过注解的抽象语法树
语义解析
字节码文件
字节码生成器
编译过程
编译器对代码进行空间复杂度时间复杂度的优化,导致了代码实际的执行顺序和书写的不一样;
原因
程序先执行下面的代码,后执行上面的代码
现象
重排序的代码没有先后逻辑关系
重排序不影响结果
指令重排原则
指令重排
对java的字节码进行修改,增强功能
修改二进制的class文件
操作
减少冗余代码,提高性能,加密代码
目的
修改java字节码工具类
作用时间:编译期
AspectJ
修改java字节码的工具库
可以直接生成一个类
在已经编译的类里面添加新方法,修改方法
功能
修改时间:运行期
Javassist
ASM是一个java字节码操作框架
直接生成class文件
拦截java文件被类加载器加载之前修改类
修改时间:编译期
ASM
APT
实现方式
字节码增强技术
加载java核心库 java.*构建ExtClassLoade,AppClassLoader
BootstrapClassLoader
加载扩展库,如classpath中的jre ,javax.*
ExtClassLoader
加载项目代码所在目录的class文件
AppClassLoader
自定义加载器
加载器的区别
类加载器种类
1.防止类被多个类加载器加载
2.保证核心的class文件不被篡改
双亲委派机制作用
类加载器需要加载类的时候,会先去让他的父类去加载;父类如果没有加载这个类,也没有权限加载,那么就会子类加载;如果有加载权限,那么就会接着让他的父类再做次判断;
双亲委派机制
当前类是否已经被加载
当前类加载器是否还有父类加载器
当前类加载器是否有权限加载这个类
三个重要判断
双亲委派机制流程
JAVA内置一种服务发现机制
spi
根据实际需求替换,扩展框架源码的实现策略
调用ServiceLoad.load方法,传入接口字节码对象
根据字节码类型到meta-info包下面找到对应的配置文件
读取配置文件中的类全路径
获得类的全路径,通过反射获取到对象
把对象存储到linkedHashMap中
spi实现过程
能够让接口更方便找到拓展的实现类
优点
不能单独获取这一个实现类,获取了接口的所有实现类,造成了性能消耗
缺点
spi优缺点
实现某个想要实现的接口
resource包下配置相关的包路径和文件名
文件中配置类的全路径
spi配置过程
spi机制创建对象的类加载器是classLoad,和传统的类加载器不存在父子关系
spi机制破坏了双亲委派机制
spi机制
1.将对象的字节码文件加载进内存
2.栈内存给对象分配一个引用变量
3.堆内存开辟一块内存空间
4.给堆内存对象属性做默认初始化赋值
5.给对象属性做显示初始化
1.父类构造代码块
2.当前类的构造代码块
3.父类的构造方法
4.当前类的构造方法
6.对代码块进行初始化
7.将堆内存的地址赋值给栈内存的引用
对象创建过程
类加载机制
将频繁调用的代码直接加载进内存,无需从硬盘上去加载这些文件
jit热点编译
客户端编译器
C1
服务端编译器
C2
Graal 编译器
从jdk10之后,JIT有三款
这三款编译器都是热点编译器,jdk17已经删除了Graal 编译器
sun公司hotspot
还有其他公司开发的,也就是其他有自己开发的openjdk
主流编译器
编译器
jvm编译
jvm总结
0 条评论
回复 删除
下一页