
Java 教程
- Java - 首頁
- Java - 概述
- Java - 歷史
- Java - 特性
- Java vs. C++
- JVM - Java虛擬機器
- Java - JDK vs JRE vs JVM
- Java - Hello World 程式
- Java - 環境搭建
- Java - 基本語法
- Java - 變數型別
- Java - 資料型別
- Java - 型別轉換
- Java - Unicode 系統
- Java - 基本運算子
- Java - 註釋
- Java - 使用者輸入
- Java - 日期和時間
Java 控制語句
- Java - 迴圈控制
- Java - 決策制定
- Java - if-else
- Java - switch
- Java - for 迴圈
- Java - for-each 迴圈
- Java - while 迴圈
- Java - do-while 迴圈
- Java - break
- Java - continue
面向物件程式設計
- Java - 面向物件概念
- Java - 物件和類
- Java - 類屬性
- Java - 類方法
- Java - 方法
- Java - 變數作用域
- Java - 建構函式
- Java - 訪問修飾符
- Java - 繼承
- Java - 聚合
- Java - 多型
- Java - 重寫
- Java - 方法過載
- Java - 動態繫結
- Java - 靜態繫結
- Java - 例項初始化塊
- Java - 抽象
- Java - 封裝
- Java - 介面
- Java - 包
- Java - 內部類
- Java - 靜態類
- Java - 匿名類
- Java - 單例類
- Java - 包裝類
- Java - 列舉
- Java - 列舉建構函式
- Java - 列舉字串
Java 內建類
Java 檔案處理
Java 錯誤和異常
- Java - 異常
- Java - try-catch 塊
- Java - try-with-resources
- Java - 多重catch塊
- Java - 巢狀 try 塊
- Java - finally 塊
- Java - throw 異常
- Java - 異常傳播
- Java - 內建異常
- Java - 自定義異常
Java 多執行緒
- Java - 多執行緒
- Java - 執行緒生命週期
- Java - 建立執行緒
- Java - 啟動執行緒
- Java - 執行緒連線
- Java - 執行緒命名
- Java - 執行緒排程器
- Java - 執行緒池
- Java - 主執行緒
- Java - 執行緒優先順序
- Java - 守護執行緒
- Java - 執行緒組
- Java - 關閉鉤子
Java 同步
Java 網路程式設計
- Java - 網路程式設計
- Java - Socket 程式設計
- Java - URL 處理
- Java - URL 類
- Java - URLConnection 類
- Java - HttpURLConnection 類
- Java - Socket 類
- Java -泛型
Java 集合
Java 介面
Java 資料結構
Java 集合演算法
高階 Java
- Java - 命令列引數
- Java - Lambda 表示式
- Java - 傳送郵件
- Java - Applet 基礎
- Java - Javadoc 註釋
- Java - 自動裝箱和拆箱
- Java - 檔案不匹配方法
- Java - REPL (JShell)
- Java - 多版本 Jar 檔案
- Java - 私有介面方法
- Java - 內部類菱形運算子
- Java - 多解析度影像 API
- Java - 集合工廠方法
- Java - 模組系統
- Java - Nashorn JavaScript
- Java - Optional 類
- Java - 方法引用
- Java - 函式式介面
- Java - 預設方法
- Java - Base64 編碼解碼
- Java - switch 表示式
- Java - Teeing 收集器
- Java - 微基準測試
- Java - 文字塊
- Java - 動態 CDS 歸檔
- Java - Z 垃圾收集器 (ZGC)
- Java - 空指標異常
- Java - 打包工具
- Java - 密封類
- Java - 記錄類
- Java - 隱藏類
- Java - 模式匹配
- Java - 簡潔數字格式化
- Java - 垃圾回收
- Java - JIT 編譯器
Java 雜項
- Java - 遞迴
- Java - 正則表示式
- Java - 序列化
- Java - 字串
- Java - Process API改進
- Java - Stream API改進
- Java - 增強 @Deprecated 註解
- Java - CompletableFuture API改進
- Java - 流
- Java - 日期時間 API
- Java 8 - 新特性
- Java 9 - 新特性
- Java 10 - 新特性
- Java 11 - 新特性
- Java 12 - 新特性
- Java 13 - 新特性
- Java 14 - 新特性
- Java 15 - 新特性
- Java 16 - 新特性
Java API 和框架
Java 類引用
- Java - Scanner
- Java - 陣列
- Java - 字串
- Java - Date
- Java - ArrayList
- Java - Vector
- Java - Stack
- Java - PriorityQueue
- Java - LinkedList
- Java - ArrayDeque
- Java - HashMap
- Java - LinkedHashMap
- Java - WeakHashMap
- Java - EnumMap
- Java - TreeMap
- Java - IdentityHashMap
- Java - HashSet
- Java - EnumSet
- Java - LinkedHashSet
- Java - TreeSet
- Java - BitSet
- Java - Dictionary
- Java - Hashtable
- Java - Properties
- Java - Collection
- Java - Array
Java 有用資源
Java - 垃圾回收
一個Java物件的生命週期由JVM管理。一旦程式設計師建立了一個物件,我們就無需擔心它的其餘生命週期。JVM 將自動查詢不再使用的物件,並從堆中回收它們的記憶體。
什麼是 Java 垃圾回收?
垃圾回收是 JVM 執行的一項主要操作,對其進行調整以滿足我們的需求可以極大地提高應用程式的效能。現代 JVM 提供了各種各樣的垃圾回收演算法。我們需要了解應用程式的需求才能決定使用哪種演算法。
您不能像在 C 和 C++ 等非 GC 語言中那樣以程式設計方式釋放 Java 中的物件。因此,您在 Java 中不會有懸空引用。但是,您可能有空引用(引用指向 JVM 永遠不會儲存物件的記憶體區域)。每當使用空引用時,JVM 都會丟擲 NullPointerException。
請注意,由於GC,在 Java 程式中很少發現記憶體洩漏,但它們確實會發生。我們將在本章末尾建立一個記憶體洩漏。
垃圾收集器的型別
現代 JVM 使用以下 GC
- 序列收集器
- 吞吐量收集器
- CMS 收集器
- G1 收集器
上述每種演算法都執行相同的任務 - 查詢物件,這些物件不再使用,並回收它們在堆中佔用的記憶體。一種簡單的方法是計算每個物件具有的引用數,並在引用數變為 0 時將其釋放(這也稱為引用計數)。為什麼這是天真的?考慮一個迴圈連結串列。它的每個節點都會對其有一個引用,但是整個物件並沒有從任何地方被引用,理想情況下應該被釋放。
記憶體合併
JVM 不僅釋放記憶體,還會將小的記憶體塊合併成更大的記憶體塊。這樣做是為了防止記憶體碎片。
簡單來說,典型的 GC 演算法執行以下活動:
- 查詢未使用的物件
- 釋放它們在堆中佔用的記憶體
- 合併碎片
GC 在執行時必須停止應用程式執行緒。這是因為它在執行時會移動物件,因此無法使用這些物件。此類停止稱為“停止世界”暫停,我們的目標是在調整 GC 時最大限度地減少這些暫停的頻率和持續時間。
下面顯示了一個記憶體合併的簡單演示
陰影部分是需要釋放的物件。即使在回收所有空間後,我們也只能分配最大大小為 75Kb 的物件。即使我們有 200Kb 的可用空間,如下所示
垃圾回收中的代
大多數 JVM 將堆分為三代:年輕代 (YG)、老年代 (OG) 和永久代(也稱為終身代)。
我們來看一個簡單的例子。Java 中的 String 類是不可變的。這意味著每次您需要更改 String 物件的內容時,都必須建立一個全新的物件。假設您在迴圈中對字串進行了 1000 次更改,如下面的程式碼所示:
String str = "G11 GC"; for(int i = 0 ; i < 1000; i++) { str = str + String.valueOf(i); }
在每次迴圈中,我們都會建立一個新的字串物件,而上一次迭代建立的字串就變得無用了(也就是說,沒有任何引用指向它)。該物件的生存期只有一次迭代——它們很快就會被垃圾收集器 (GC) 回收。這種短暫存在的物件儲存在堆的年輕代區域中。從年輕代收集物件的程序稱為次要垃圾回收,它總是會導致“stop-the-world”暫停。
次要垃圾回收
隨著年輕代被填滿,GC 會進行次要垃圾回收。已死亡的物件會被丟棄,而存活的物件會被移動到老年代。在此過程中,應用程式執行緒會停止。
在這裡,我們可以看到這種分代設計帶來的優勢。年輕代只是堆的一小部分,很快就滿了。但是處理它所需的時間遠遠小於處理整個堆所需的時間。因此,在這種情況下,“stop-the-world”暫停時間要短得多,儘管頻率更高。我們應該始終追求更短的暫停時間,即使它們可能更頻繁。
完全垃圾回收
年輕代被分為兩個空間——**伊甸園區 (eden)** 和**倖存者區 (survivor space)**。在伊甸園區收集期間倖存下來的物件會被移動到倖存者區,而那些在倖存者區倖存下來的物件會被移動到老年代。年輕代在收集時會被壓縮。
隨著物件被移動到老年代,它最終會被填滿,必須進行收集和壓縮。不同的演算法採用不同的方法來實現這一點。有些演算法會停止應用程式執行緒(這會導致長時間的“stop-the-world”暫停,因為老年代與年輕代相比相當大),而有些演算法則在應用程式執行緒繼續執行的同時併發地執行此操作。這個過程稱為完全 GC (full GC)。CMS 和 G1 就是兩種這樣的垃圾收集器。
垃圾收集器調優
我們也可以根據需要調整垃圾收集器。以下是可以根據情況配置的區域
- 堆大小分配
- 分代大小分配
- 永久代和元空間配置
讓我們在瞭解它們的影響的同時詳細瞭解每一個方面。我們還將討論基於可用記憶體、CPU 配置和其他相關因素的建議。
堆大小分配
堆大小是 Java 應用程式效能的一個重要因素。如果它太小,它將頻繁填滿,結果將不得不被 GC 頻繁收集。另一方面,如果我們只是增加堆的大小,雖然它需要收集的頻率較低,但暫停的長度會增加。
此外,增加堆大小會對底層作業系統造成嚴重的懲罰。透過分頁,作業系統使我們的應用程式程式看到的記憶體比實際可用的記憶體多得多。作業系統透過使用磁碟上的某些交換空間來管理這一點,將程式的非活動部分複製到其中。當需要這些部分時,作業系統會將它們從磁碟複製回記憶體。
讓我們假設一臺機器有 8G 記憶體,而 JVM 看到了 16G 虛擬記憶體,JVM 將不知道實際上系統上只有 8G 可用。它只會向作業系統請求 16G,一旦獲得該記憶體,它將繼續使用它。作業系統將不得不大量地交換資料進出,這對系統來說是一個巨大的效能損失。
然後是這種虛擬記憶體完全 GC 期間發生的暫停。由於 GC 將對整個堆進行收集和壓縮,它將不得不等待很長時間才能將虛擬記憶體從磁碟交換出去。如果是併發收集器,後臺執行緒將不得不等待很長時間才能將資料從交換空間複製到記憶體。
因此,這裡就出現瞭如何確定最佳堆大小的問題。第一條規則是永遠不要向作業系統請求超過實際存在的記憶體。這將完全防止頻繁交換的問題。如果機器安裝並執行多個 JVM,則所有 JVM 的總記憶體請求小於系統中實際存在的RAM。
您可以使用兩個標誌控制 JVM 的記憶體請求大小:
- -XmsN − 控制請求的初始記憶體。
- -XmxN − 控制可以請求的最大記憶體。
這兩個標誌的預設值取決於底層作業系統。例如,對於在 MacOS 上執行的 64 位 JVM,-XmsN = 64M,-XmxN = 最小 1G 或總物理記憶體的 1/4。
請注意,JVM 可以自動在這兩個值之間進行調整。例如,如果它注意到 GC 發生得太頻繁,它將不斷增加記憶體大小,只要它低於 -XmxN 並滿足所需的效能目標。
如果您確切知道您的應用程式需要多少記憶體,那麼您可以設定 -XmsN = -XmxN。在這種情況下,JVM 不需要計算堆的“最佳”值,因此 GC 過程變得更高效。
分代大小分配
您可以決定要為年輕代分配多少堆記憶體,以及要為老年代分配多少堆記憶體。這兩個值都會以以下方式影響應用程式的效能。
如果年輕代的大小非常大,那麼它將被收集的頻率較低。這將導致晉升到老年代的物件數量較少。另一方面,如果您過分增加老年代的大小,那麼收集和壓縮它將花費太多時間,這將導致長時間的 STW 暫停。因此,使用者必須在這兩個值之間找到平衡。
以下是您可以用來設定這些值的標誌:
- -XX:NewRatio=N:年輕代與老年代的比率(預設值為 2)
- -XX:NewSize=N:年輕代的初始大小
- -XX:MaxNewSize=N:年輕代的最大大小
- -XmnN:使用此標誌將 NewSize 和 MaxNewSize 設定為相同的值
年輕代的初始大小由 NewRatio 的值根據以下公式確定:
(total heap size) / (newRatio + 1)
由於 newRatio 的初始值為 2,上述公式得出年輕代的初始值為總堆大小的 1/3。您可以始終透過顯式指定年輕代的大小來覆蓋此值,使用 NewSize 標誌。此標誌沒有任何預設值,如果未顯式設定,年輕代的大小將繼續使用上述公式計算。
永久代和元空間配置
永久代和元空間是 JVM 保留類元資料的堆區域。在 Java 7 中,該空間稱為“永久代”,在 Java 8 中,它被稱為“元空間”。編譯器和執行時使用此資訊。
您可以使用以下標誌控制永久代的大小:-XX:PermSize=N 和 -XX:MaxPermSize=N。元空間的大小可以使用:-XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N 控制。
未設定標誌值時,永久代和元空間的管理方式存在一些差異。預設情況下,兩者都有一個預設的初始大小。但是,雖然元空間可以佔用所需的大量堆記憶體,但永久代最多隻能佔用預設初始值。例如,64 位 JVM 的最大永久代大小為 82M。
請注意,除非指定不佔用,否則元空間可以佔用無限量的記憶體,因此可能會出現記憶體不足錯誤。每當這些區域調整大小時,都會發生完全 GC。因此,在啟動過程中,如果有很多類正在載入,元空間可能會不斷調整大小,每次都會導致完全 GC。因此,對於大型應用程式來說,如果初始元空間大小太小,啟動時間會很長。最好增加初始大小,因為它可以減少啟動時間。
儘管永久代和元空間儲存類元資料,但它不是永久的,並且像物件一樣,空間會被 GC 回收。這通常是伺服器應用程式的情況。每當您向伺服器進行新的部署時,都必須清理舊的元資料,因為新的類載入器現在需要空間。此空間由 GC 釋放。