自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JVM堆外內(nèi)存導(dǎo)致的FGC問題排查

開發(fā) 后端
Java虛擬機(jī)定義了程序執(zhí)行期間使用的各種運(yùn)行時數(shù)據(jù)區(qū)域。其中一些數(shù)據(jù)區(qū)域是在Java虛擬機(jī)啟動時創(chuàng)建的,只有在Java虛擬機(jī)退出時才會被銷毀,這部分線程共有。其他數(shù)據(jù)區(qū)域?yàn)槊總€線程。每線程數(shù)據(jù)區(qū)域在創(chuàng)建線程時創(chuàng)建,在線程退出時銷毀,也就是線程私有。

問題發(fā)現(xiàn)

服務(wù)在線上環(huán)境頻繁的Full GC。把相關(guān)運(yùn)行時數(shù)據(jù)區(qū)的監(jiān)控打開,發(fā)現(xiàn)堆外內(nèi)存一直在上升。

圖片

我使用的版本是 java8,jvm廠商是orcale hotspot,垃圾回收器使用的CMS+ParNew。

我使用的jvm參數(shù)是:

-Xmx6g
-Xms6g
-XX:NewRatio=1
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:MaxTenuringThreshold=6
-XX:+ParallelRefProcEnabled
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+heapDumpOnOutOfMemoryError
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/export/Logs/gc.log

為了明確排查方向,需要研究堆外內(nèi)存都具體有什么東西。于是我翻看了jvm的虛擬機(jī)規(guī)范。解讀如下:

Java虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)

Java虛擬機(jī)定義了程序執(zhí)行期間使用的各種運(yùn)行時數(shù)據(jù)區(qū)域。其中一些數(shù)據(jù)區(qū)域是在Java虛擬機(jī)啟動時創(chuàng)建的,只有在Java虛擬機(jī)退出時才會被銷毀,這部分線程共有。其他數(shù)據(jù)區(qū)域?yàn)槊總€線程。每線程數(shù)據(jù)區(qū)域在創(chuàng)建線程時創(chuàng)建,在線程退出時銷毀,也就是線程私有。

運(yùn)行時數(shù)據(jù)區(qū)分為以下幾個部分:

1、PC寄存器(The pc Register)

每個線程一個,以保存當(dāng)前執(zhí)行指令的地址。一旦執(zhí)行了指令,PC寄存器將用下一條指令更新。

2、虛擬機(jī)棧( Java Virtual Machine Stacks)

每個Java虛擬機(jī)線程都有一個私有Java虛擬機(jī)堆棧,與線程同時創(chuàng)建。虛擬機(jī)棧存儲棧幀,它保存局部變量和部分結(jié)果。

虛擬機(jī)??赡軙霈F(xiàn)Java虛擬機(jī)將拋出StackOverflowerError。

3、堆(Heap)

Java虛擬機(jī)線程之間共享堆,堆只有一個。堆是為所有類實(shí)例和數(shù)組分配內(nèi)存的運(yùn)行時數(shù)據(jù)區(qū)域。這也是我們創(chuàng)建的對象放置的區(qū)域。是最大的,最需要調(diào)優(yōu)的地方。

堆是在虛擬機(jī)啟動時創(chuàng)建的。對象的堆存儲由垃圾收集器回收;對象永遠(yuǎn)不會顯式解除分配。

如果計算需要的堆超過了自動存儲管理系統(tǒng)的可用堆,Java虛擬機(jī)會拋出OutOfMemoryError。

4、方法區(qū)(Method Area)

存儲所有類級別的數(shù)據(jù),包括靜態(tài)變量所有線程共享。Java虛擬機(jī)只有一個方法區(qū)。存儲的有類結(jié)構(gòu),例如運(yùn)行時常量池、字段和方法數(shù)據(jù),以及方法和構(gòu)造函數(shù)的代碼,包括類和實(shí)例初始化以及接口初始化中使用的特殊方法。

5、運(yùn)行時常量池(Run-Time Constant Pool)

運(yùn)行時常量池是類文件中常量池表的每類或每接口運(yùn)行時表示形式。它包含多種常量,從編譯時已知的數(shù)字文本到必須在運(yùn)行時解析的方法和字段引用。運(yùn)行時常量池的功能類似于傳統(tǒng)編程語言的符號表,盡管它包含比典型符號表更廣泛的數(shù)據(jù)范圍。

這段我抄的,為了保持完整性,運(yùn)行時常量池其實(shí)是方法區(qū)的一部分。

6、本地方法棧(Native Method Stacks)

存儲本地方法信息,線程私有。

整體結(jié)構(gòu)表示如下?


圖片


問題:方法區(qū)和元空間有什么關(guān)系?

簡單理解,方法區(qū)是java的定義,而元空間則是hotspot虛擬機(jī)在1.8及其以后的實(shí)現(xiàn)。在1.7之前叫永久代(永久代還包含了部分老年對象),如果使用java8的話忽略永久代就行了。

根據(jù)jvm的規(guī)范,方法區(qū)內(nèi)存儲的都是jvm類級別的數(shù)據(jù),包括什么構(gòu)造方法,什么常量池什么的。那什么操作會使得這方面一直在上漲呢?帶著問題,一步步搞唄。

簡單嘗試

首先先定死m(xù)etaspce的大小,不讓他動態(tài)擴(kuò)容,因?yàn)樵臻g每次調(diào)整大小都會進(jìn)行一次full gc。

jvm啟動參數(shù)新增。

-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m

但是發(fā)現(xiàn)并沒有用。

是否能從堆看出些端倪?

堆外內(nèi)存,沒有特別好的查看方法。我決定還是把堆內(nèi)存dump下來看看,看能否通過堆內(nèi)存,看出一些貓膩來。

將堆dump下來進(jìn)行分析。

使用命令 jps 找到j(luò)ava進(jìn)程pid,指定生成文件的path。

jmap -dump:file=/path ${pid}

dump完畢后。

借助工具進(jìn)行查詢 首先使用mat,官方網(wǎng)站:https://www.eclipse.org/mat/。

圖片圖片

這邊看到了很多Netty的PoolThreaCache。

聯(lián)想到netty使用了直接內(nèi)存,是否和這個有關(guān)呢?

為此查詢了大量資料,找到了一個參數(shù):-Dio.netty.maxDirectMemory 。

這個參數(shù)大概意思是調(diào)整netty堆外內(nèi)存,通過它有三個取值,無論調(diào)成什么都沒辦法阻止堆外內(nèi)存的上漲。其實(shí)在這就有點(diǎn)無頭蒼蠅亂撞了。

確實(shí),只有兩種情況會導(dǎo)致netty相關(guān)的堆外內(nèi)存上漲。

1、要么是netty有bug 。

2、要么是使用方法不對。

netty有bug,這個可能性就算了吧。使用的版本也不是最新的,也沒有直接引用netty包,都是通過例如http-client或者rpc框架引入的netty。

使用方法不對?在http client或者rpc服務(wù)的部分代碼排查了一遍,基本上都是比較簡單的用法,并沒有直接設(shè)置很怪的參數(shù),或者很非常規(guī)的操作。

在這就確實(shí)在堆里面找不到有用的線索了。

找到原因

貌似確實(shí)沒轍了。

隨后我請教了我司的超級大佬:森哥。森哥給我要了相關(guān)權(quán)限后,上去機(jī)器一頓操作。推測可能是C2 Compiler或者什么即時編譯導(dǎo)致的問題,因?yàn)槎淹舛际莏vm級別的數(shù)據(jù),常規(guī)的排查確實(shí)比較難找到線索。

聽完后聯(lián)想到堆外不就是方法區(qū)嗎,我用的java8 hotspot虛擬機(jī),也就是元空間了。

代碼里面會有什么導(dǎo)致元空間上漲呢?

元空間是存儲jvm級別的數(shù)據(jù),是否有很多類加載?

帶著這個猜想,找到相應(yīng)的參數(shù) -verbose:class,這個會將類加載全部打印出來。

如下圖:

圖片圖片

發(fā)現(xiàn)有非常多的ASMAccessorImpl_,而且是不會停止,一直在加載。

厚禮蟹,這就查到了原因。

那ASM是什么,如果研究過spring,就知道在aop擴(kuò)展動態(tài)生成字節(jié)碼,最底層其實(shí)就是ASM生成的,其實(shí)是一個字節(jié)碼編輯框架。官網(wǎng):https://asm.ow2.io/。

也就是說,我的代碼有一個地方一直在動態(tài)生成類字節(jié)碼,加載到方法區(qū)。從而導(dǎo)致堆外內(nèi)存一直在上漲,從而導(dǎo)致full gc。

代碼修改

那怎么定位到是哪段代碼?

這個簡單,打開idea,double shift,調(diào)search everywhere。

圖片

排查到是mvel這個依賴框架生成的。

關(guān)于mvel,其實(shí)是spel差不多,表達(dá)式解析引擎。在項(xiàng)目中,mvel的使用我們只用了兩行代碼。

MVEL.executeExpression()
MVEL.compileExpression()

然后我們也有把編譯完的進(jìn)行緩存,按道理說不會一直生成類的。因?yàn)閙vel這個框架實(shí)在是相關(guān)文檔太少,沒人維護(hù)的感覺,抱著死馬當(dāng)活馬醫(yī)的態(tài)度,去github上提一個issue,然后自己同時接著排查。

圖片

幸運(yùn)的是這個框架還沒死絕,還有人回復(fù)。

大概意思是說,我問為什么使用你們的mvel會導(dǎo)致我jvm出現(xiàn)oom錯誤(頻繁的full gc),另外如果說每次編譯相同的內(nèi)容的話,為什么沒有框架層面緩存起來?;卮鹫f是需要自己緩存的。

也就是我的代碼還是緩存失效了。

找到緩存的那一行,使用的是map,用key去查找的時候,發(fā)現(xiàn)用的是contains,而沒有用containsKey。這就導(dǎo)致了永遠(yuǎn)查不到,也就導(dǎo)致了永遠(yuǎn)會重新編譯。

圖片

經(jīng)過修改后,問題得以解決。

圖片

一條平平的線,并且沒有full gc,皆大歡喜

圖片

總結(jié)

堆外內(nèi)存有點(diǎn)難搞,難以和代碼聯(lián)系起來。提供一個思路:可通過-verbose:class查看類加載的情況,然后具體分析。

責(zé)任編輯:姜華 來源: 凱哥的Java技術(shù)活
相關(guān)推薦

2020-08-27 21:36:50

JVM內(nèi)存泄漏

2017-01-11 14:02:32

JVM源碼內(nèi)存

2022-06-15 16:04:13

Java編程語言

2020-05-09 13:49:00

內(nèi)存空間垃圾

2019-12-17 10:01:40

開發(fā)技能代碼

2014-02-27 13:30:26

CacheLinux系統(tǒng)內(nèi)存不足

2021-06-01 09:29:43

ArthasJVM內(nèi)存

2019-02-26 14:33:22

JVM內(nèi)存虛擬機(jī)

2018-11-06 12:12:00

MySQL內(nèi)存排查

2021-06-28 08:00:00

Python開發(fā)編程語言

2022-04-29 08:05:06

內(nèi)存堆外GC

2024-10-10 15:32:51

2022-11-09 17:10:47

JVM內(nèi)存區(qū)域

2019-02-14 13:30:54

內(nèi)存泄露運(yùn)維

2010-09-27 13:41:22

JVM內(nèi)存回收

2015-07-20 10:23:24

NET內(nèi)存問題排查

2024-08-19 00:10:00

C++內(nèi)存

2022-09-21 08:39:52

堆外內(nèi)存泄露內(nèi)存分布

2022-04-15 07:51:12

off-heap堆外內(nèi)存JVM

2021-06-02 09:55:20

JVM排查JVM內(nèi)存過高技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號