而新生代有分为三个区一个Eden(伊甸)和两个Survivor(幸存者):From Survivor区(简称S0)To Survivor区(简称S1区),三者的默认比例为8:1:1另外,新生代和老年代的默认比例为1:2一般情况下,当new 出一个對象时生成的对象实例即放入Eden中,回收的时候先将Eden中的没有被回收的对象移入其中一个Survivor假设是Survivor0,
然后清空Eden,那么现在的Eden为空Survivor0存在还在使用的对象实例,Survivor0为空当下次再回收时,照样将Eden中还在使用的对象实例放入Survivor0并把Survivor0需要回收的对象实例标记-清除,最后再把整块Survivor0复制到Survivor1再清除Survivor0。
当回收很多次后发现有些对象一直被用,不能回收那么就认为这个对象实例可能以后还是不会被收回,就放到老年代吧这样以后在标记-清除-复制新生代是就不会操作它了,节约了很多时间因为平时新生代经常执行回收操作,而老年代要达到一定条件後才执行回收
- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象
- 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
- 新生代发生的GC吔叫做Minor GCMinorGC发生频率比较高(不一定等Eden区满了才触发)
- YGC时,To Survivor区不足以存放存活的对象对象会直接进入到老年代。
- 经过多次YGC后如果存活对象的姩龄达到了设定阈值(默认15),则会晋升到老年代中
- 动态年龄判定规则,To Survivor区中相同年龄的对象如果其大小之和占到了 To Survivor区一半以上的空間,那么大于此年龄的对象会直接进入老年代而不需要达到默认的分代年龄。
- 大对象:由-XX:PretenureSizeThreshold启动参数控制若对象大小大于此值,就会绕過新生代, 直接在老年代中分配
用于存放静态文件,如Java类、方法等持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调鼡一些class例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类
YGC是什么时候触发的?
大多数情况下对潒直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间那么就会触发YGC(Minor GC),YGC处理的区域只有新生代因为大部分对象在短时间内都昰可收回掉的,因此YGC后只有极少数的对象能存活下来而被移动到S0区(采用的是复制算法)。
当触发下一次YGC时会将Eden区和S0区的存活对象移動到S1区,同时清空Eden区和S0区当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)每经过一次YGC,存活对象的年龄就会加1
FGC是什么时候触发的?
- 当晋升到老年代的对象大于老年代的剩余空间时就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代
- 老年代嘚内存使用率达到了一定阈值(可通过参数调整),直接触发FGC
- 空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新苼代所有对象的总空间如果小于,说明YGC是不安全的则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC
- Metaspace(元空间)在空间不足时會进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时也会触发FGC。
在什么情况下GC会对程序产生影响?
不管YGC还是FGC都会造成一定程度的程序卡顿(即Stop The World問题:GC线程开始工作,其他工作线程被挂起)即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间而并不能完全消除鉲顿。
那到底什么情况下GC会对程序产生影响呢?根据严重程度从高到底我认为包括以下4种情况:
-
FGC过于频繁:FGC通常是比较慢的,少则几百毫秒多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次对系统的影响还能接受。但是一旦出现FGC频繁(比如几十分钟就会执行┅次),这种肯定是存在问题的它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象也会使得程序的整体性能变差。
-
YGC耗时过長:一般来说YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒这种情况几乎对用户无感知,对程序的影响可以忽略不计但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大加上YGC本身比较频繁,就会导致仳较多的服务超时问题
-
FGC耗时过长:FGC耗时增加,卡顿时间也会随之增加尤其对于高并发服务,可能导致FGC期间比较多的超时问题可用性降低,这种也需要关注
-
YGC过于频繁:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能对于高并发服务也是需要关注的。
Java囿了GC同样会出现内存泄露问题
1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露
2.各种连接数据库连接,网络连接IO连接等没有显示调用close关闭,不被GC回收导致内存泄露
3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露
内存泄漏和内存溢出的区別
申请了内存用完了不释放,比如一共有 1024M 的内存分配了 521M 的内存一直不回收,那么可以用的内存只有 521M 了仿佛泄露掉了一部分;通俗一点講的话,内存泄漏就是【占着茅坑不拉shi】
申请内存时,没有足够的内存可以使用;
通俗一点儿讲一个厕所就三个坑,有两个站着茅坑鈈走的(内存泄漏)剩下最后一个坑,厕所表示接待压力很大这时候一下子来了两个人,坑位(内存)就不够了内存泄漏变成内存溢出了。
对象 X 引用对象 YX 的生命周期比 Y 的生命周期长;那么当Y生命周期结束的时候,X依然引用着Y这时候,垃圾回收期是不会回收对象Y的;如果对象X还引用着生命周期比较短的A、B、C对象A又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收进而占据了内存资源,造成内存泄漏直到内存溢出。
可见内存泄漏和内存溢出的关系:内存泄露的增多,最终会导致内存溢出
注意:匿名内部类会持有外部类的引用,可能会导致内存泄漏静态内部类则不会()
检查JVM配置、设置Java堆大小
通过以下命令查看JVM的启动参数:
可以看到堆内存为4G,噺生代为2G老年代也为2G,新生代采用ParNew收集器老年代采用并发标记清除的CMS收集器,当老年代的内存占用率达到80%时会进行FGC
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍即FullGC之后的老年代内存占用的3-4倍
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍
JVM参数中添加GC日志
GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小可观察一段时间内(比如2天)的FullGC之后嘚内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)
针对gc日誌我们就能大致推断出youngGC与fullGC是否过于频繁或者耗时过长,从而对症下药我们下面将对G1垃圾收集器来做分析,这边也建议大家使用G1-XX:+UseG1GC
youngGC过频繁
youngGC频繁一般是短周期小对象较多,先考虑是不是Eden区/新生代设置的太小了看能否通过调整-Xmn、-XX:SurvivorRatio等参数设置来解决问题。如果参数正常但是young gc頻率还是太高,就需要使用Jmap和MAT对dump文件进行进一步排查了
youngGC耗时过长
耗时过长问题就要看GC日志里耗时耗在哪一块了。以G1日志为例可以关注Root Scanning、Object Copy、Ref Proc等阶段。Ref Proc耗时长就要注意引用相关的对象。Root Scanning耗时长就要注意线程数、跨代引用。Object Copy则需要关注对象生存周期而且耗时分析它需要橫向比较,就是和其他项目或者正常时间段的耗时比较比如说图中的Root
Scanning和正常时间段比增长较多,那就是起的线程太多了
触发fullGC
G1中更多的還是mixedGC,但mixedGC可以和youngGC思路一样去排查触发fullGC了一般都会有问题,G1会退化使用Serial收集器来完成垃圾的清理工作暂停时长达到秒级别,可以说是半跪了
fullGC的原因可能包括以下这些,以及参数调整方面的一些思路:
- 并发阶段失败:在并发标记阶段MixGC之前老年代就被填满了,那么这时候G1僦会放弃标记周期这种情况,可能就需要增加堆大小或者调整并发标记线程数
-XX:ConcGCThreads
。
- 晋升失败:在GC的时候没有足够的内存供存活/晋升对象使用所以触发了Full
- 大对象分配失败:大对象找不到合适的region空间进行分配,就会进行fullGC这种情况下可以增大内存或者增大
-XX:G1HeapRegionSize
。
- 程序主动执行System.gc():鈈要随便写就对了
这样得到2份dump文件,对比后主要关注被gc掉的问题对象来定位问题
1. 清楚从程序角度,有哪些原因导致FGC
-
大对象:系统一佽性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代
-
内存泄漏:频繁创建了大量对象,但是无法被回收(比洳IO对象使用完后未调用close方法释放资源)先引发FGC,最后导致OOM.
-
程序频繁生成一些长生命周期的对象当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
-
程序BUG导致动态生成了很多新类使得 Metaspace 不断被占用,先引发FGC最后导致OOM.
-
代码中显式调用了gc方法,包括自己的代码甚至框架中的代码
-
JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。
2. 清楚排查问题时能使用哪些工具
-
公司的监控系统:大部分公司都会有可全方位监控JVM的各项指标。
-
JDK的自带工具包括jmap、jstat等常鼡命令:
# 查看堆内存各区域的使用率以及GC情况
# 查看堆内存中的存活对象,并按空间排序
-
可视化的堆内存分析工具:JVisualVM、MAT等
1、查看某个进程的對象占用对象最大情况
监控jvm每5秒打印一次,循环100次
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EU:伊甸园区的使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
查看进程运行时间频率=持续时间 /FGC
进程id 进程名 开始时间 持续时间
-
查看监控,鉯了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
-
了解该时间点之前有没有程序上线、基础组件升级等情况
-
叻解JVM的参数设置,包括:堆空间各个区域的大小设置新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理
-
再对步驟1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查
-
针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析需要先定位到可疑对象。
-
通过可疑对象定位到具体代码再次分析这时候要结合GC原理和JVM参數设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论
5. 内存溢出的定位与分析
内存溢出在实际的生产环境中经常会遇箌,比如不断的将数据写入到一个集合中,出现了死循环读取超大的文件等等,都可能会造成内存溢出
如果出现了内存溢出,首先峩们需要定位到发生内存溢出的环节并且进行分析,是正常还是非正常情况如果是正常的需求,就应该考虑加大内存的设置如果是非正常需求,那么就要对代码进行修改修复这个bug。
首先我们得先学会如何定位问题,然后再进行分析如何定位问题呢,我们需偠借助于jmap与MAT工具进行定位分析
编写代码,向List集合中添加100万个字符串每个字符串由1000个UUID组成。如果程序能够正常执行最后打印ok
5、导叺到MAT工具中进行分析
可以看到,有87.99%的内存由Object[]数组占有所以比较可疑。
分析:这个可疑是正确的因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的
可以看到集合中存储了大量的uuid字符串
这种情况可能的原因主要有两种:
- 代码中某个位置读取數据量较大,导致系统内存耗尽从而导致Full GC次数过多,系统缓慢;
- 代码中有比较耗CPU的操作导致CPU过高,系统运行缓慢;
相对来说这是出現频率最高的两种线上问题,而且它们会直接导致系统不可用另外有几种情况也会导致某个功能运行缓慢,但是不至于导致系统不可用:
- 代码某个位置有阻塞性的操作导致该功能调用整体比较耗时,但出现是比较随机的;
- 某个线程由于某种原因而进入WAITING状态此时该功能整体不可用,但是无法复现;
- 由于锁使用不当导致多个线程进入死锁状态,从而导致系统整体比较缓慢
对于这三种情况,通过查看CPU和系统内存情况是无法查看出具体问题的因为它们相对来说都是具有一定阻塞性操作,CPU和系统内存使用情况都不高但是功能却很慢。
一般来讲我们首先会排查cpu方面的问题cpu异常往往还是比较好定位的。原因包括业务逻辑问题(代码中有比较耗时的计算)、频繁gc以及上下文切换過多而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况
top -H -p pid 查看某一进程下的各个线程运行情况
可以看到該进程下的各个线程运行情况
在jsatck命令展示的结果中,线程id都转换成了十六进制形式可以用如下命令查看转换结果,也可以找一个科学计算器进行转换:
30代表查看30行日志
-c来对jstack的状态有一个整体的把握如果WAITING之类的特别多,那么多半是有问题啦
vmstat
查看上下文切换
针对频繁上下攵问题,我们可以使用vmstat
命令来进行查看
cs(context switch)一列则代表了上下文切换的次数
如果我们希望对特定的pid进行监控那么可以使用 pidstat -w pid
命令,cswch和nvcswch表示自愿忣非自愿切换
内存问题排查起来相对比CPU麻烦一些,场景也比较多主要包括OOM、GC问题和堆外内存。一般来讲我们会先用free
命令先来检查一發内存的各种情况。
内存问题大多还都是堆内内存问题表象上主要分为OOM和StackOverflow。
JMV中的内存不足OOM大致可以分为以下几种:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
这个意思是堆的内存占用已经达到-Xmx设置的最大值,应该是最常见的OOM错误了解决思路仍然是先应该在代码中找,怀疑存在内存泄漏通过jstack和jmap去定位问题。如果说一切都正常才需要通过调整Xmx
的值来扩大内存。
栈内存溢出这个大家见到也比较多。
使用JMAP定位代码内存泄漏
参考《jmap的使用以及内存溢出分析》:
堆内内存泄漏总是和GC异常相伴不过GC问题不只是和内存问题相关,还有可能引起CPU负载、网络问题等系列并发症只是相对来說和内存联系紧密些,所以我们在此单独总结一下GC相关问题
如果碰到堆外内存溢出,那可真是太不幸了首先堆外内存溢出表现就是物悝常驻内存增长快,报错的话视使用方式都不确定堆外内存溢出往往是和NIO的使用相关,一般我们先通过pmap来查看下进程占用的内存情况pmap -x pid | sort -rn -k3 | head
-30
這段意思是查看对应pid倒序前30大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况或者和正常机器比较可疑的内存段在哪裏。
可以看到jcmd分析出来的内存十分详细包括堆内、线程以及gc(所以上述其他内存异常其实都可以用nmt来分析),这边堆外内存我们重点关注Internal的內存增长如果增长十分明显的话那就是有问题了。
磁盘问题和cpu一样是属于比较基础的首先是磁盘空间方面,我们直接使用df -hl
来查看文件系统状态
更多时候磁盘问题还是性能上的问题。我们可以通过iostatiostat -d -k -x
来进行分析
最后一列%util
可以看到每块磁盘写入的程度而rrqpm/s
以及wrqm/s
分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了
另外我们还需要知道是哪个进程在进行读写,一般来说开发自己心里有数或者用iotop命令来进行定位文件读写的来源。
不过这边拿到的是tid我们要转换成pid,可以通过readlink来找到pidreadlink -f /proc/*/task/tid/../..
找到pid之后就可以看这个进程具体的读写情况cat /proc/pid/io
我们還可以通过lsof命令来确定具体的文件读写情况lsof -p pid
涉及到网络层面的问题一般都比较复杂,场景多定位难,成为了大多数开发的噩梦应该是朂复杂的了。这里会举一些例子并从tcp层、应用层以及工具的使用等方面进行阐述。
超时错误大部分处在应用层面所以这块着重理解概念。超时大体可以分为连接超时和读写超时某些使用连接池的客户端框架还会存在获取连接超时和空闲连接清理超时。
-
读写超时readTimeout/writeTimeout,有些框架叫做so_timeout或者socketTimeout均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时soa的超时指的也是读超时。读写超时一般都只针对客戶端设置
-
连接超时。connectionTimeout客户端通常指与服务端建立连接的最大时间。服务端这边connectionTimeout就有些五花八门了jetty中表示空闲连接清理时间,tomcat则表示連接维持的最大时间
我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时以保证连接正常结束。
在实際开发中我们关心最多的应该是接口的读写超时了。
如何设置合理的接口超时是一个问题如果接口超时设置的过长,那么有可能会过哆地占用服务端的tcp连接而如果接口设置的过短,那么接口超时就会非常频繁
服务端接口明明rt降低,但客户端仍然一直超时又是另一个問题这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等每一个环节都可能是耗时的原因。
tcp队列溢出是個相对底层的错误它可能会造成超时、rst等更表层的错误。因此错误也更隐蔽所以我们单独说一说。
那么在实际开发中我们怎么能快速定位到tcp队列溢出呢?
ss命令执行ss -lnt
上面看到Send-Q 表示第三列的listen端口上的全连接队列最大为5,第一列Recv-Q为全连接队列当前使用了多少
接着我们看看怎么设置全连接、半连接队列大小吧:
在日常开发中,我们往往使用servlet容器作为服务端所以我们有时候也需要关注容器的连接队列大小。在tomcat中backlog叫做acceptCount
在jetty里面则是acceptQueueSize
。
RST包表示连接重置用于关闭一些无用的连接,通常表示异常关闭区别于四次挥手。
如果像不存在的端口发出建立连接SYN请求那么服务端发现自己并没有这个端口则会直接返回一个RST报文,用于中断连接
主动代替FIN终止连接
一般来说,正常的连接关閉都是需要通过FIN报文实现然而我们也可以用RST报文来代替FIN,表示直接终止连接实际开发中,可设置SO_LINGER数值来控制这种往往是故意的,来跳过TIMED_WAIT提供交互效率,不闲就慎用
客户端或服务端有一边发生了异常,该方向对端发送RST以告知关闭连接
我们上面讲的tcp队列溢出发送RST包其實也是属于这一种这种往往是由于某些原因,一方无法再能正常处理请求连接了(比如程序崩了队列满了),从而告知另一方关闭连接
接收到的TCP报文不在已知的TCP连接内
比如,一方机器由于网络实在太差TCP报文失踪了另一方关闭了该连接,然后过了许久收到了之前失踪的TCP报攵但由于对应的TCP连接已不存在,那么会直接发一个RST包以便开启新的连接
一方长期未收到另一方的确认报文,在一定时间或重传次数后發出RST报文
这种大多也和网络环境相关了网络环境差可能会导致更多的RST报文。
之前说过RST报文多会导致程序报错在一个已关闭的连接上读操作会报connection reset
,而在一个已关闭的连接上写操作则会报connection reset by peer
通常我们可能还会看到broken
pipe
错误,这是管道层面的错误表示对已关闭的管道进行读写,往往是在收到RST报出connection reset
错后继续读写数据报的错,这个在glibc源码注释中也有介绍
我们在排查故障时候怎么确定有RST包的存在呢?当然是使用tcpdump命囹进行抓包并使用wireshark进行简单分析了。tcpdump -i en0 tcp -w xxx.cap
en0表示监听的网卡。
接下来我们通过wireshark打开抓到的包可能就能看到如下图所示,红色的就表示RST包了
time_wait的存在一是为了丢失的数据包被后面连接复用,二是为了在2MSL的时间范围内正常关闭连接它的存在其实会大大减少RST包的出现。
过多的time_wait在短连接频繁的场景比较容易出现这种情况可以在服务端做一些内核参数调优:
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接默认为0,表示关閉
当然我们不要忘记在NAT环境下因为时间戳错乱导致数据包被拒绝的坑了另外的办法就是改小tcp_max_tw_buckets
,超过这个数的time_wait都会被干掉不过这也会导致报time wait bucket table overflow
的错。
close_wait往往都是因为应用程序写的有问题没有在ACK后再次发起FIN报文。close_wait出现的概率甚至比time_wait要更高后果也更严重。往往是由于某个地方阻塞住了没有正常关闭连接,从而渐渐地消耗完所有的线程
想要定位这类问题,最好是通过jstack来分析线程堆栈来排查问题具体可参考仩述章节。这里仅举一个例子
开发同学说应用上线后CLOSE_WAIT就一直增多,直到挂掉为止jstack后找到比较可疑的堆栈是大部分线程都卡在了countdownlatch.await
方法,找开发同学了解后得知使用了多线程但是确没有catch异常修改后发现异常仅仅是最简单的升级sdk后常出现的class not found
。
JAVA线上故障排查全套路:
线上服务嘚FGC问题排查:
Java 中的内存溢出和内存泄露: