面试必会系列 - 1.6 Java 垃圾回收机制
本文已收錄至 Github(MD-Notes),若博客中有圖片打不開,可以來我的 Github 倉庫:https://github.com/HanquanHq/MD-Notes,涵蓋了互聯網大廠面試必問的知識點,講解透徹,長期更新中,歡迎一起學習探討 ~ 另外:
 面試必會系列專欄:https://blog.csdn.net/sinat_42483341/category_10300357.html
 操作系統系列專欄:https://blog.csdn.net/sinat_42483341/category_10519484.html
垃圾回收機制
垃圾:沒有引用指向的對象
JVM 堆分代模型
JVM 內存模型和具體的垃圾回收器有關,面試問的時候,你應該說明是哪一種回收器!
新生代(堆空間)
- 分為 1 個 伊甸區,2 個 survivor 區
 - 存活對象少,使用 拷貝算法
 
老年代(堆空間)
- 存活對象多,使用 標記壓縮算法 / 標記清除算法
 
永久代(方法區 MethodArea)(堆之外空間)
存的是class的元信息,代碼的編譯信息等待等等
- 1.8 之前:Perm Generation,固定設置大小
 - 1.8 之后:MetaSpace,默認受限于物理內存,可以設置
 - 字符串常量1.7在Perm Generation,1.8在堆內存
 
除了 Epsilon,ZGC,Shenandoah 之外的垃圾回收器,都是 邏輯分代 模型
G1是 邏輯分代,物理不分代(物理分代就是內存里確實有這樣一塊空間)
除此之外,其余的不僅邏輯分代,而且物理分代。
對象何時進入老年代?
對象頭 MarkWord 中存儲分代年齡的只有 4 bit,所以最大是15次。
PS 默認是15,CMS 默認是 6。
對象分配過程?
new 一個對象,首先嘗試在 棧 上分配。如果能分配到棧上,則分配到棧上。
為什么?因為一旦方法彈出,整個生命周期就結束,不需要垃圾回收,提高了效率。
什么樣的對象能在棧上分配?滿足可以進行 逃逸分析、標量替換 的對象,可以分配到棧上
什么是逃逸分析?如果判斷一段代碼中堆上的所有數據都只被一個線程訪問,就可以當作棧上的數據對待,認為它們是線程私有的,無須同步。例如,一個對象只出現在 for 循環內部,沒有外部的引用。
什么是標量替換?如果你這個對象可以被分解為基礎數據類型來替換,比如一個對象 T 有兩個 int 類型。類似于 C 的結構體。
如果棧上分配不下,判斷是否 大對象(多大?有參數可以設置)
- 如果是大對象,直接進入 old 區
 - 如果不是大對象,放入 ThreadLocalAllocationBuffer(TLAB) 線程本地分配緩沖區,它其實是在 eden 區分配給每個線程的一小塊空間,線程把對象 new 在自己兜里,兜里滿了再去搶公共的空間,避免 new 對象的時候不同線程對空間的征用。
 
進行 GC 清除,在分代模型 GC 下:
- 如果 是垃圾,則對象被清除
 - 如果 不是垃圾,進入 survivor 區,或在 survivor 區之間復制
 - 如果 survivor 區年齡到達 15,進入 old 區
 
動態年齡,分配擔保:了解即可
-  
動態對象年齡判定:為了適應不同內存狀況,虛擬機不要求對象年齡達到閾值才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 的一半,年齡不小于該年齡的對象就可以直接進入老年代。
 -  
空間分配擔保:MinorGC 前虛擬機必須檢查老年代最大可用連續空間是否大于新生代對象總空間,如果滿足則說明這次 Minor GC 確定安全。
如果不滿足,虛擬機會查看 -XX:HandlePromotionFailure 參數是否允許擔保失敗,如果允許會繼續檢查老年代最大可用連續空間是否大于歷次晉升老年代對象的平均大小,如果滿足將冒險嘗試一次 Minor GC,否則改成一次 FullGC。
冒險是因為新生代使用復制算法,為了內存利用率只使用一個 Survivor,大量對象在 Minor GC 后仍然存活時,需要老年代進行分配擔保,接收 Survivor 無法容納的對象。
 
為什么 hotspot 不使用 C++ 對象來代表 java 對象?
為什么不為每個Java類生成一個C++類與之對應?
因為 C++ 對象里面有一個 virtual table 指針,而HotSopt JVM的設計者不想讓每個對象中都含有一個vtable(虛函數表),而是設計了一個 OOP-Klass Model,把對象模型拆成 klass 和 oop
-  
OOP 指的是 Ordinary Object Pointer (普通對象指針),它用來表示對象的實例信息,看起來像個指針實際上是藏在指針里的對象。OOP 中不含有任何虛函數。
 -  
Klass 簡單的說是Java類在HotSpot中的 C++ 對等體,用來描述 Java 類。Klass 含有虛函數表,可以進行method dispatch,Klass主要有兩個功能:
- 實現語言層面的Java類
 - 實現Java對象的分發功能
 
 
Class 對象是在堆還是在方法區?
在程序運行期間,Java 運行時系統為所有對象維護一個運行時類型標識,這個信息會跟蹤每個對象所屬的類,虛擬機利用運行時類型信息選擇要執行的正確方法,保存這些信息的類就是 Class,這是一個泛型類。
運行時常量池的作用是什么?
運行時常量池是 方法區(hotspot 的實現為元數據區) 的一部分
Class 文件 中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是 常量池表,用于存放編譯器生成的各種 字面量 與 符號引用,這部分內容在類加載后,被存放到 運行時常量池。一般除了保存 Class 文件中描述的 符號引用 外,還會把符號引用翻譯的 直接引用 也存儲在運行時常量池。
運行時常量池相對于 Class 文件常量池的一個重要特征是動態性,Java 不要求常量只有編譯期才能產生,運行期間也可以將新的常量放入池中,這種特性利用較多的是 String 的 intern 方法。
運行時常量池是方法區的一部分,受到方法區內存的限制,常量池無法申請到內存時拋出 OutOfMemoryError。
如何判斷一個常量是廢棄常量?
我們需要對運行時常量池的廢棄常量進行回收。那么,如何判斷一個常量是廢棄常量呢?
沒有棧引用指向這個常量,這個常量就是廢棄的。
假如在常量池中存在字符串 “abc”,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,“abc” 就會被系統清理出常量池。
GC的分類
MinorGC / YoungGC:年輕代空間耗盡時觸發
MajorGC / FullGC:老年代無法分配空間時觸發,新生代、老年代同時進行回收
“找到”垃圾的算法?
1、引用計數算法(ReferenceCount)
在對象中添加一個引用計數器,如果被引用計數器加 1,引用失效時計數器減 1,如果計數器為 0 則被標記為垃圾。原理簡單,效率高,在 Java 中很少使用,因對象間循環引用的問題會導致計數器無法清零。
2、根可達算法(RootSearching)
通過一系列 GC Roots 對象 作為起始點,開始向下搜索,若一個對象沒有任何引用鏈相連,則不可達。
GC Roots 對象 包括:
- JVM stack - JVM 棧
 - native method stack - 本地方法棧
 - run-time constant pool - 運行時常量池
 - static references in method area - 方法區靜態方法
 - Clazz - 類
 
“清除”垃圾的算法?
1、Mark-Sweep 標記清除算法
- 需要兩遍掃描:第一遍標記,第二遍清除
 - 產生碎片,內存空間不連續,大對象分配不到內存觸發 FGC
 - 適合 存活對象較多 的情況,不適合Eden區
 
2、Copying 拷貝算法
HotSpot 把新生代劃分為一塊較大的 Eden 和兩塊較小的 Survivor。垃圾收集時將 Eden 和 Survivor 中仍然存活的對象一次性復制到另一塊 Survivor 上,然后直接清理掉 Eden 和已用過的那塊 Survivor。
- 只掃描一次,效率高
 - 不產生碎片
 - 空間折半浪費
 - 移動、復制對象時,需要調整對象引用
 - 適合 存活對象比較少 的情況,老年代一般不使用此算法
 
3、Mark-Compact 標記壓縮算法
- 需要掃描兩次,需要移動對象,效率偏低,開銷大,停頓時間長
 - 不產生碎片
 - 不會產生內存減半
 - 老年代使用
 
4、分代收集算法
現在的商用虛擬機的垃圾收集器,基本都采用"分代收集"算法,根據對象存活周期的不同將內存分為幾塊。一般將java堆分為新生代、老年代,這樣我們可以根據各個年代的特點,選擇合適的垃圾收集算法。
新生代 每次收集都有大量對象死去,所以選擇 復制算法,只要付出少量對象復制成本,就可以完成垃圾收集。IBM 做過統計,工業上一般來講,一次回收會回收掉 90% 的對象。
老年代 對象存活幾率比高,且無額外的空間對它進行分配擔保,就必須選擇 標記-清除 或者 標記-壓縮 行垃圾收集。
常見的垃圾回收器
隨著硬件內存越來越大,原有的回收器需要回收的時間變得越來越長,于是發展出了各種各樣的垃圾回收器。
常見組合:
- Serial + Serial Old
 - ParNew + CMS
 - Parallel Scavenge + Parallel Old(PS + PO),1.8 默認
 - G1
 
1、Serial + Serial Old
- STW
 - 單線程
 - Serial 年輕代 復制算法,Serial Old 老年代 標記-壓縮算法
 - 與其他收集器的單線程相比,簡單高效,對于運行在 client 模式下的虛擬機是個不錯的選擇
 
2、Parallel Scavenge + Parallel Old(jdk 8 默認)
10G內存的話,回收一次要十幾秒
- STW
 - 多個GC線程并行回收
 - 年輕代 復制算法,老年代 標記-壓縮算法
 - 線程數不可能被無限增多,CPU會將資源耗費在線程切換上
 
3、ParNew + CMS
java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01ParNew 年輕代
- STW
 - 多個GC線程
 - ParNew 與 Parallel Scavenge 相似,只不過 ParNew 有可以與 CMS 結合的同步機制。除了 Serial 外,只有它能與 CMS 配合。
 
CMS 老年代
-  
以 獲取最短回收停頓時間 為目標,并發回收,用戶線程和 GC 線程同時進行,暫停時間短
 -  
基于 標記-清除 算法
 -  
分為 四個階段:
- 初始標記(STW):暫停所有其他線程,并記錄直接與 root 相連的對象。初始垃圾并不多,因此很快。
 - 并發標記:垃圾回收線程和用戶線程同時執行。一邊產生垃圾,一邊標記(最耗時的階段并發執行)。
 - 重新標記(STW):對并發標記的過程中新產生的垃圾進行重新標記 / 取消標記,修正錯標。由于 CMS 的并發標記存在并發漏標的問題,所以 CMS 的 remark 階段必須從頭掃描一遍,耗時很長,也是為什么 JDK 默認并沒有使用 CMS 的 原因。
 - 并發清理:開啟用戶線程,同時 GC 線程開始對未標記的區域做清掃。清理的過程也會產生新的“浮動垃圾”,需要等下一次CMS重新運行的時候再次清理,影響不大。
 
 -  
CMS 存在的 問題:
-  
Memory Fragmentation 內存碎片 問題
-  
標記清除會產生碎片化,如果老年代不能再分配位置,CMS會讓 Serial Old 來清理,效率很低。
 -  
解決方案:
-XX:+UseCMSCompactAtFullCollection 在 FGC 時進行壓縮
-XX:CMSFullGCsBeforeCompaction 多少次 FGC 之后進行壓縮,默認是 0
 
 -  
 -  
Floating Garbage 浮動垃圾 問題
-  
如果老年代滿了,浮動垃圾還沒有清理完,會讓 Serial Old 清理。
 -  
解決方案:
降低觸發 CMS 的閾值,保持老年代有足夠的空間
-XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后開始CMS收集。實際回收的時候它是一個近似值,可能沒達到這個值就已經觸發了。默認是68%,有人說是 92%,好像有個計算公式,沒有去深究。如果頻繁發生SerialOld卡頓,應該把它調小,但是調小的缺點是頻繁CMS回收。
如何查看這個默認值?
java -XX:+PrintFlagsFinal -version | grep CMSInitiatingOccupancyFraction 
 -  
 
 -  
 
4、G1 回收器
開創了收集器面向局部收集的設計思路和基于 Region 的內存布局,主要面向服務端,最初設計目標是替換 CMS
如果你生產是 1.8 的 jdk,推薦使用 G1 回收器。啟動方式:
java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+Use G1GC com.mashibing.jvm.gc.T15_FullGC_Problem01G1的特點:適用于需要特別快的響應時間的場景(不需要很高吞吐量)。可由用戶指定期望停頓時間是 G1 的一個強大功能,但該值不能設得太低,一般設置為100~300 ms
G1的新老年代的比例是動態的,默認年輕代占用 5%-60%,一般不用手工指定,也不要手工指定。因為這是G1預測停頓時間的基準。它會根據上次回收的時間,進行新老年代的比例的動態調整。
G1 的一些概念:
1、card table 卡表
- 基于 card table,將堆空間劃分為一系列2^n大小的 card page
 - card table 用 bitmap 來實現,用于標記卡頁的狀態,每個 card table 項對應一個 card page
 - 當對一個對象引用進行寫操作時(對象引用改變),寫屏障邏輯會標記對象所在的 card page 為 dirty
 
2、Cset(collection set)
Cset 是一組可以被回收的分區的集合。它里面記錄了有哪些對象需要被回收。
3、Rset(remembered set)
Rset 中有一個 Hash 表,里面記錄了其它 region 中的對象到本 region 的引用。
Rset 的價值在于,它使得垃圾收集器不需要掃描整個堆,去找誰引用了當前分區中的對象,只需要掃描 Rset 即可。
Rset 會不會影響賦值的效率?會!由于Rset的存在,那么每次給對象復制引用的時候,需要在Rset中做一些額外的記錄,比如說記錄有哪些引用指向了我的對象等等,這些操作在GC中被稱為寫屏障。此處不同于內存屏障,是GC專有的寫屏障。NO Silver Bullet!只有特定條件下特定的解決方案,沒有通用的解決方案。
G1 的回收過程:
-  
把內存空間分為一塊一塊的 region,把內存化整為零。
 -  
G1 收集器在后臺維護了一個優先列表,當需要進行垃圾回收時,會優先回收存活對象最少的 region,也就是垃圾最多的 region,這就是“Garbage-First 垃圾優先”。每一個 region 都有自己的邏輯分代:
- old
 - suvivor
 - eden
 - humongous 存放巨型對象(跨越多個region的對象)
 
 -  
G1 的 GC 分為三種,不同種類的 GC 可能會同時進行。比如 YGC 和 MixedGC 的 initial mark 同時進行
-  
YGC
 -  
MixedGC:比如YGC已經回收不過來的,堆內存空間超過了45%,默認就啟動了MixedGC。
-  
MixedGC 發生的閾值可以自行設定
 -  
MixedGC 相當于一套完整的 CMS
-  
初始標記 需要STW
標記 GC Roots 能直接關聯到的對象,讓下一階段用戶線程并發運行時能正確地在可用 Region 中分配新對象。需要 STW 但耗時很短,在 Minor GC 時同步完成
 -  
并發標記 不需要STW
從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆的對象圖。耗時長但可與用戶線程并發,掃描完成后要重新處理 SATB 記錄的在并發時有變動的對象,三色標記 算法
 -  
最終標記 需要STW(重新標記)
對用戶線程做短暫暫停,處理并發階段結束后仍遺留下來的少量 SATB 記錄
 -  
篩選回收 需要STW(并行)
對各 Region 的回收價值排序,根據用戶期望停頓時間制定回收計劃。必須暫停用戶線程,由多條收集線程并行完成
 
 -  
 
 -  
 -  
FullGC:G1 也是有 FGC 的,對象分配不下的時候,就會產生 FGC。我們說 G1 和 CMS 調優目標之一就是盡量不要有 FGC,但這并不容易達到。因此有了面試題:如果G1產生FGC,你應該做什么?
 - 擴內存
 - 提高CPU性能(回收的快,業務邏輯產生對象的速度固定,垃圾回收越快,內存空間越大)
 - 降低MixedGC觸發的閾值,讓MixedGC提早發生(默認是45%)
 
 -  
 
并發標記算法 - 三色標記算法
-  
CMS 和 G1 用到的都是 三色標記 算法,“三色”只是邏輯概念,并不是真的顏色。
- 白色:未被標記的對象,也就是還沒有遍歷到的節點
 - 灰色:自身標記完成,沒來得及標記成員變量
 - 黑色:自身和成員變量均標記完成
 
 -  
漏標:本來是 live object,但是由于沒有遍歷到,被當成 garbage 回收掉了。例如,在 remark 的過程中,B 原來指向 D 的線消失了,但是 A 又指向了 D。如果不對 A 進行重新掃描,則會漏標,導致D被當做垃圾回收掉。
 -  
漏標解決方案:
-  
Incremental update,CMS 方案:增量更新,關注引用的 增加,把黑色的 A 重新標記為灰色,下次重新掃描它。
 -  
SATB(snapshot at the begining),G1方案:關注引用的 刪除。當 B->D 消失時,要把這個引用推到GC的堆棧,保證D還能被GC掃描到。下次掃描時會拿到這個引用,由于有Rset的存在,不需要掃描整個堆去查找指向白色的引用,效率比較高。
SATB配合Rset,渾然天成。
 
 -  
 
5、ZGC
美團技術團隊:新一代垃圾回收器ZGC的探索與實踐
ZGC 用了顏色指針
一些拓展知識
- 阿里的多租戶 JVM:把一個 JVM 分為還幾個小塊,分給租戶用
 - 專門針對 web application 的,session base 的 JVM。請求來訪問的時候產生的垃圾,在請求結束之后被回收
 
JVM 常用命令參數
HotSpot參數分類
標準: - 開頭,所有的HotSpot都支持 非標準:-X 開頭,特定版本HotSpot支持特定命令 不穩定:-XX 開頭,下個版本可能取消調優前的基礎概念:
所謂調優,首先確定:是吞吐量優先,還是響應時間優先?還是在一定的響應時間下,要求達到多大的吞吐量?
-  
吞吐量:用戶代碼時間 /(用戶代碼執行時間 + 垃圾回收時間)
- 科學計算、數據挖掘,一般吞吐量優先
 - PS + PO
 
 -  
響應時間:STW越短,響應時間越好
- 網站、GUI、API,一般響應時間優先
 - 1.8 G1
 
 
并發:淘寶雙11并發歷年最高54萬,據說12306并發比淘寶更高,號稱上百萬
- TPS
 - QPS,Query Per Second
 
什么是調優?
1、如何根據需求進行 JVM 規劃和預調優?
有人要問你,你應該選用多大的內存?什么樣的垃圾回收器組合?你怎么回答?
- 要有實際的業務場景,才能討論調優
 - 要有監控,能夠通過壓力測試看到結果
 
步驟:
(1)熟悉業務場景,選擇合適的垃圾回收器。是追求吞吐量,還是追求響應時間?
(2)計算內存需求。沒有一定之規,是經驗值。 1.5G -> 16G,突然卡頓了,為啥?
(3)選定CPU。預算能買到的,當然是越高越好,CPU多核,可以多線程運行呀!
(4)設定年代大小、升級年齡
(5)設定 日志參數
這個是 Java 虛擬機的參數,也可以在Tomcat里面配置,貌似是在叫 catalina options 里面指定java日志的參數
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause HelloGC5個日志文件循環產生,生產中的日志一般是這么設置,%t是生成時間的意思。
(6)觀察日志情況
案例1:垂直電商,最高每日百萬訂單,處理訂單系統需要什么樣的服務器配置?
這個問題比較業余,因為很多不同的服務器配置都能支撐(1.5G 16G 都有可能啊)
我們做一個假設吧,1小時360000個訂單。在集中時間段, 100個訂單/秒,(找一小時內的高峰期,可能是1000訂單/秒)。我們就要找到這個最高峰的時間,保證你的架構能夠承接的住。
大多數情況下,是靠經驗值,然后做壓測。
如果非要計算的話,你預估一下,一個訂單對象產生需要多少內存?512K * 1000 = 500M
專業一點的問法:要求響應時間在多少時間的情況下,比如100ms,我們去挑一個市面上性價比比較高的服務器,做壓測去測試,再不行加內存,再不行,就上云服務器…
 這樣說就OK了
案例2:12306遭遇春節大規模搶票應該如何支撐?(架構上的一個設計,與調優關系不大)
12306應該是中國并發量最大的秒殺網站:號稱并發量最高100W
架構模型:CDN -> LVS -> NGINX -> 業務系統 -> 100臺機器,每臺機器1W并發(單機10K問題),目前這個問題主要用Redis解決
業務流程:普通電商訂單 -> 下單 -> 訂單系統(IO)減庫存 -> 生成訂單,等待用戶付款
12306的一種可能的模型,是異步來進行的: 下單 -> 減庫存 和 訂單(redis kafka) 同時異步進行 ->等付款,付完款,持久化到Hbase, MySQL等等
減庫存最后還會把壓力壓到一臺服務器,怎么辦?可以做分布式本地庫存 + 單獨服務器做庫存均衡
大流量的處理方法:分而治之,每臺機器只減自己機器上有的庫存
流量傾斜的問題怎么解決?比如有的機器上已經沒庫存了,有的機器上還剩很多?這時候你還需要一臺單獨的服務器,去做所有服務器的平衡,如果某臺服務器沒庫存了,從別的機器上挪一些過去。
2、優化運行JVM運行環境(慢,卡頓)
案例1:升級內存后反而網站更卡
有一個50萬PV的文檔資料類網站(從磁盤提取文檔到內存)原服務器32位,1.5G的堆,用戶反饋網站比較緩慢。因此公司決定升級,新的服務器為64位,16G的堆內存,結果用戶反饋卡頓十分嚴重,反而比以前效率更低了!
為什么原網站慢?
因為很多用戶瀏覽數據,很多用戶瀏覽導致很多數據Load到內存,產生了很多文檔對應的Java包裝對象(而不是文檔對象,文檔本身可以走Nginx)。內存不足,頻繁GC,STW長,響應時間變慢
為什么會更卡頓?
內存越大,FGC時間越長
怎么解決?
PS 換成 PN + CMS,或者 G1
 或者業務上的調整,文檔不走JVM
案例2:系統CPU經常100%,如何排查、調優?
推理過程是:CPU 100%,那么一定有線程在占用系統資源,所以
找出哪個進程cpu高(top 命令)
該進程中的哪個線程cpu高(top -Hp)
如果是java程序,導出該線程的堆棧 (jstack命令,列出當前程序有哪些線程,以及線程編號,使用arthas工具可以heapdump導出堆棧),導出之后就可以用圖形界面了,也可以使用 jmap 分析堆文件,看哪個對象占用內存過多,再去分析業務邏輯。生產環境中,jmap 命令比 archas 好用一些,但是都會導致服務暫停。生產環境一般直接配置參數 -XX:+HeapDumpOnOutOfMemoryError
archas 用的是 agent,可以線上替換 class
(查找哪個方法(棧幀)消耗時間,哪個方法調用的哪個方法 (jstack),然后去看這個方法的代碼)
要區分是業務線程占比高 / 垃圾回收線程占比高?
案例3:系統內存飆高,如何查找問題?
如何監控 JVM?
可以使用 jstat jvisualvm jprofiler arthas top…等等
3、解決JVM運行過程中出現的各種問題(不完全等同于解決OOM的問題,因為前面兩項也很重要)
OOM 案例
- C++ 轉 Java 的程序員重寫了 finalize 方法,當一個對象被回收的時候,這個函數默認被調用,如果你在這個函數中寫了很多業務邏輯,回收這個對象就要花好長時間,對象產生速度大于回收速度,導致了 OOM
 
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GSuTQaAf-1604373530280)(images/image-20200829191206740.png)]
總結
以上是生活随笔為你收集整理的面试必会系列 - 1.6 Java 垃圾回收机制的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 数据结构: 试用判定树的方法给出在中序线
 - 下一篇: 操作系统:第二章 进程管理1 - 进程、