10连JVM问题(进阶)
2022-02-24 17:00:15 0 举报
AI智能生成
JVm
作者其他创作
大纲/内容
4.JVM基础问题
1.垃圾回收器使用算法
1.复制算法
1.适用于新生代,gc回收速度比较快,不会产生内存碎片
2.缺点是空间需要2倍数
2.标记清除算法
步骤
1.标记垃圾对象
2.进行清除
优缺点
1.老年代算法,算法简单
2.会产生内存碎片
3.标记压缩算法
优缺点
1.解决了内存碎片问题
2.因为涉及到标清除以及移动会比较慢
2.垃圾回收使用的算法
1.引用计数法
无法解决重复引用的问题
2.GCROOT法(可达性分析)
从GCROOT的根对象开始向下搜索,搜索到的对象就不是垃圾,反之就是垃圾
1.占内存的 局部变量表引用的变量
2.方法区引用的静态变量、常量
3.本地方法栈上的局部变量
3.TLAB 线程本地分配缓冲区
专门为每个线程再Eden区分配的一块内存,由于是线程内部共享的,没有线程安全问题
缺点是
空间比较小
当线程多了容易积少成多
3.CarTable是什么有什么作用(CMS和G1都用到了卡表记录老年代引用的年轻代对象)
描述
1.因为进行分代回收时,年轻代的对象可能被老年代的对象引用。不能每一次都去扫描一下堆内存,为了提高GC效率,
就将老年代对年轻代对象的引用放到一个卡表中(CarTable)标记为drity,使用写屏障技术(其实就是记录一下)
就将老年代对年轻代对象的引用放到一个卡表中(CarTable)标记为drity,使用写屏障技术(其实就是记录一下)
问题
1.无条件写屏障带来的开销,每一次对象引用的更新都会去写一次
2.高并发下虚拟内存带来了 缓存伪共享问题,需要频繁的进行更新
解决
JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。
5.元空间和栈内存溢出解决
现象
元空间内存内存溢出
java.lang.OutOfmemoryError:MetaSpace
栈内存溢出
java.lang.StackOverflowError
解决方案
1.设置元空间内存大小
-XX:metaspaceSize
-XX:MaxMetaSpaceSize
2.栈内存溢出
1.适当增加-Xss大小
2.查看是否栈的递归深入太深了,设置终止条件
6.年轻代对象进入老年代的条件
1.年轻代的对象达到了年龄(参数:-XX:maxTuringThresold)
2.对象比较大会直接被分配到老年代中,只对serial和parNew设计器有效(-XX:PretenureSizeThreshold=1M )
3.动态年龄,幸存者区满了,有50%的对象年龄大于平均年龄,那么大于的这些对象就会被移送到老年代
4.满足空间担保机制,假如这次MInorGC幸存者区不能存下了,那么就会直接放到老年代
7.垃圾收集器相关
1.重点概念
吞吐量:执行用户程序时间占程序总运行时间的比例(包含GC时间)
高效利用CPU,尽快完成任务
停顿时间:垃圾回收时间短
适用于提成用户交互体验
G1垃圾收集器(是面向服务端的垃圾收集器),优先收集垃圾多的区
背景(之前的垃圾收集器)
1.采用分代垃圾回收的方式,每一个分代的内存空间是连续的
2.没有解决大对象分配的问题,大对象会直接分配到老年代,即使他存活不长,后续会进行频繁的FullGC
3.老年代进行GC时候,必须扫描老年代所有的内存空间
特点
1.采用分区垃圾回收的方式,每一个分区时一个region(1-32M)不等,默认2048个分区
2.有专门存储大对象的区域
3.有mixedGC,当进行老年代垃圾回收时候可以不必每次扫描所有老年代内存空间
4.没有内存碎片
5.支持并发和并行,像CMS支持用线程和GC线程的并发执行,GC线程可以并行执行
区域划分
年轻代区
RmeberSet 记录这个区老年代区对象对其的引用
幸存者区
老年代区
大对象区
回收算法
young GC
采用复制算法,当Eedn区被耗尽后进行GC,会将存活的对象拷贝到幸存者区,或者通过担保机制放到老年代中
年轻代进行GC时候,只会收集年轻代的Region到Cset中进行垃圾回收
mixed GC
支持进行一部分老年代的GC
子主题
会选择部分老年的Region进入Cset中进行回收(为了满足最大停顿时间),可以设置-XX:G1MixedGCLiveThresholdPercent=85%进行控制
fullGC
常用参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=n 设置分区大小
-XX:MaxGcPauseMills=xx(期望GC停顿时间小于这个数字)
-XX:InitiatingHeapOccupancyPercent=n(堆内存占用了多少触发MIXEDGC)
-XX:G1MixedGCLiveThresholdPercent=85% 老年代进入Cset的条件,存活的对象占区域小于多少百分比才进行回收,默认85%
加入Collect set中进行回收
-XX:ConcGCThreads=n(并发进行GC时候使用的线程数)
逃逸分析
通过对对象进行逃逸分析的手段,进而去优化程序
方式
1.通过判断方法中对象是否返回,没有返回就说明这个对象没有逃逸
2.类的变量或者实例变量是否会被其他线程访问到
开启
XX:-DoEscapeAnalysis 新版本默认开启
手段
1.标量替换
标量就是无法再分解的更小的数据,进行JIT编译的时候发现某个对象要是没有被外部引用,就会在栈拆分成若干个成员变量
2.栈上分配
要是对象没有被外部引用,对象可以被直接分配在栈上
3.锁消除
判断某个共享资源是否真的存在并发问题
优点
直接在栈上分配,减少了垃圾回收器的压力
栈上分配,速度会很快
问题
1.对象和数组的创建一定会分配到堆内存上, 不一定
2.加了锁一定会生效吗? 不一定
创建的对象会分配到哪里?
1.栈上分配(有可能,这个对象不会逃逸出这个方法)
判断对象不会逃逸,栈上分配会更快,而且不需要垃圾回收器进行回收
2.TLAB(线程本地分配缓冲)
为什么需要
创建一个线程时,会为线程分配一块内存,后续只给这个线程使用,使用的是Eden区空间
默认开启(-XX:UseTLAB)
优点
主要是提高了堆内存分配的效率
3.Eden区
4.Older区
JIT编译的优化手段
1.方法内联
2.逃逸分析
1.栈上分配
2.锁消除
3.标量替换
8.G1垃圾收集器为什么可以做到让用户设置最大停顿时间
1.Garbage First 主要指的是去回收那些垃圾数量比较大的分区
2.传统的垃圾回收采用分代回收方式,每个分代内存空间是连续的,通常进行FullGc的时候就会去回收整片内存区域
3.G1将堆内存换分成了多个Region区,内存块分别代表着不同的分代例如:Eden区、幸存者区、H大对象区、Older区。将内存划分成了多个小内存块之后,每个内存块回收采用标记压缩算法就会更高效,减少了内存碎片问题
4.另外一方面,通过用户设置最大停顿时间,在进行MixedGc时候可以去回收部分Older区以满足最大停顿时间的要求
9.Jstack的原理
这个工具通常可以用力观察服务的状态、检查是否有死锁和查看热点代码等
1.基于attach api,在tools.jar中能够找到
2.基于sa的实现,放在了sa-jdi.jar中实现
原理
内部应该是建立了一个socket连接,然后将threaddump指令发送给Jvm进程,进行相应的检查或者下载
jvm的Attach Listener线程监听套接字,读取jstack发来的指令,然后将相关的操作扔给VM Thread线程来执行,最后返回给jstack。
1.服务器CPU飙高为100%问题解决
解决方法
1.先定位那个进程占用的CPU较高,TOP命令查看
2.输出堆栈信息 jstack pid > statLog.txt
3.ps -mp pid -o THREAD,tid,time 直接查看高CPU线程或者(top -H -p 13668)
4.找到飙高的线程,将tid转化成16进制 printf "%x" 13668 ,得到3564
5.进入到statLog.txt中查看栈信息,得到最终问题定位
产生原因
1.出现死循环
2.出现死锁
还可以通过jconsole查看线程的死锁
1.定时任务项目要和业务逻辑项目分开部署
3.tomcat并发,线程池所有线程处于运行状态,消耗CPU资源
4.乐观锁
如何避免
2.检查代码死循环
3.比较耗时的接口不要用同步,改成mp
4.服务限流、熔断和降级
2.内存溢出和内存泄漏
区别
内存溢出
申请内存空间,超出了可以分配的大小,导致溢出
内存泄漏
就是有对象无法被回收,导致内存溢出了(未关流、超多的静态变量)
查看某个进程GC的情况
jstat -gcutil pid
解决方式
1.加大内存
2.从编码层面尽量去减少大的局部变量和尽量让变量尽快去进行回收
怎么排查内存溢出问题呢
1.使用Jvisuakvm工具打开,可以dump出堆的heapdump文件
2.使用gceasy.io或者mat工具,这种工具会帮助分析一些没有释放的大对象以及可能导致内存溢出的对象,很清晰
3.生产环境不可能去dump使用,配置堆内存溢出时候打印。后续将这个文件导入到mat或者gceasy中进行分析即可
内存溢出的通常情况
1.循环的创建了大对象
3.JVM性能优化思路
思路
1.通过调整内存。减少老年代进行GC的频率
2.减少STW的时间
核心指标
吞吐量
最大停顿时间
GC次数
方案
1.减少GC的频率和时间
1.Xms和Xmx的设置值要相同
主要原因就是要减少JVM的内存抖动
如果JVM内存小于40%可用,就会进行扩容
如果有70%空余,又会进行缩容
举一反三:将类似成对出现的设置成一致
MaxnewSize和newSize
MaxMetaSpaceSize和MetaSpaceSize
2.少调用System.gc(),因为会触发一次FullGc
3.尽量不要去存放大对象和太多的全局变量,容易触发FUllGC
4.过早晋升老年代的问题
1.可能由于年轻代空间比较小,本应该回收的对象参与了晋升,导致老年带频繁进行GC
1.当IO这样的任务或者对象存活时间不会太长的,就将新生代设置的大一点
2.要是计算比较密集型,对象存活时间比较长,就适当设置老年代空间大一点
5.看下是否存在内存泄漏问题
使用工具进行查看,定位代码问题
2.配合相应的工具和日志打印
1.打印GC的信息
2.打印GC前后对象的年龄分布
3.打印GC前后的内存大小
4.使用jvmsivsul 和gceasy网页工具
2.设置合理的堆内存大小和选择合适的垃圾收集器
1.对于正常的小项目来说,堆内存4G即可,可选用ParNew和CMS垃圾收集器
2.对于并发比较高的项目,堆内存8G,使用G1垃圾收集器,注重吞吐量和低延迟
追问:那么可以无脑采用G1垃圾回收器吗
不可以,因为对于内存比较小的,基本上每次回收都是需要去对所有内存区域进行扫描处理的,由于本身实现算法比较复杂。效果可能还不如parNew+CMS垃圾收集器。因此大内存可以采用G1,小内存没必要用
经常使用的参数
必填的参数
Xms、Xmx
Xss 设置每个线程的堆栈大小,默认线程堆栈大小为1M
当 栈帧个数*单个栈帧大小>设置的单个线程栈内存的大小就发生栈内存溢出
可用栈内存是= 进程最大内存-方法区- 虚拟机本身内存等等。 可用栈内存/每个线程分配的栈内存大小=线程个数
结论1:那么在高并发场景下,可以调整单个线程栈内存大小,提高线程数量
结论2:递归过深怎么优化呢?
该方法去掉递归,改成循环
-XX:+HeapDumpOnOutOfMemoryError、XX:HeapDumpPath=hdpserver_oom.hprof 设置内存溢出的时候打印内存快照
优化参数
1.-XX:PrintGC 每次GC的时候打印GC信息
堆设置
1.-XX:newRatio 新生代和老年代的占比,默认1:2,建议值1:3 -XX:newRatio=3
2.-XX:ServivorRatio 设置新生代的 eden区和幸存者区的占比,默认8:1
3.-Xmn 年轻代的大小,官方推荐的是3/8
4.-XX:maxTenuringThreslod 最大垃圾回收的年龄,设置的大一些,可以让更多对象在年轻代被回收
5.-XX:GcTimeRatio 设置垃圾回收时间占程序运行时间百分比 1/(n+1),n=19 就是5%
6.-Xss 栈空间大小, 现在1m左右,之前版本是256K。相同的物理内存下,可以存放更多的线程。不适合太高。
元空间设置
日志参数
打印GC前后对象的年龄分布,避免过早的出现对象晋升。 打印各个空间回收前后内存情况
注意
1.并不能无脑增加堆内存的大小,当进行GC回收时,内存越大,扫描的范围就越多,GC的时间就越长
2.尽量在年轻代多GC,因为是比较快的。老年代GC需要进行多次扫描,就会很慢
步骤
1.分析dump文件
2.观察gc的时间以及gc次数,修改参数,反复测试
常用工具以及命令
命令
1.jstack pid > a.txt 堆栈信息输入和输出
2.jmap 打印内存快照
3.jstat -gcutil pid 查看这个进程的gc情况
工具
jvmvisul
jconsole
9.CMS垃圾回收器了解
概述
使用的是标记清除算法真正做到了并发
步骤
1.初始标记
标记GCroot上的直接关联对象
2.并发标记
根据标记出来的GcRoot,标记出引用链上的对象
3.重新标记
重新扫描整个堆,将新进入的对象和新生代的对象进行标记
4.并发清除
根据CarTable进行对象清除
优点:
做到了用户线程和来垃圾回收线程并行执行,只有初始标记,重新标记会进行STW,时间很短,提高了效率
缺点
由于采用的是标记清除法,会有浮动垃圾问题
追问:为什么不采用标记整体算法呢?
因为,需要做到用户线程和垃圾回收线程并发,要是标记整体算法清除垃圾后会进行内存移动,需要STW
无法处理大对象的问题,大对象会被分配到老年代。进而会进行频繁fullGC
10.谈一下CMS和G1进行并发标记的三色标记法
描述
主要是进行垃圾回收时候并发收集的算法,并发标记会存在两个问题
1.错标,本来已经标记了,但是现在已经变成了垃圾(浮动垃圾)
2.之前是垃圾,但是现在又有对象引用了
三色标记法
三色
1.白色(还没被检查,或者被检查了没有引用的)
2.灰色(自身进行了检查,成员没进行检查)
3.黑色(自身和成员都进行了检查)
过程
1.所有对象默认都是白色标记,都在白色集合中
2.GC root上直接关联的对象是灰色标记,放入到【灰色集合中】
3. 遍历灰色对象
3.1 将灰色对象关联的对象标记为灰色,放到灰色集合中
3.2 自己本身标记成黑色,放入到【黑色集合中】
4.重复上述步骤,知道灰色集合中没有对象。白色集合中剩下的就是垃圾对象
问题
1.错标(错误的标记成了黑色)
原本已经被标记成了黑色,现在变成了垃圾对象(浮动垃圾)。这种倒是还好,下一次进行回收就行
2.漏标(有用的对象没有标记成黑色)
这种情况比较严重,之前是垃圾,后来又有对象引用了
场景描述:黑色对象引用着灰色对象,对色对象引用着白色对象
此时,灰色对象放弃了对白色对象的引用,黑色对象引用了白色对象
最终,白色对象就被回收了,发生了问题
解决
1.CMS (写屏障+增量更新)
思路就是:将黑色引用白色的对象变成灰色,然后再去遍历
2.G1(写屏障+快照的方式[SATB])
上一种方式会有再去遍历灰色对象的问题,G1在并发清除阶段进行了解决
会记录一个快照,当灰色对象解除对白色对象的引用,就会把这个快照放到栈中。最后,在【最终标记】阶段,对【STAB】快照中的对象进行处理
补充:这也是G1比CMS垃圾回收高效率的一点,倘若G1采用【增量更新】的方式,还需要将灰色对象再次进行遍历,因为三个标记法要求最终,灰色集合为空
3.ZGC(读屏障)
11.什么情况进行FULLGC
1.系统调用System.gc
2.老年代空间不足
3.CMS有个参数可以设置老年代内存超过这个比例之后,出发CMS回收。-XX:CMSInitiatingOccupancyFraction
4.出发了担保机制,就是当新生代GC时候发现幸存者空间也存不下后,会去判断老年代是否有连续空间可以存下。老年代空间可以存下就正常GC,不能存下就会发生FullGC,进行回收
12.GC日志分析
设置参数:-XX:PrintGC、-XX:PRintGcDetails 、-XX:PrintGcTimeStamps 、-Xloggc:./gc.log设置gc文件路径
内容:CommandLine:Jvm参数信息、每一行的GC时间,启动多久触发GC、触发GC原因(对象分配失败)、触发的是什么代的垃圾回收以及GC前后情况是什么样的(GC前、GC后、可用空间)、本次GC耗费时间
程序结束后会打印,各个内存区域使用的占比情况以及具体大小和内存地址范围
0 条评论
下一页