項目上線了一個接口,先灰度一臺機器觀察調用狀況; 接口不斷的調用,過了一段時間,發現機器上的接口調用開始報OOM異常
! 當天就是上線deadline了,刺激。。 java
jps
命令獲取出問題jvm進程的進程ID使用jps -l -m
獲取到當前jvm進程的pid,經過上述命令獲取到了服務的進程號:427726 (此處假設爲這個) 數據庫
jps
(JVM Process Status Tool):顯示指定系統內全部的HotSpot虛擬機進程
jps -l -m
: 參數-l列出機器上全部jvm進程,-m顯示出JVM啓動時傳遞給main()的參數微信
jstat
觀察jvm狀態,發現問題由於是OOM異常,因此咱們首先重啓機器觀察了JVM的運行狀況;數據結構
咱們使用jstat -gc pid time
命令觀察GC,發現GC在YGC後,GC掉的內存並很少,每次YGC後都有一部份內存未回收,致使在屢次YGC後回收不掉的內存被挪到堆的old區,old滿了以後FGC發現也是回收不掉; 這裏基本能夠肯定是內存泄漏的問題了,下面咱們有簡單看了下機器的cpu、內存、磁盤狀態併發
jstat命令:框架
jstat
(JVM statistics Monitoring)是用於監視虛擬機運行時狀態信息的命令,它能夠顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。jvm
jstat -gc pid time
: -gc 監控jvm的gc信息,pid 監控的jvm進程id,time每一個多少毫秒刷新一次ide
jstat -gccause pid time
: -gccause 監控gc信息並顯示上次gc緣由,pid 監控的jvm進程id,time每一個多少毫秒刷新一次函數
jstat -class pid time
: -class 監控jvm的類加載信息,pid 監控的jvm進程id,time每一個多少毫秒刷新一次fetch
在這裏先簡單說一下,堆的GC:
在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。
年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的,minor GC會一直重複這樣的過程。
使用top -p pid
獲取進程的cpu和內存使用率;查看RES 和 %CPU %MEM三個指標:
在這裏先簡單說一下,top命令展現的內容:
VIRT:virtual memory usage 虛擬內存
一、進程「須要的」虛擬內存大小,包括進程使用的庫、代碼、數據等
二、假如進程申請100m的內存,但實際只使用了10m,那麼它會增加100m,而不是實際的使用量
RES:resident memory usage 常駐內存
一、進程當前使用的內存大小,但不包括swap out
二、包含其餘進程的共享
三、若是申請100m的內存,實際使用10m,它只增加10m,與VIRT相反
四、關於庫佔用內存的狀況,它只統計加載的庫文件所佔內存大小
SHR:shared memory 共享內存
一、除了自身進程的共享內存,也包括其餘進程的共享內存
二、雖然進程只使用了幾個共享庫的函數,但它包含了整個共享庫的大小
三、計算某個進程所佔的物理內存大小公式:RES – SHR
四、swap out後,它將會降下來
DATA 一、數據佔用的內存。若是top沒有顯示,按f鍵能夠顯示出來。
二、真正的該程序要求的數據空間,是真正在運行中要使用的。
ps : 若是程序佔用實存比較多,說明程序申請內存多,實際使用的空間也多。
若是程序佔用虛存比較多,說明程序申請來不少空間,可是沒有使用。
發現機器的自身狀態不存在問題, so毋庸置疑,發現問題了,典型的內存泄漏。。
咱們使用jmap -dump:format=b,file=dump_file_name pid
命令,將當前機器的jvm的狀態dump下來或缺的一份dump文件,用作下面的分析
jmap命令:
jmap
(JVM Memory Map)命令用於生成heap dump文件,還能夠查詢finalize執行隊列、Java堆和永久代的詳細信息,如當前使用率、當前使用的是哪一種收集器等。
jmap -dump:format=b,file=dump_file_name pid
: file=指定輸出數據文件名, pid jvm進程號
接下來,回滾灰度的機器,開始解決問題=.=
在這裏,咱們分析dump文件,使用的Jprofiler
軟件,就是下面這個東東:
具體的使用方法,在這就再也不贅述了,下面將dump文件導入到Jprofiler
中: 選擇Heap Walker
中的Current Object Set
,這裏面顯示的是當前的類的佔用資源,從佔用空間從大到小排序;
Biggest Objects
,查看哪一個對象的佔用的內存高:
org.janusgraph.graphdb.database.StandardJanusGraph
這個對象竟然佔用了高達
724M的內存! 看來內存泄漏八九不離十就是這個對象的問題了! 再點開看看 ,以下圖,能夠發現是一個
openTransactions
的類型爲
ConcurrentHashMap
的數據結構:
這究竟是什麼對象呢,去項目中查找一下,打開idea-打開項目-雙擊shift鍵-打開全局類查找-輸入StandardJanusGraph
,以下圖:
janusgraph
的一個類,找到對應的數據結構: 類型定義:
private Set<StandardJanusGraphTx> openTransactions;
複製代碼
初始化爲一個ConcurrentHashMap:
openTransactions = Collections.newSetFromMap(new
ConcurrentHashMap<StandardJanusGraphTx, Boolean>(100,
0.75f, 1));
複製代碼
觀察上述代碼,咱們能夠看到,裏面的存儲的StandardJanusGraphTx
從字面意義上理解是janusgraph框架中的事務對象,下面往上追一下代碼,看看何時會往這個Map中賦值:
// 找到執行openTransactions.add()的方法
public StandardJanusGraphTx newTransaction(final TransactionConfiguration configuration) {
if (!isOpen) ExceptionFactory.graphShutdown();
try {
StandardJanusGraphTx tx = new StandardJanusGraphTx(this, configuration);
tx.setBackendTransaction(openBackendTransaction(tx));
openTransactions.add(tx); // 注意! 此處對上述的map對象進行了add
return tx;
} catch (BackendException e) {
throw new JanusGraphException("Could not start new transaction", e);
}
}
// 上述發現,是一個newTransaction,建立事務的一個方法,爲確保起見,再往上跟找到調用上述方法的類:
public JanusGraphTransaction start() {
TransactionConfiguration immutable = new ImmutableTxCfg(isReadOnly, hasEnabledBatchLoading,
assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExistence,
verifyInternalVertexExistence, acquireLocks, verifyUniqueness,
propertyPrefetching, singleThreaded, threadBound, getTimestampProvider(), userCommitTime,
indexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(),
logIdentifier, restrictedPartitions, groupName,
defaultSchemaMaker, customOptions);
return graph.newTransaction(immutable); // 注意!此處調用了上述的newTransaction方法
}
// 接着找上層調用,發現了最上層的方法
public JanusGraphTransaction newTransaction() {
return buildTransaction().start(); // 此處調用了上述的start方法
}
複製代碼
在咱們對圖數據庫中圖數據操做的過程當中,採用的是手動建立事務的方式,在每次查詢圖數據庫以前,咱們都會調用相似於dataDao.begin()
代碼, 其中就是調用的public JanusGraphTransaction newTransaction()
這個方法;
最後,咱們簡單的看下源碼能夠發現,從上述內存泄漏的map中去除數據的邏輯就是commit
事務的接口,調用鏈以下:
public void closeTransaction(StandardJanusGraphTx tx) {
openTransactions.remove(tx); // 從map中刪除StandardJanusGraphTx對象
}
private void releaseTransaction() {
isOpen = false;
graph.closeTransaction(this); // 調用上述closeTransaction方法
vertexCache.close();
}
public synchronized void commit() {
Preconditions.checkArgument(isOpen(), "The transaction has already been closed");
boolean success = false;
if (null != config.getGroupName()) {
MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit").inc();
}
try {
if (hasModifications()) {
graph.commit(addedRelations.getAll(), deletedRelations.values(), this);
} else {
txHandle.commit(); // 這個commit方法中釋放事務也是調用releaseTransaction
}
success = true;
} catch (Exception e) {
try {
txHandle.rollback();
} catch (BackendException e1) {
throw new JanusGraphException("Could not rollback after a failed commit", e);
}
throw new JanusGraphException("Could not commit transaction due to exception during persistence", e);
} finally {
releaseTransaction(); // // 調用releaseTransaction
if (null != config.getGroupName() && !success) {
MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit.exceptions").inc();
}
}
}
複製代碼
終於,咱們找到了內存泄漏的根源所在:項目代碼中存在調用了事務begin
可是沒有commit
的代碼!
解決問題: 找到內存泄漏接口的代碼,並發現了沒有commit()的位置,try-catch-finally中添加上了commit()代碼;
提交-部署-發佈-灰度一臺機器後觀察內存泄漏的現象消失,GC回收正常;
內存泄漏問題解決,項目如期上線~
你們,有沒有遇到過內存泄漏的狀況,歡迎在評論區說出你的故事=.=
寫這篇文章耗費的時間超出了個人預料,預計2個小時寫完,結果花了一下午的時間...
原創不易,若是你們有所收穫,但願你們能夠點贊評論支持一下~
也歡迎你們關注個人掘金
和微信搜索公衆號[匠心Java
]支持一下做者,做者按期分享工做中的所見所得~