## 一、問題是怎麼發現的 最近有個新系統開發完成後要上線,由於系統調用量很大,所以先對核心介面進行了一次壓力測試,由於核心介面中基本上只有純記憶體運算,所以預估核心介面的壓測QPS能夠達到上千。 壓測容器配置:4C8G 先從10個併發開始進行發壓,結果cpu一下就飆升到了100%,但是核心介面的qp ...
一、問題是怎麼發現的
最近有個新系統開發完成後要上線,由於系統調用量很大,所以先對核心介面進行了一次壓力測試,由於核心介面中基本上只有純記憶體運算,所以預估核心介面的壓測QPS能夠達到上千。
壓測容器配置:4C8G
先從10個併發開始進行發壓,結果cpu一下就飆升到了100%,但是核心介面的qps才200左右。於是觀察jvm的垃圾回收發現younggc很頻繁,但是fullGC數量為零。
二、排查問題的詳細過程
由於剛一開始壓測,容器cpu就飆升到了100%,所以需要先定位cpu使用率問題,找出使用cpu最高的幾個進程。可以通過top命令查找進程ID,發現正是壓測的Java應用進程ID;然後在定位該金晨曦cpu使用率最高的線程,可以通過top -p 進程ID -H 命令顯示該進程下的線程使用cpu信息。
top
top -p 進程ID -H
圖片中PID列則為十進位顯示的線程ID,然後轉換為16進位;在通過jstack 系統進程ID | grep 16進位線程ID 命令找到對應的線程信息如下,也就是該線程占用了一半左右的cpu。
jstack 系統進程ID | grep 16進位線程ID
此時定位到了Finalizer線程,但是這個線程又有什麼作用呢?
原來這個線程會不停的迴圈等待java.lang.ref.Finalizer.ReferenceQueue中的新增對象。一旦Finalizer線程發現隊列中出現了新的對象,它會彈出該對象,調用它的finalize()方法,將該引用從Finalizer類中移除,因此下次GC再執行的時候,這個Finalizer實例以及它引用的那個對象就可以被垃圾回收掉了。如果這個線程一直在不停的工作,說明Finalizer的隊列中有許多等待GC的垃圾對象。此時可以通過另一個命令來查看等待回收的垃圾對象有哪些。
jmap -finalizerinfo 進程ID
Count Class description
-------------------------------------------------------
32221 com.jd.price.deep.exact.entity.coupons.DeepExactCouponVo$$EnhancerByCGLIB$$200e6ee6
14908 com.jd.pricedoor.compute.promotion.MultiplePromotion$$EnhancerByCGLIB$$a59933de
11982 java.util.zip.Deflater
1 java.net.SocksSocketImpl
通過上述結果可以發現有好多的業務對象,通過類名可以看到這些對象都是通過CGLIB動態代理創建的,而且這些動態代理類都預設實現了finalize方法,導致這些對象在進行垃圾回收時必須先要執行finalize方法,所以都積壓到了finalizer的隊列中。
三、如何解決問題
通過上述排查過程發現,是由於大量的業務對象通過CGLIB創建了動態代理類,而這些代理都是系統處理請求時創建的臨時對象,請求完成後,這些臨時對象就需要被垃圾回收掉,從而導致Finalizer線程執行頻繁搶占了cpu資源。
針對以上分析結果所以有瞭如下幾種解決方案:
1.不要使用CGLIB來給那些需要頻繁進行垃圾回收的對象創建動態代理,可以手動創建靜態代理類。
2.對象復用,儘量減少臨時對象的產生。
作者:京東零售 曹志飛
來源:京東雲開發者社區