前言
首先 java 語(yǔ)言的特性是不需像 C 和 C++ 那樣自己手動(dòng)釋放內(nèi)存,因?yàn)?java 本身有垃圾回收機(jī)制(垃圾回收稱(chēng)為 GC),顧名思義就是釋放垃圾占用的空間,防止內(nèi)存泄露。JVM 運(yùn)行時(shí)占用內(nèi)存最大的空間就是堆內(nèi)存,另外棧區(qū)和方法區(qū)也會(huì)占用空間但是占用有限本章就不探究了。
那么堆中的空間又分為年輕代和老年代,所以我們粗略的把垃圾回收分為兩種:年輕代的垃圾回收稱(chēng)為 Young GC,老年代的垃圾回收稱(chēng)為 Full GC,實(shí)際上此處的 Full GC 也包含了新生代,老年代,元空間等的回收。 因?yàn)?Full GC 的回收過(guò)程會(huì)使系統(tǒng)的所有線(xiàn)程 STW(Stop The World),那么我們一定希望讓系統(tǒng)盡量不要進(jìn)行 Full GC,或者必須要進(jìn)行 FullGC 的時(shí)候執(zhí)行的時(shí)間越短越好。
下面我們主要探究 Full GC 的角度出發(fā)分析我在開(kāi)發(fā)運(yùn)營(yíng)后臺(tái)的時(shí)候遇到的頻繁 Full GC 過(guò)程。
事件背景
項(xiàng)目介紹: 我們團(tuán)隊(duì)做的是一個(gè)后臺(tái)管理系統(tǒng),因?yàn)獒槍?duì)不同用戶(hù)負(fù)責(zé)的功能不同那么需要的權(quán)限也就不一樣,所以引入了主流的 shiro 框架做權(quán)限控制,該框架可以控制菜單欄,按鈕,操作框等。在引入這個(gè)框架時(shí)一并引入了輔助組件shiro-redis,該組件是一個(gè)緩存層方便管理用戶(hù)登錄信息,內(nèi)存泄漏的問(wèn)題也是就現(xiàn)在這個(gè)輔助組件上。
事件還原: 在周五的中午 11:30 分收到了監(jiān)控的報(bào)警信息提示系統(tǒng)在頻繁 Full GC,此時(shí)我們立刻做兩件事情: 第一:登錄公司的 UMP 監(jiān)控平臺(tái)(開(kāi)源監(jiān)控可以參考:【Prometheus+grafana 監(jiān)控】)查看該機(jī)器的系統(tǒng)指標(biāo),發(fā)現(xiàn)確實(shí)在頻繁 FullGC 從 11 點(diǎn)持續(xù)到了 11 點(diǎn)半
第二:保留一臺(tái)機(jī)器作為證據(jù)收集,其他機(jī)器進(jìn)行重啟保障業(yè)務(wù)能正常訪(fǎng)問(wèn),重啟后 full gc 正常
第三:堆棧信息操作指令./jmap -F -dump:live,format=b,file=/jmapfile.hprof 18362(-F 操作是強(qiáng)制導(dǎo)出堆棧信息,18362 是應(yīng)用 pid,通過(guò) top -c 指令獲?。?br />
第四:因?yàn)閭€(gè)人無(wú)權(quán)限導(dǎo)出堆棧信息,馬上電話(huà)聯(lián)系運(yùn)維通過(guò)上面指令導(dǎo)出該機(jī)器上的堆棧文件,就是抓取現(xiàn)場(chǎng)證據(jù),因?yàn)檫^(guò)了這個(gè)時(shí)間堆內(nèi)存可能就正常了 根據(jù) JVM 知識(shí)分析,常見(jiàn) Full GC 時(shí)的五種情況如下:
1. 老年代內(nèi)存不足(大對(duì)象過(guò)多或內(nèi)存泄漏) 2. Metaspace 空間不足 3. 代碼主動(dòng)觸發(fā) System.gc() 4. YGC 時(shí)的悲觀策略 5. dump live 的內(nèi)存信息時(shí),比如 jmap -dump:live
分析原因
1、查看公司 SGM 監(jiān)控平臺(tái)(開(kāi)源監(jiān)控可以參考:【Prometheus+grafana 監(jiān)控】),元空間最大內(nèi)存 256M,F(xiàn)ullGC 發(fā)生前后為 117M,排除 Metaspace 不足造成的原因
2、在系統(tǒng)中搜索第三方 jar 包,沒(méi)有主動(dòng)執(zhí)行 System.gc () 操作的代碼 3、查看 JVM 啟動(dòng)參數(shù)中有下面兩個(gè)參數(shù),所以排除了 YGC 時(shí)候的悲觀策略原因
-XX:CMSInitiatingOccupancyFraction=70 # 堆內(nèi)存達(dá)到 70%進(jìn)行 FullGC -XX:+UseCMSInitiatingOccupancyOnly # 禁止 YGC 時(shí)的悲觀策略(YGC 前后判斷是否需要 FullGC),只有達(dá)到閾值才進(jìn)行 FullGc4、通過(guò)和運(yùn)維、研發(fā)組溝通沒(méi)有人主動(dòng)執(zhí)行 dump 操作,查看系統(tǒng)的歷史執(zhí)行指令也沒(méi)有 dump 操作,主動(dòng) dump 的原因排除 初步分析結(jié)果: 通過(guò)上面依靠監(jiān)控平臺(tái)、JVM 啟動(dòng)參數(shù)、代碼排除、指令分析,最終嫌疑最大的就是老年代內(nèi)存空間不足造成頻繁 Full GC,但是作為技術(shù)者,排除法顯然不能作為原因定位的依據(jù),我們還需要繼續(xù)確定我們的猜想,下面會(huì)結(jié)合 JVM 啟動(dòng)參數(shù),Tomcat 啟動(dòng)參數(shù),堆棧文件三大關(guān)鍵要素做具體分析。
下圖是進(jìn)行 FullGC 時(shí)候的老年代內(nèi)存情況,把下面的 72%、1794Mb、2496Mb、448Mb 先記住,下面會(huì)跟這些值做對(duì)比
指標(biāo)信息: JVM 核心參數(shù):
-Xms2048M # 系統(tǒng)啟動(dòng)初始化堆空間 -Xmx4096M # 系統(tǒng)最大堆空間 -Xmn1600M # 年輕代空間(包括 From 區(qū)和 To),F(xiàn)rom 和 To 默認(rèn)占年輕代 20% -XX:MaxPermSize=256M # 最大非堆內(nèi)存,按需分配 -XX:MetaspaceSize=256M # 元空間大小,JDK1.8 取消了永久代(PermGen)新增元空間,元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,存儲(chǔ)類(lèi)和類(lèi)加載器的元數(shù)據(jù)信息 -XX:CMSInitiatingOccupancyFraction=70 # 堆內(nèi)存達(dá)到 70%進(jìn)行 FullGC -XX:+UseCMSInitiatingOccupancyOnly # 禁止 YGC 時(shí)的悲觀策略(YGC 前后判斷是否需要 FullGC),只有達(dá)到閾值才進(jìn)行 FullGc -XX:+UseConcMarkSweepGC # 使用 CMS 作為垃圾收集器Tomcat 核心參數(shù):
maxThreads=750# Tomcat 線(xiàn)程池最多能起的線(xiàn)程數(shù) minSpareThreads=50# Tomcat 初始化的線(xiàn)程池大小或者說(shuō) Tomcat 線(xiàn)程池最少會(huì)有這么多線(xiàn)程 acceptCount=1000# Tomcat 維護(hù)最大的隊(duì)列數(shù)
導(dǎo)出堆棧指令:jmap -dump:live,format=b,file=jmapfile.hprof [pid]。導(dǎo)出的文件需要使用 MAT 軟件分析,全稱(chēng) MemoryAnalyzer,主要分析堆內(nèi)存。
從堆棧文件分析結(jié)果中發(fā)現(xiàn)有 50 個(gè) org.apache.tomcat.util.threads.TaskThread 占用空間很大。共占用空間 96.16%
每個(gè) TaskThread 實(shí)例占用空間 36M 左右
查看內(nèi)存詳情保存最大最多的對(duì)象是 ThreadLocal 中存儲(chǔ)的 SessionInMemory 對(duì)象
最終原因: 通過(guò)分析上面的 JVM 參數(shù)、Tomcat 參數(shù)、堆棧文件,內(nèi)存泄漏的原因是每個(gè)線(xiàn)程中有一個(gè) ThreadLocal 存儲(chǔ)大量 SessionInMemory,因?yàn)?Tomcat 的啟動(dòng)核心線(xiàn)程數(shù)是 50 個(gè),每個(gè)線(xiàn)程的內(nèi)存占用 36M 左右,共占用 1.8G,老年代內(nèi)存達(dá)到 70% 也就是 2496 * 0.7 = 1747.2M 就會(huì)進(jìn)行垃圾回收,1.8G 剛好比 1747.2M 稍微大一些。但是線(xiàn)程中的對(duì)象又沒(méi)辦法被回收,所以就會(huì)看到系統(tǒng)再頻繁 FullGC。
定位問(wèn)題
通過(guò)上面內(nèi)存分析已經(jīng)定位到內(nèi)存泄漏的原因是每個(gè)線(xiàn)程中有大量 SessionInMemory,下面步驟就認(rèn)真分析代碼找到其中創(chuàng)建如此多對(duì)象還不銷(xiāo)毀的原因。
經(jīng)過(guò)初步分析發(fā)現(xiàn) SessionInMemory 是引用 shiro-redis 的工具包里面的對(duì)象,主要封裝 Session 信息和創(chuàng)建時(shí)間。主要作用是在當(dāng)前線(xiàn)程的 jvm 中做一層緩存當(dāng)系統(tǒng)頻繁獲取 Session 時(shí)不用去 redis 獲取了。SessionInMemary 對(duì)象是 shiro 判斷用戶(hù)登錄成功時(shí)候存儲(chǔ)的數(shù)據(jù),主要包括用戶(hù)信息,認(rèn)證信息,權(quán)限信息等,因?yàn)橛脩?hù)登錄成功后不會(huì)重復(fù)認(rèn)證,shiro 會(huì)對(duì)不同用戶(hù)做權(quán)限判斷
分析代碼發(fā)現(xiàn)處理本地緩存 Session 的流程有明顯問(wèn)題,我畫(huà)了一個(gè)簡(jiǎn)易的流程圖,在介紹流程圖前我先描述一下 Session 和用戶(hù)登錄操作如何聯(lián)系起來(lái)
我們都知道運(yùn)營(yíng)后臺(tái)需要用戶(hù)登錄,登錄成功后會(huì)生成一個(gè) cookie 保存到瀏覽器中,cookie 存儲(chǔ)一個(gè)關(guān)鍵字段 sessionId 用來(lái)標(biāo)識(shí)用戶(hù)的狀態(tài)和信息,當(dāng)用戶(hù)訪(fǎng)問(wèn)頁(yè)面調(diào)用接口的時(shí)候 shiro 會(huì)從請(qǐng)求 Request 中獲取 cookie 中的 sessionId,根據(jù)這個(gè)唯一標(biāo)識(shí)生成 Session 來(lái)存儲(chǔ)用戶(hù)的登錄態(tài)和登錄信息等,這些信息會(huì)保存到 redis 中。shiro-redis 組件負(fù)責(zé)從 redis 中獲取的 Session 信息通過(guò) ThreadLoca 做到線(xiàn)程隔離。
上圖流程概括就是:用戶(hù)訪(fǎng)問(wèn)頁(yè)面先從本地緩存獲取 Session,如果存在且沒(méi)有超過(guò)一秒就返回結(jié)果,如果沒(méi)有 Session 或者過(guò)期了就把現(xiàn)在的 Session 刪除并新建一個(gè)返回結(jié)果。整體看思路清晰,先獲取 Session,如果沒(méi)有就新建返回,如果過(guò)期了就刪除再新建返回。
流程圖隱藏的問(wèn)題(核心問(wèn)題)
1、多個(gè)線(xiàn)程會(huì)復(fù)制多份相同 Session 使內(nèi)存成倍增加(Session 一樣線(xiàn)程不同) 舉個(gè)例子:用戶(hù)登錄后臺(tái)生成一個(gè) Session,假設(shè)請(qǐng)求都到一臺(tái)機(jī)器上,第一次請(qǐng)求到線(xiàn)程 1,第二個(gè)請(qǐng)求到線(xiàn)程 2,因?yàn)?Session 一樣但是線(xiàn)程之間是隔離的,所以線(xiàn)程 1 和線(xiàn)程 2 都會(huì)創(chuàng)建一份相同 Session 存儲(chǔ)到 ThreaLocal 中,Tomcat 最小空閑線(xiàn)程數(shù)越多復(fù)制的 Session 份數(shù)也越多。因?yàn)?Tomcat 的核心線(xiàn)程數(shù)不會(huì)關(guān)閉,所以里面的資源也不會(huì)釋放。此處有個(gè)疑問(wèn) ThreadLocad 的 key 是弱引用但是為什么沒(méi)回收呢?下面統(tǒng)統(tǒng)解答
2、舊 Session 無(wú)法清除(線(xiàn)程一樣 Session 不同)
舉個(gè)例子 1:假設(shè)所有請(qǐng)求都到一臺(tái)機(jī)器的同一個(gè)線(xiàn)程,用戶(hù)第一次登錄后臺(tái)生成 Session1,第一次請(qǐng)求到線(xiàn)程 1,1 秒內(nèi)所有請(qǐng)求都執(zhí)行完了,此時(shí) Session 沒(méi)有移除(因?yàn)?Session 移除策略是懶刪除,需要等下次同一個(gè) Session 訪(fǎng)問(wèn)時(shí)判斷過(guò)期條件再刪除),用戶(hù)重新登錄,生成了 Session2,因?yàn)?Session2 在線(xiàn)程 1 中還沒(méi)有就會(huì)重新創(chuàng)建,導(dǎo)致第一次登錄時(shí)候用到的 Session1 就一直保存到該線(xiàn)程中了
舉個(gè)例子 2:參考例子 1 的思路,如果用戶(hù)用 Session1 沒(méi)有在 1 秒內(nèi)把所有請(qǐng)求執(zhí)行完,就會(huì)執(zhí)行懶刪除操作,但是刪除后又新建了一個(gè),那么用戶(hù)重新登錄后剛才新建的那個(gè) Session 還是沒(méi)有被刪除,所以總結(jié)出來(lái)只要用戶(hù)重新登錄必定有一個(gè)舊的 Session 會(huì)保留到線(xiàn)程中
代碼分析
1、在 RedisSessionDAO.java 文件中定義了一個(gè) ThreadLocal 變量作為線(xiàn)程隔離
2、用戶(hù)訪(fǎng)問(wèn)接口、js 文件、css 文件等資源的時(shí)候會(huì)進(jìn)入 shiro 的攔截機(jī)制。在攔截過(guò)程中會(huì)頻繁調(diào)用 doReasSession () 方法獲取用戶(hù)的 Session 信息,主要是獲取信息校驗(yàn)用戶(hù)的權(quán)限控制等。
下面的方法主要整合了獲取 Session 操作和設(shè)置 Session 操作,如果從 ThreadLocal 中沒(méi)有獲取到或者本地緩存超過(guò) 1 秒了就返回 null,判斷為 null 之后就會(huì)從 redis 中獲取并新建一個(gè) Session 存儲(chǔ)到 ThreadLoca 中
3、從 ThreadLocal 中取出 sessionMap,根據(jù) sessionId 在 sessionMap 中尋找 Session,如果沒(méi)找到直接返回 null,如果找到了再判斷時(shí)間是否超過(guò)了 1 秒,如果沒(méi)超過(guò)返回 Session,如果超過(guò)了移除返回 null
4、從 ThreadLocal 中獲取 sessionMap,如果為 null 就新建一個(gè)保存起來(lái),因?yàn)橛脩?hù)第一次訪(fǎng)問(wèn)的時(shí)候線(xiàn)程中的 sessionMap 還沒(méi)有呢所以要新建。然后向 sessionMap 中存儲(chǔ) Session 對(duì)象
所以代碼的完成流程總結(jié):獲取 Session 的操作是調(diào)用 getSessionFromThreadLocal () 方法,如果沒(méi)有獲取到 Session 就返回 null,調(diào)用 setSessionToThreadLocal () 方法會(huì)重新設(shè)置一個(gè) Session。如果 Session 在當(dāng)前線(xiàn)程的保存時(shí)間超過(guò) 1 秒就 remove。
通過(guò)上面分析 JVM、Tomat、堆棧、代碼已經(jīng)把問(wèn)題定位了,因?yàn)?shiro-redis 中存儲(chǔ)的 SessionInMemory 對(duì)象處理不當(dāng)導(dǎo)致線(xiàn)程間存儲(chǔ)越來(lái)越多,最終使內(nèi)存泄漏進(jìn)而導(dǎo)致了頻繁 FullGC。因?yàn)槲覀円玫?shiro-redis 版本是 3.2.2 版本,所以存在這個(gè)漏洞,作者已于 2019 年 3 月升級(jí) jar 包到 3.2.3 版本把該問(wèn)題解決。備注:3.2.2 及以下版本存在該問(wèn)題
解決問(wèn)題
解決問(wèn)題的方案目前有四種。針對(duì)我們系統(tǒng)使用的是方案 1 + 方案 4
序號(hào) | 方案描述 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|---|
方案 1 | 每次設(shè)置 session 時(shí)遍歷刪除以前過(guò)期或者為 null 的 session | 主動(dòng)刪除,刪除頻次依賴(lài)用戶(hù)的訪(fǎng)問(wèn)頻次 | 如果在 1 秒內(nèi)有大量用戶(hù)訪(fǎng)問(wèn),總 session 很多無(wú)效 session 很少,遍歷所有 session 做了很多無(wú)用功導(dǎo)致訪(fǎng)問(wèn)變慢 |
方案 2 | 取消 threadLocal 策略,所有請(qǐng)求直接查詢(xún)緩存(redis) | 減少本地內(nèi)存使用 | 訪(fǎng)問(wèn)緩存耗時(shí)比本地長(zhǎng),經(jīng)過(guò)測(cè)試發(fā)現(xiàn)一個(gè)接口會(huì)調(diào)用 16 次左右的獲取 session 操作,一個(gè)頁(yè)面幾十個(gè)接口,直接查詢(xún)緩存性能存在問(wèn)題 |
方案 3 | 使用本地緩存(guavaCache 或者 EhCache 等),并對(duì)緩存做移除策略 | 多個(gè)線(xiàn)程共用一份內(nèi)存,節(jié)省內(nèi)存空間,提升系統(tǒng)性能 | 對(duì)框架有深入了解,接入需要開(kāi)發(fā)成本 |
方案 4 | 把 tomcat 的核心線(xiàn)程數(shù)減小,比如把原來(lái)的 50 改成 5 | 減少系統(tǒng)資源,減少相同 Session 的復(fù)制份數(shù),大于 5 的線(xiàn)程銷(xiāo)毀資源也一起回收 | 處理并發(fā)能力略低 |
疑問(wèn)解答
Q:在 RedisSessionDAO 里面只定義了一個(gè) ThreadLocal 的變量 sessionsInThread,怎么就會(huì)是 50 個(gè)線(xiàn)程把相同的 Session 復(fù)制 50 份呢?
A:首先我們先理解 ThreadLocal 的結(jié)構(gòu),ThreadLocal 有一個(gè)靜態(tài)類(lèi) ThreadLocalMap,ThreadLocalMap 里面還有一個(gè) Entry,我們的 key 和 value 就是保存在 Entry 的,key 是一個(gè)弱引用的 ThreadLocal 類(lèi)型,,這個(gè) key 在所有的線(xiàn)程中都是一樣的,實(shí)際上就是我們定義的靜態(tài) sessionsInThread。那又是怎么做到線(xiàn)程隔離的呢?
這就講到 Thread 中的一個(gè)成員變量 threadLocals,這個(gè)對(duì)象就是 ThreadLocal.ThreadLocalMap 類(lèi)型,也就是每次創(chuàng)建一個(gè)線(xiàn)程都會(huì) new 一個(gè) ThreadLocalMap,所以每個(gè)線(xiàn)程中的 ThreadLocalMap 都是不同的,但是里面 Entry 存儲(chǔ)的 key 都是一樣的,也就是我們前面定義的 sessionsInThread 靜態(tài)變量。
當(dāng)一個(gè)線(xiàn)程需要獲取 Entry 中存儲(chǔ)的 value 時(shí)候,調(diào)用 sessionsInThread.get () 方法,這個(gè)方法做了三件事情,一是獲取當(dāng)前線(xiàn)程的實(shí)例,二是從線(xiàn)程實(shí)例中獲取 ThreadLocalMap,三是從 ThreadLocalMap 中根據(jù) ThreadLocal 這個(gè) key 獲取指定的 value
獲取 Thread 中的 ThreadLocalMap
從 ThreadLocalMap 中獲取指定的 value,又有個(gè)疑問(wèn),獲取 Entry 為什么還要從一個(gè) table 數(shù)組中拿呢?這個(gè)很好理解一個(gè)線(xiàn)程不一定只有一個(gè) ThreadLocal 變量吧,多個(gè) ThreadLocal 變量就是有多個(gè) key,所以就放到 table 數(shù)組里面了
Q:都說(shuō) ThreadLocal 的 key 是一個(gè)弱引用,如果內(nèi)存不足了會(huì)被垃圾回收,咱們的 key 從堆??床](méi)有回收呀?
A:這是個(gè)好問(wèn)題,首先我們的 RedisSessionDAO 是 Spring 注入的單例模式,ThreadLocal 被定義成一個(gè)靜態(tài)變量,靜態(tài)變量在內(nèi)存中是不會(huì)回收的。
補(bǔ)充:一般我們?cè)谑褂?ThreadLocal 的時(shí)候都會(huì)定義成靜態(tài)變量,如果定義成非靜態(tài)變量創(chuàng)建一個(gè)對(duì)象就會(huì) new 一個(gè) ThreadLocal,那么 ThreadLocal 就沒(méi)有存在的意義了。
Q:已經(jīng)結(jié)束的線(xiàn)程,為什么還會(huì)存活,里面的對(duì)象也不會(huì)消失?
A:因?yàn)樵O(shè)置的最小空閑線(xiàn)程數(shù)是 50,業(yè)務(wù)量不大并發(fā)數(shù)沒(méi)有超過(guò) 50,tomcat 會(huì)保留最小的線(xiàn)程數(shù)量不會(huì)新建也不用回收,ThreadLocalMap 是線(xiàn)程中的成員變量所以不會(huì)回收
Q:訪(fǎng)問(wèn)一次接口就會(huì)生成一個(gè) sessionId 嗎?
A:訪(fǎng)問(wèn)接口先判斷用戶(hù)信息是否有效,無(wú)效才會(huì)重新登錄獲取新的 sessionId
Q:shiro-redis 在本地保存 Session 為什么設(shè)置 1 秒過(guò)期時(shí)間?
A:因?yàn)檫\(yùn)營(yíng)后臺(tái)不同于業(yè)務(wù)接口會(huì)持續(xù)調(diào)用,后臺(tái)接口大部分的場(chǎng)景是用戶(hù)訪(fǎng)問(wèn)一個(gè)頁(yè)面并停留在頁(yè)面上做一些操作,訪(fǎng)問(wèn)一個(gè)頁(yè)面的時(shí)候?yàn)g覽器會(huì)加載多個(gè)資源,包括靜態(tài)資源 html,css,js 等,和接口的動(dòng)態(tài)數(shù)據(jù),整個(gè)資源加載過(guò)程盡量保持在一秒內(nèi)完成,如果超過(guò)一秒的話(huà)系統(tǒng)體驗(yàn)性能較差,所以本地緩存一秒足夠了。
收獲總結(jié)
報(bào)警前:
1. 熟悉第三方 jar 包的工作原理,尤其是個(gè)人開(kāi)發(fā)工具包,因?yàn)闆](méi)有經(jīng)過(guò)市場(chǎng)檢驗(yàn)使用前要格外小心
2. 可以使用 jvisualvm 進(jìn)行本地壓測(cè)觀察 jvm 情況
3. 關(guān)注監(jiān)控報(bào)警,掌握監(jiān)控平臺(tái)操作,能夠從監(jiān)控中查詢(xún)系統(tǒng)各項(xiàng)指標(biāo)信息
4. 根據(jù)業(yè)務(wù)合理配置 JVM 參數(shù)和 Tomcat 參數(shù)
報(bào)警后:
1. 能夠第一時(shí)間抓取系統(tǒng)的 JVM 信息,比如堆棧,GC 信息,線(xiàn)程棧等
2. 通過(guò)使用 MAT 內(nèi)存輔助軟件幫助自己分析問(wèn)題原因
審核編輯:劉清
-
存儲(chǔ)器
+關(guān)注
關(guān)注
38文章
7430瀏覽量
163515 -
JAVA語(yǔ)言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20062 -
cms
+關(guān)注
關(guān)注
0文章
59瀏覽量
10949 -
緩存器
+關(guān)注
關(guān)注
0文章
63瀏覽量
11641 -
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12197
原文標(biāo)題:頻繁FullGC的原因竟然是 “開(kāi)源代碼”?
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論