JVM线上问题实战总结
2023-07-13 20:06:24 0 举报
AI智能生成
JVM线上问题实战总结
作者其他创作
大纲/内容
1.内存问题
内存溢出
堆内存溢出
内存溢出
堆内存剩余空间小于该对象需要分配的空间
内存泄漏
对象一直没有被垃圾回收,造成可用的堆内存越来越少
栈内存溢出
栈内存中栈帧过多,栈帧的总内存超过了当前线程的栈内存大小
线程分配过度,导致总的栈内存不足以给下一个线程分配栈内存
方法区+运行时常量池
内存溢出
内存溢出
方法区
存储的字节码文件大小超过了方法区内存大小
动态创建了大量java类,这些类需要被存储到方法区,导致方法区内存不足
常量池内存溢出
程序中动态的创建了大量的基础数据类型和字符串,导致常量池不够分配新创建的常量
本地内存溢出
频繁调用本地方法,创建对象导致本机直接内存不够分配
对象内存分配原则
对象优先在eden区域分配
对象优先在eden区域分配内存
eden区域内存满了,再存放from survive区域
长期存活对象进入年老代
新生代对象经历过一定的垃圾回收次数还存活,就会被放到老年代内存中去
大对象直接进入老年代
内存大小超过一定大小的对象直接放入老年代
相同年纪大小的对象的内存总共超过survive区域内存的一般,那么大于这个年纪的对象都会放在老年代
内存分配担保原则
新生代每一次GC之后,都有可能吧存储不下的对象放在老年代;所以老年代会留出一些空间给这些新生代对象
老年代每次测量进入老年代对象的年纪大小,去评估老年代剩余的空间是否足够装下下次进入年老代的新生代对象,
如果不够,就会做老年代的垃圾回收
如果不够,就会做老年代的垃圾回收
JAVA 内存模型
内存模型概述:Java是跨平台的,需要统一的内存模型来兼容不同的操作系统的差异、硬件差异。
不能因为操作系统硬件的差异导致相同的程序出现不一样的结果。
不能因为操作系统硬件的差异导致相同的程序出现不一样的结果。
主内存+工作内存概述
线程都有自己的工作内存;每一线程的工作内存之间都是相互屏蔽
线程操作变量,都是先通过工作内存,然后复制到主内存;其他线程才能再主内存中访问这个变量;
主内存,是可以被所有线程访问
主内存+工作内存之间数据交互
交互概述
线程的工作内存去访问主内存,获取变量值
内存之间的基本操作都是原子操作,不可再分割
8大原子操作
1.lock,表示主内存中变量已经被某个线程占有
2.unlock,主内存中变量已经被某个线程释放,其他线程可以去获取这个变量
3.read,主内存中读取变量到工作内存
4.load,主内存中读取的值赋予给工作内存中的变量、
5.use,工作内存中的值传递给工作引擎让工作引擎去做操作
6.assign,工作引擎中计算后的值赋值到工作内存中
7.store,工作内存把变量传递给主内存
8.write,工作内存的值存入到主内存中
交互原则
所有的操作必须符合前后逻辑关系
操作之前必须要满足前后依赖关系
配对操作之前含有其他操作关系,但是必须保证操作前后逻辑关系
变量被加锁多少次,就要被释放多少次
内存模型的三大特点
有序性
线程内部一定会按照串行的方式执行指令
线程之间,由于CPU执行权问题,多线程之间执行的任何代码都可能是交叉进行的,除volatile\synchronized
原子性
主内存和工作内存之间的基本操作都是原子操作
可见性
共享变量被一个线程操作,操作后的结果能被其他线程知道
可见性的实现
1.volatile,修饰的变量,修改之后立即从工作内存同步到主内存;实现其他线程对该变量的课件性
3.final,对象的引用是不变的,所以说对所有线程来说都是可见的。
Java先行发生原则
解决的问题是并发情况下,两个操作是否存在冲突的情况;判断数据是否存在并发问题,以及线程是否安全的重要依据
原则
锁定规则:同一个锁,只有被释放之后才能被另一个线程再次占用
读写原则:读写是一对操作,下一个的读操作必定在写操作之后;
对象终结原则:对象被回收之前必须先要被初始化
传递性:a操作优先 与b操作,b操作优先与c操作,那么a操作也先于b操作。
运行时数据区域
程序计时器
作用
java的多线程是通过计算机内核线程来回相互切换的,java的线程执行到了某一步,cpu执行权被切换到其他线程上时候,这个程序计数器的作用来了, 就是去记录它所属的线程执行到了哪一步,哪一个指令,执行权再次切换回来的时候,这个计数器就会帮助线程准确无误的接着切换之前的代码接着执行。
特点
1.是线程所独有的
2.生命周期和线程生命周期相同
3.永远都不会有异常,不存在内存异常情况
本地方法栈
native方法运行的时候用到的内存空间就是本地方法栈
为Java语言调用本地方法,也就是调用native修饰的方法服务的。
方法区
特点:方法区被各个线程所共享
存储内容
也称作永久代,存储的都是经过虚拟机加载之后的字节码文件,类信息,常量池,静态变量
运行时常量池,存储了编译期的各种字面量(字面量都是常量池的一部分)
栈内存
栈帧,特点1:线程所独有,存储的都是临时数据;2.和线程生命周期一样;3.方法在执行的时候栈内存都会去创建这个方法对应的栈帧,栈栈中存储了这个方法的局部变量表,方法返回值,方法出口等等。我们在调用方法的时候通过方法当中嵌套方法,那么栈内存,同样会为这些方法都去创建对应的栈帧,线程去执行这个栈帧(方法),执行完一个栈帧,这个栈帧对应的内存就会被回收,这就是所谓的弹栈。线程永远都只会在栈内存中最上层的栈帧上执行。并且由于前后调用的方法之间存在着返回值的原因,对应的栈内存中的上下两个栈帧之间也并不是完全割裂的,他们需要返回值的传递。每个栈内存最多可以存储1000-2000个栈帧
栈内存大小设置:-Xss128k 给栈内存分配128kb
JVM没有设置总的栈内存大小,操作系统会限制线程的数量,从而达到限制总的栈内存大小
堆内存
堆内存区域划分
新生代
eden
from suvivor
to suvivor
老年代
内存比例
默认的新生代:老年代=1:2
默认eden : from suvivor : to suvivor = 8:1:1
比例大小可以通过JVM参数调整
堆内存作用
JAVA存储对象的主要区域
永久代说明
对内存所说的永久代,只是在jdk1.8版本之前有这个概念,1.8完全摒弃了这个概念,
采用本地硬盘的方式来存储这些数据,有效防止了JAVA永久代内存溢出
采用本地硬盘的方式来存储这些数据,有效防止了JAVA永久代内存溢出
jdk1.7:永久代也属于内存,必须制定大小,大小受限制于所分配的内存大小
jdk1.8:存放在磁盘,可以不指定大小,大小受限制于磁盘
JAVA对象在内存中存储
对象头
mark word:记录内存和锁的状态有关
指向类的指针
数组长度
实例数据
对齐填充字节
拷贝
深拷贝
基础数据类型拷贝值,非基础数据类型,新创建对象,并用老对象给新对象字段赋值;
浅拷贝
基础数据类型拷贝值,非基础数据类型拷贝对应的引用
2.垃圾回收问题
垃圾收集算法
判断对象是否存活算法
可达性算法
过程:GcRoot对象作为起点,向下搜素,搜素走过的路径称为引用链;一个对象到GcRoot没有任何引用链,那么这个对象就是不可达的。
GcRoot对象
1.new出来的对象
2.栈内存中栈帧引用的对象
3.方法区中引用的对象
4.本地方法区中引用的对象
5.软引用、弱引用、虚引用
引用计数算法
概述:给对象添加一个引用计数器,当程序有地方用到这个对象,计数器+1;引用失效就会-1,
任何时候如果引用计数器为0,那么久表示该对象要被回收了
任何时候如果引用计数器为0,那么久表示该对象要被回收了
问题:循环依赖,造成内存泄漏,最后导致内存溢出
垃圾回收算法
分代收集算法
根据新生代、老年代情况的不同,针对新生代,老年代会有不同的垃圾回收算法;
复制算法(新生代)
eden区域和一块存有对象的survior的区域中还存活的对象,会复制到另外一块空闲的survivor区域;
如果这块survivor区域内存大小不够,那么还会放在老年代当中;
如果这块survivor区域内存大小不够,那么还会放在老年代当中;
注意:由于老年代中可以存放新生代的对象,如果此时老年代内存也不够,
就会触发老年代的fullGC,新生代使用复制算法原因:新生代的对象存活率比较低,复制起来成本低
就会触发老年代的fullGC,新生代使用复制算法原因:新生代的对象存活率比较低,复制起来成本低
标记-整理算法(老年代)
老年代不适用复制算法原因:老年代的对象存活率高,如果使用复制算法成本太高,根据可达性算法,把存活的对象会向内存区域的一边做迁移跃迁,最后会有一个迁移末端,末端之外的对象就是需要被回收的对象;
图示
标记-清除算法(老年代)
根据可达性算法需要被回收的对象会被标记,然后对标记的对象的存储回收;
缺点:标记-清除的效率都不高;清除会造成内存碎片,可能导致二级gc
三色标记算法
概述:三色标记算法是一种垃圾回收的标记算法
作用:让JVM不发生或仅短时间发生STW(stop the world),从而达到清除JVM内存垃圾的目的
使用范围:JVM中的CMS、G1垃圾回收器,所使用垃圾回收算法即为三色标记法
三色标记法过程
黑色:代表该对象以及该对象下的属性全部被标记过了。(程序需要使用的对象,不应该被回收)
灰色:对象被标记了,但该对象下的属性未被全部标记。(需要在该对象中寻找垃圾)
白色:对象未被标记(需要被清除的垃圾)
三色标记存在的问题:并发标记时,存在漏标记的情况
综合概述:老年代垃圾回收算法还是选择标记-整理算法
垃圾回收器
新生代垃圾回收器
serial 单线程回收,用户线程需要停止
PN多线程回收,用户线程需要停止
PS 1.多线程回收,用户线程需要停止,2.以高吞吐量标准设计的:用户线程时间/(用户线程时间+垃圾回收时间)
老年代垃圾回收器
serial-old 老年代的单线程垃圾回收,用户线程需停止
PS-old老年代多线程回收,用户线程需需要停止,以高吞吐量为原则设计
CMS多线程回收,以用户线程暂停时间最短为设计标准,回收的时候,用户线程有一段时间不需要停止
G1回收器
特点:
垃圾回收的时候几乎没有stop the world时间
新生代、老年代都可以回收
可将内存分为多个大小相同的region区域,根据区域之间使用标记-整理算法,区域内部使用标记复制算法;
可预测垃圾回收时间
对region区域做选择性回收,回收价值高的region区域
过程
1.初始标记:停顿所有的用户线程,标记各个region区域中能被gcroot关联到的对象
2.并发标记:用户线程和GC线程并行,gc线程根据可达性分析算法找出存活的对象
3.最终标记:停顿用户线程,并发执行gc线程去标记刚才用户线程操作引用对象的那部分内存
4.筛选标记:根据可停顿时间,计算出最优的region区域,并发的对最优的区域进行垃圾回收。(用户线程和gc线程是可以并发的)
分区region
将Java的堆内存分为2048个大小相同的region快
region大小特点
每个region区域的大小都是2的N次幂;即1MB,2MB,4MB
region区域大小在jvm运行期间都不会被改变
每个region区域的大小都是相同的
存储特点
每一个region区域只会属于Eden,survivor、old其中的一种
新增一种新的内存区域,humongous内存区域,超过0.5个region对象就会被放Humongous区域。
Eden、survivor、老年代的区域并不是连续的
三个过程
Young GC
Mixed GC
Full GC
G1回收器缺点
G1回收器本身运行垃圾回收程序时相对cms垃圾回收期需要占有更多的系统CPU、内存资源
在内存小于6g时,cms表现优于G1,大于8g,G1回收器更好,6-8g之间,差不多
相关参数
-XX:G1HeapRegionSize 设置region区域大小
-XX:+UseG1GC 手动设置G1垃圾回收器;jdk1.9默认就是G1
-XX:MaxGcPauseMillis
ZGG
Shenandoah
Epsilon GC
垃圾回收器搭配使用
Serial +old serial
新生代
serial
老年代
old serial
Par new + cms
新生代
par new
老年代
cms
PS + PO
新生代
PS
老年代
PO
垃圾回收器最佳回收内存大小
Serial
几十兆
ps
几个G
cms
20G
G1
上百G
ZGC
4T
垃圾回收器常用参数
Parallel
XX:+UseSerialGc
XX:+SurvivorRatio
XX:+PreTenureSizeThreshold
XX:+SurvivorRatio
XX:+MaxTenuingThreshold
XX:+SurvivorRatio
XX:+ParallelGCThreads
并行回收垃圾线程数量
XX:+UseAdaptiveSizePolicy
自动选择个区大小比例
CMS
G1
通用常数
垃圾回收时间点
线程运行到安全点,安全区域会进行GC信号检查,也就是是否需要做垃圾回收操作;如果需要做,那么线程都会停止
在安全点或者安全区域,等待垃圾回收完成;如果不需要垃圾回收,那么就不会停留在安全点,或者安全区域
在安全点或者安全区域,等待垃圾回收完成;如果不需要垃圾回收,那么就不会停留在安全点,或者安全区域
安全点设置:设置在一些执行时间长的指令上
安全区域设置:相对于安全点来说,就是指令跨度大的安全点
垃圾回收时间
jdk8及以前版本
当Eden区或者S区不够用了
当老年代空间不够用了
当方法区不够用了
jdk9开始
根据垃圾回收器的不同,会有所不同
System.gc()通知JVM进行一次垃圾回收,具体执行还要看JVM,另外在代码中尽量不要用,毕竟GC一次还是很消耗资源的。
GC日志分析
3.JVM调优问题
调优常见问题
1.jdk1.7、jdk1.8、jdk1.9的默认垃圾回收器是什么?
jdk1.7默认垃圾回收器PS(新生代)+PO(老年代)
jdk1.8默认垃圾回收器PS(新生代)+PO(老年代)
Jdk1.9以上G1回收器
2.常见的HotSpot垃圾回收器组合有哪些?
3.所谓的调优,到底在调什么?
提高用户线程吞吐量
提高用户线程相应时间
4.PN+CMS怎么才能让系统基本不产生FullGc
扩大jvm内存
调整内存比例
加大新生代区间比例
提高to survivor区域比例
提高新生代到老年代的年纪
避免代码内存泄漏
5.PS+CMS怎样才能让系统基本不产生FullGc
避免代码内存泄漏
6.G1垃圾回收器是否分代?G1回收器会产生FullGc吗?
不分区收集,在G1回收器概念里面,已经没有新生代,老年代的概念,
所有的堆内存区域划分为不同的区域
所有的堆内存区域划分为不同的区域
会发生FullGC
7.如果G1回收器发生FullGc?
扩大内存
提高CPU性能,可以提高回收效率
降低MixedGC触发的阈值,让MixedGC提早发生
8.生产环境可以随便dump吗?
小的堆内存影响不大;大的堆内存会有服务器卡顿
9.常见OOM问题有哪些?
栈、堆、方法区直接内存溢出
JVM调优实战
线程
CPU经常飙升100%,如何调优
找出哪个进程的CPU高,top命令
找出该进程中哪个线程CPU高 ,top -hp命令
到处该线程的堆栈信息,jstack
工作线程占比和垃圾回收线程的占比对比
假如100个线程很多线程都在等待,
找到持有这把锁的线程
找到持有这把锁的线程
找到java的线程,看有哪些线程正在运行,然后拷贝出来运行的线程id
再去等待线程中,找这些线程等待的是哪个线程,拿出这些线程ID和运行的线程对比,可以找到持有锁的线程
内存
JVM调优目的
目的
吞吐量
响应时间
调优前需要明确是以高吞吐量为要求,还是以响应时间低为要求;还是说满足一定的相应时间的情况下,达到多少的吞吐量
垃圾回收器选择
吞吐量优先:PS+PO回收器,场景数据计算、数据挖掘
追求响应时间:G1回收器,网站\API服务
4.JVM编译
编译过程
词法解析:token流
语法解析
抽象语法树
语义解析
经过注解的抽象语法树
字节码生成器
字节码文件
指令重排
原因:编译器对代码进行空间复杂度、时间复杂度优化,导致了代码实际的操作顺序与书写的不一样
现象:程序执行下面的代码,后执行上面的代码
指令重排原则
重排序的代码没有先后逻辑关系
重排序不影响结果
类加载机制
字节码增强技术
介绍
作用:对JAVA的字节码进行修改,增强功能
操作:修改二进制的class文件
目的:减少冗余代码,提高性能,加密代码
具体实现方式
AspectJ
修改Java字节码工具类
作用时间:编译期
Javassist
修改Java字节码的工作库
可直接生成一个类,在已经编译的类里面添加新方法,修改方法
修改时间:运行期
ASM
ASM是一个java字节码操作框架
直接生成class文件,拦截java文件被类加载器加载之前修改类
修改时间:编译期
APT
双亲委派机制
类加载器的种类
BootstrapClassLoader
加载java核心库,java.*构建extClassLoade,AppClassLoader
ExtClassLoader
加载扩展库,如classpath中的jre,javax.*
AppClassLoader
加载项目代码所在目录的class文件
自定义加载器
用户自定义的类加载器,可加载指定路径的class文件
加载器的区别
双亲委派机制作用
防止类被多个类加载器加载
保证核心的class文件不被篡改
机制
类加载器需要加载类的时候,会先去让他的父类去加载,父类如果没有加载这个类,也没有权限加载,那么久会子类加载,如果有权限加载,那么久会接着让他的父类再做次判断;
流程
三个重要判断
当前类是否已经被加载
当前类加载器是否还有父类加载器
当前类加载器是否有权限加载这个类
SPI机制
SPI是java内置的一种服务发现机制,根据实际需求替换,扩展框架远吗的实现策略
实现过程
调用ServiceLoad.load方法,传入接口字节码对象
根据字节码类型到meta-info包下面找到对应的配置文件
读取配置文件中的类全路径
获取类的全路径,通过反射获取到对象
把对象存储到linkedHashMap中
优缺点
优点
能够让接口更方便找到拓展的实现类
缺点
不能单独获取这个实现类,获取了接口的所有实现类,造成了性能消耗
配置过程
实现某个想要实现的接口
resource包下配置相关的包路径和文件名
文件中配置类的全路径
SPI破坏了双亲委派机制
SPI机制创建对象的类加载器是classload,和传统的类加载器不存在父子关系
对象创建过程
1.降对象的字节码文件加载到内存
2.栈内存给对象分配一个引用变量
3.堆内存开辟一块内存空间
4.给堆内存对象属性做默认初始化赋值
5.给对象属性做显示初始化
6.对代码块进行初始化
1.父类构造代码快
2.当前类的构造代码块
3.父类的构造方法
4.当前类的构造方法
7.将堆内存的地址赋值给栈内存的引用
编译器
jit热点编译
将频繁调用的代码直接加载到内存,无需从硬盘上去加载这些文件
主流编译器
sun公司的hotspot热点编译器
C1 客户端编译器
C2服务端编译器
Graal编译器
其他公司开发的openjdk
0 条评论
下一页