技術問答集錦(三)

猿碼道發表於2018-01-10

1 Java基礎

1.1 編譯型語言VS解釋型語言

編譯型語言:程式在執行之前需要一個專門的編譯過程,把程式編譯成為機器語言的檔案,執行時不需要重新翻譯,直接使用編譯的結果就行了。因此效率比較高。比如 C 語言。

解釋型語言:程式不需要編譯,程式在執行時才翻譯成機器語言,每執行一次都要翻譯一次。因此效率比較低。比如Basic語言,專門有一個直譯器能夠直接執行Basic程式,每個語句都是執行的時候才翻譯。

C語言是編譯型的。C程式——>機器語言(編譯)

Java比較特殊,Java程式也需要編譯,但是沒有直接編譯成機器語言,而是編譯成位元組碼,然後用解釋方式執行位元組碼。 Java程式—— >位元組碼(編譯)—— >機器語言(解釋)

1.2 JVM工作原理

JVM 主要由 ClassLoader執行引擎 兩子系統組成,執行資料區分為五個部分: 方法區、堆、棧、程式計數器、本地方法棧。其中的方法區和堆是所有執行緒共享的,JVM將臨時變數放在棧中,每個執行緒都有自己獨立的棧空間和程式計數器。

任何一個Java類的main函式執行都會建立一個JVM例項,JVM例項啟動時預設啟動幾個守護執行緒,比如:垃圾回收的執行緒,而 main 方法的執行是在一個單獨的非守護執行緒中執行的。只要非守護執行緒結束JVM例項就銷燬了。

那麼在Java類main函式執行過程中,JVM的工作原理如下:

  1. 根據系統環境變數,建立裝載JVM的環境與配置;
  2. 尋找JRE目錄,尋找jvm.dll,並裝載jvm.dll;
  3. 根據JVM的引數配置,如:記憶體引數,初始化jvm例項;
  4. JVM例項產生一個 引導類載入器例項(Bootstrap Loader),載入Java核心庫,然後引導類載入器自動載入 擴充套件類載入器(Extended Loader),載入Java擴充套件庫,最後擴充套件類載入器自動載入 系統類載入器(AppClass Loader),載入當前的Java類;
  5. 當前Java類載入至記憶體後,會經過 驗證、準備、解析 三步,將Java類中的 型別資訊、屬性資訊、常量池 存放在方法區記憶體中,方法指令直接儲存到棧記憶體中,如:main函式;
  6. 執行引擎開始執行棧記憶體中指令,由於main函式是靜態方法,所以不需要傳入例項,在類載入完畢之後,直接執行main方法指令;
  7. main函式執行主執行緒結束,隨之守護執行緒銷燬,最後JVM例項被銷燬;

1.3 JVM類載入機制

類載入是Java程式執行的第一步,在java.lang包裡有個ClassLoader類,ClassLoader 的基本目標是 對類的請求提供服務,按需動態裝載類和資源 ,只有當一個類要使用 (1. Class.forName();2. 呼叫類的靜態方法;3. 使用 new 關鍵字來例項化一個類) 的時候,類載入器才會載入這個類並初始化。當我們自定義類載入器載入類檔案時(繼承自ClassLoader類,只需覆蓋 findClass方法,即可),其類載入機制如下:

當自定義類載入器載入類時,會呼叫loadClass方法載入類,而由於類載入的雙親委託模式,會將類的載入代理給父類載入器:系統類載入器來完成,依次類推至最頂層引導類載入器載入,如果父類載入器沒有載入到類,則最終返回由自定義類載入器載入類,通過雙親委託模式,對於 Java 核心庫的類的載入工作由引導類載入器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類

需要說明一下Java虛擬機器是如何判定兩個Java 類是相同的。Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。然而,類載入器又分初始類載入器定義類載入器,由於類載入的代理模式,初始類載入器並不一定是定義類載入器,所以確切的說,判定兩個 Java 類是否相同的, 哪個類載入器啟動類的載入過程並不重要,重要的是最終定義這個類的載入器

1.4 JVM記憶體分配策略

JVM記憶體主要是指執行資料區,粗略的分為:堆、棧,細緻區分五部分:方法區、堆、棧、程式計數器、本地方法棧

  1. 程式計數器: 執行緒私有,記錄執行緒所執行的虛擬機器位元組碼指令的地址;如果正在執行的是native方法,這個計數值則為空。唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

  2. Java虛擬機器棧: 執行緒私有,描述Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀,用於儲存 區域性變數表,運算元棧,動態連結,方法出口 等資訊。每一個方法從呼叫直至執行完成的過程,就對應這一個棧幀在虛擬機器棧中入棧到出棧的過程。棧幀是方法執行時的基礎資料結構。如果執行緒請求棧深度大於虛擬機器所允許的深度,丟擲StackOverflowError異常;如果虛擬機器棧動態擴充套件時無法申請到足夠的記憶體,丟擲 OutOfMemoryError異常。

  3. 本地方法棧: 執行緒私有,描述native方法執行的記憶體模型。有的虛擬機器(如Sun HotSpot虛擬機器)直接就把 本地方法棧和虛擬機器棧 合二為一。

  4. Java堆: 執行緒共享,存放物件例項及陣列,是垃圾收集器管理的主要區域,採用分代收集策略,所以Java堆會細分為新生代和老年代。根據Java虛擬機器規範的規定,Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上連續的即可。如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,丟擲OutOfMemoryError異常。

  5. 方法區執行緒共享,儲存已被虛擬機器載入的類資訊、常量池、靜態變數、即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫Non-Heap(非堆),目的應該是與Java堆區分開來。如果方法區無法滿足記憶體分配需求,丟擲OutOfMemoryError異常。對於HotSpot虛擬機器,方法區被稱為永久代,本質上兩者不等價,僅僅是因為HotSpot虛擬機器設計團隊選擇把GC分代收集擴至方法區,或者說使用永久代來實現方法區而已

    JVM堆一般又可以分為以下三部分:Young 新生代、Tenured 老年代、Perm 永久代;

    Perm永久代主要儲存class,method,filed物件,這部分的空間一般不會溢位,除非一次性載入了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤。

    Tenured老年代主要儲存生命週期長的物件,多次未被GC掉的物件

    Young新生代主要儲存新生成物件,根據JVM的策略,在經過幾次垃圾收集後,而沒有被垃圾回收的物件將被移動到Tenured區間。有時候該區經常會遇到java.lang.OutOfMemoryError :Java heap space的錯誤。

    -Xms:指定了JVM初始啟動以後 初始化記憶體

    -Xmx:指定JVM堆得 最大記憶體,在JVM啟動以後,會分配-Xmx引數指定大小的記憶體給JVM,但是不一定全部使用,JVM會根據-Xms引數來調節真正用於JVM的記憶體;

    -Xmn:引數設定了新生代的大小;老年代等於-Xmx減去-Xmn;

    -XX:Xss:引數設定了永久代的大小;

  6. 直接記憶體: 不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域。但是這部分記憶體也被頻繁的使用,而且也可能導致OutOfMemoryError異常出現。例如:NIO類,引入了一種基於通道(Channel)與緩衝區(Buffer)的IO方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了Java堆和Native堆中來回複製資料。直接記憶體雖然不會受到Java堆大小的限制,但是,既然是記憶體,仍會受到本機總記憶體大小及處理器定址空間的限制。

  7. JVM引數:

    -XX:+HeapDumpOnOutOfMemoryError:可讓虛擬機器在記憶體溢位異常時Dump出當前的記憶體堆轉儲快照以便事後分析;

    -Xss:設定執行緒棧大小;

    -XX:PermSize:設定永久代初始大小;

    -XX:MaxPermSize:設定永久代最大大小;

    -XX:MaxDirectMemorySize:設定直接記憶體大小,預設與Java堆最大值一樣

1.5 物件是否可回收

  1. 引用計數演算法 儲存對特定物件的所有引用數,也就是說,當應用程式建立引用以及引用超出範圍時,JVM必須適當增減引用數。當某物件的引用數為0時,便可以進行垃圾收集。

    優點:實現簡單、效率高;

    缺點:很難解決物件之間相互引用問題;

  2. 可達性分析演算法 通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

    可作為GC Roots的物件包括:

    1. 虛擬機器棧(棧幀的本地變數表)中引用的物件;
    2. 方法區中類靜態屬性引用的物件;
    3. 方法區中常量引用的物件;
    4. 本地方法棧中JNI(即一般說的是native方法)引用的物件;

1.6 四種引用

  1. 強引用:只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件
  2. 軟引用:對軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。
  3. 弱引用:對弱引用關聯著的物件,只能生存到下一次垃圾收集發生之前。
  4. 虛引用:物件是否有虛引用,完全不會對其生存時間構成影響,也無法通過虛引用來取得物件例項。關聯虛引用唯一目的就是能在物件被收集器回收時收到系統通知。

1.7 finalize()方法

任何一個物件的finalize()方法都僅會被系統自動呼叫一次。如果物件面臨下一次回收,它的finalize()方法不會被再次執行。建議避免使用該方法。

1.8 JVM垃圾回收策略

GC即垃圾收集機制是指JVM用於釋放那些不再使用的物件所佔用的記憶體。常用機制:

  1. 標記-清除演算法【適用於老年代】 首先根據可達性分析演算法,標記出所有需要回收的物件,在標記完成後統一回收所有標記的物件。

    缺點:效率問題:標記、清除兩個過程效率都低;空間問題:標記清除之後產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中,需要分配大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

  2. 複製演算法【空間換時間,適用於物件存活率低的新生代】 將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當一塊的記憶體用完了,就將還存活著的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。

    優點:實現簡單、效率高、無記憶體碎片;

    缺點:記憶體使用率低,太浪費。

    由於新生代中的物件98%是朝生夕死的,所以 將記憶體分為一塊較大的記憶體、兩塊較小的記憶體。每次使用一塊大記憶體和一塊小記憶體,當垃圾回收時,會將該大記憶體和小記憶體中存活的物件一次性的複製到另外一塊小記憶體中,最後清理掉該大記憶體、小記憶體。但是如果物件的存活率較高,那麼當複製物件至另一塊小記憶體時,該小記憶體空間會不夠用,則需要依賴其他記憶體(老年代)進行分配擔保

    當物件的存活率較高時,複製演算法要進行較多的複製操作,效率會變低。

  3. 標記-整理演算法【適用於老年代】

    標記過程與“標記-清除”演算法一樣,唯一區別是在後續步驟不是直接對記憶體進行清除,而是先讓所有活著的物件都向一端移動,然後直接清除掉端邊界以外的記憶體

  4. 分代收集策略 一般是把Java堆分成 新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。

    新生代中物件存活率低,採用複製演算法,有老年代對它進行分配擔保

    老年代中物件存活率高,無額外空間對它進行分配擔保,必須採用 “標記-清除”或“標記-整理” 演算法。

  5. 回收方法區(永久代) 很多人認為方法區(永久代)是沒有垃圾收集的,Java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集,況且在方法區中進行垃圾收集價效比很低;

    永久代的垃圾收集主要回收兩方面: 廢棄常量和無用的類

    廢棄常量:判斷常量池的物件是否還存在任何引用;

    無用的類:(1)該類的所有例項都被回收;(2)該類的ClassLoader已被回收;(3)該類的Class物件沒有任何引用;

1.9 垃圾收集器

JDK1.7 Update14之後的HotSpot虛擬機器正式提供了G1收集器;

  1. Serial收集器: 單執行緒、Stop The Wold、Client模式預設新生代收集器複製演算法

  2. ParNew收集器: Serial多執行緒版、並行、Stop The Wold、Server模式預設新生代收集器、只有該收集器能與CMS收集器配合、 複製演算法

    ParNew收集器也是使用:

    -XX:+UseConcMarkSweepGC 選項後的預設新生代收集器;

    -XX:+UserParNewGC 選項來強制指定它;

    -XX:ParallelGCThreads 引數來限制垃圾收集的執行緒數,預設開啟的執行緒數與CPU的數量相等;

  3. Parallel Scavenge收集器: 多執行緒、並行、Stop The Wold、 新生代收集器、Server模式複製演算法; CMS等收集器的關注點是 儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而 該收集器關注的是達到一個可控制的吞吐量

    所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),吞吐量與垃圾收集時間成反比;

    停頓時間越短就越適合需要與使用者互動的程式,而高吞吐量則可以高效率的利用CPU,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務;

    -XX:MaxGCPauseMillis:設定最大垃圾收集停頓時間;

    -XX:GCTimeRatio:設定吞吐量大小,大於0且小於100的整數,預設為99;

    -XX:UseAdaptiveSizePolicy:設定開啟GC自適應的調節策略,以達到最大的吞吐量;

  4. Serial Old收集器: 單執行緒、Stop The Wold、 老年代標記-整理演算法、Client模式;作為CMS收集器的備選方案,在併發收集發生Concurrent Mode Failure時使用;

  5. Parallel Old收集器: 多執行緒、Stop The Wold、 老年代標記-整理演算法、Server模式、並行;適合與Parallel Scavenge配合使用

  6. CMS收集器: 多執行緒、Stop The Wold、 老年代標記-清除演算法、Server模式、併發; 缺點:記憶體碎片

    -XX:+UseCMSCompactAtFullCollection:預設開啟,用於在CMS收集器頂不住進行Full GC時開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發,會導致停頓時間變長;

    -XX:CMSFullGCsBeforeCompaction:用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的GC,預設為0,表示每次進行Full GC時都進行碎片整理;

  7. G1收集器: 多執行緒、新老年代複製+標記-整理演算法、併發、Server模式、GC整個堆; 特點:並行與併發、分代收集、空間整理、可預測的停頓

    G1雖然儲存了新老年代的概念,但已經不是物理分割了,他們都是一部分Region(不需要連續)的集合

    args="-Dfile.encoding=UTF-8 
    -J-server 
    -J-Xss128k
    -J-XX:ThreadStackSize=128
    -J-XX:PermSize=64m -J-XX:MaxPermSize=256m 
    -J-verbose:gc -J-XX:+PrintGCDetails -J-XX:+PrintGCTimeStamps 
    -Djava.library.path=${RESIN_HOME}/libexec:/opt/j2sdk/lib:/usr/lib64 
    -Djmagick.systemclassloader=false -DNO_TIMEOUT"
    
    args="$args -Xdebug - Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9090"
    args="$args -Xmn5g -Xms10g -Xmx10g"
    args="$args -J-XX:+UseParNewGC -J-XX:+UseConcMarkSweepGC"
    複製程式碼

    Client模式:預設-XX:+UseSerialGC,Serial + Serial Old; Server模式:預設-XX:+UseParallelGC,Parallel Scavenge + Serial Old;

    -XX:+PrintGC             列印GC資訊
    -XX:+PrintGCDetails   列印較為詳細的GC資訊
    -XX:+PrintGCTimeStamps  列印GC時間戳,相對於應用程式啟動的時間
    複製程式碼

    Serial:序列收集器,當進行垃圾收集時,會暫停所有執行緒; Parallel:並行收集器,是序列收集器的多執行緒版本,多CPU下; ParallelOld:老年代的Parallel版本; ConcMarkSweep:簡稱CMS,是併發收集器,將部分操作與使用者執行緒併發執行; CMSIncrementalMode:CMS收集器變種,屬增量式垃圾收集器,在併發標記和併發清理時交替執行垃圾收集器和使用者執行緒; G1:面向伺服器端應用的垃圾收集器,計劃未來替代CMS收集器;

GC收集器

1.10 JVM引數配置

  1. 跟 Java 堆大小相關的 JVM 記憶體引數

    下面三個 JVM 引數用來指定堆的初始大小和最大值以及堆疊大小:

    -Xms 設定 Java 堆的初始化大小

    -Xmx 設定最大的 Java 堆大小

    -Xss 設定Java執行緒棧大小

  2. 關於列印垃圾收集器詳情的 JVM 引數

    -verbose:gc 記錄 GC 執行以及執行時間,一般用來檢視 GC 是否是應用的瓶頸

    -XX:+PrintGCDetails 記錄 GC 執行時的詳細資料資訊,包括新生成物件的佔用記憶體大小以及耗費時間等

    -XX:+PrintGCTimeStamps 列印垃圾收集的時間戳

  3. 設定 Java 垃圾收集器行為的 JVM 引數

    -XX:+UseParallelGC 使用並行垃圾收集

    -XX:+UseConcMarkSweepGC 使用併發標誌掃描收集 (Introduced in 1.4.1)

    -XX:+UseSerialGC 使用序列垃圾收集 (Introduced in 5.0.)

    需要提醒的是,但你的應用是非常關鍵的、交易非常頻繁應用時,應該謹慎使用 GC 引數,因為 GC 操作是耗時的,你需要在這之中找到平衡點。

  4. JVM除錯引數,用於遠端除錯

    -Xdebug -Xnoagent 
    -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
    複製程式碼
  5. 用於修改 Perm Gen 大小的 JVM 引數

    下面的這三個引數主要用來解決 JVM 錯誤:java.lang.OutOfMemoryError:Perm Gen Space

    -XX:PermSize and MaxPermSize
    -XX:NewRatio=2  Ratio of new/old generation sizes.
    -XX:MaxPermSize=64m     Size of the Permanent Generation.
    複製程式碼
  6. 用來跟蹤類載入和解除安裝的資訊

    -XX:+TraceClassLoading-XX:+TraceClassUnloading 用來列印類被載入和解除安裝的過程資訊,這個用來診斷應用的記憶體洩漏問題非常有用。

  7. 用於除錯目的的 JVM 開關引數

    -XX:HeapDumpPath=./java_pid.hprof  Path to directory or file name for heap dump.
    -XX:+PrintConcurrentLocks       Print java.util.concurrent locks in Ctrl-Break thread dump.
    -XX:+PrintCommandLineFlags   Print flags that appeared on the command line.
    複製程式碼

    如果你的應用追求低停頓,那G1現在已經可以作為一個可嘗試的選擇;如果你的應用追求吞吐量,那G1並不會為你帶來什麼特別的好處;

1.11 Stop The Wold

可達性分析必須在一個能確保一致性的快照中進行,一致性是指在整個分析過程中整個執行系統必須凍結,不可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話可達性分析結果不準確。所以這點是導致GC進行時必須停頓所有執行緒的原因。

1.12 新生代GC、老年代GC

新生代GC:Minor GC,指發生在新生代的垃圾收集動作;

老年代GC:Major GC/Full GC,指發生在老年代垃圾收集動作,出現了Major GC,經常會伴隨至少一次的Minor GC。會發生Stop The Wold。

1.13 物件進入老年代

  1. 大物件:通過引數 -XX:PretenureSizeThreshold=3145287,令大於這個設定值的物件直接在老年代分配。以避免大物件在新生代分配,從而觸發新生代GC。

  2. 多次GC仍存活的物件:當物件每”熬過“一次Minor GC,物件年齡就增加1歲,當物件年齡增加到一定程度 (預設15歲),就會晉升到老年代中。物件晉升老年代的年齡閥值,可以通過引數 -XX:MaxTenurigThreshold 設定。

  3. 動態物件年齡判定:虛擬機器並不是永遠地要求物件年齡必須達到MaxTenurigThreshold閥值才能晉升到老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenurigThreshold閥值

  4. 新生代—分配擔保:由於新生代採用複製收集演算法,當新生代Minor GC 時,如果存活物件的大小大於Survivor,依據分配擔保策略會將Survivor無法容納的物件直接存入老年代。如果老年代仍不能夠存放剩餘的物件,則會發生Major GC/Full GC,就會Stop The Wold。

1.14 JVM常量池機制

常量池其實就是方法區一個記憶體空間,虛擬機器必須為每個被裝載的型別維護一個常量池。常量池就是該型別所用到常量的一個有序集和。以字串為例,在Java原始碼中的每一個字面值字串,都會在編譯成class檔案階段,形成標誌號為 8(CONSTANT_String_info)的常量表 。當JVM載入 class檔案的時候,會為對應的常量池建立一個記憶體資料結構,並存放在方法區中。 如下程式碼:

public class Test{   
    private String str="我們"。
}
複製程式碼

將Test編譯之後形成class檔案,那麼在class檔案中"我們"會以一種 CONSTANT_UTF8_info 表的形式存在,位元組序列如下:1 0 6 230 136 145 228 187 172 1表示常量表的型別,0 6表示有6個位元組的長度。後面6個位元組是UTF-8編碼的“我們”。當JVM執行的時候會將這些常量池的資訊載入進方法區。也就是說在執行過程中記憶體儲存的"我們"是UTF-8編碼的。

1.15 為什麼使用JVM

Java語言的一個非常重要的特點就是 平臺無關性。而使用JVM是實現這一特點的關鍵。一般的高階語言如果要在不同的平臺上執行,至少需要編譯成不同的目的碼。而引入JVM後,Java語言在不同平臺上執行時不需要重新編譯。Java語言使用JVM遮蔽了與具體平臺相關的資訊,使得Java語言編譯程式只需生成在JVM虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。JVM在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行

1.16 java.lang.OutOfMemoryError: unable to create new native thread

這個異常問題本質原因是我們 建立了太多的執行緒,而能建立的執行緒數是有限制的,導致了異常的發生。能建立的執行緒數的具體計算公式如下:

(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
複製程式碼

MaxProcessMemory:指的是一個程式的最大記憶體

JVMMemory:JVM記憶體

ReservedOsMemory:保留的作業系統記憶體

ThreadStackSize:執行緒棧的大小

在java語言裡, 當你建立一個執行緒的時候,虛擬機器會在JVM記憶體建立一個Thread物件同時建立一個作業系統執行緒,而這個系統執行緒的記憶體用的不是JVMMemory,而是系統中剩下的記憶體(MaxProcessMemory - JVMMemory - ReservedOsMemory),可以下面方法解決問題:

(1) 通過設定 -Xmx512m 減少JVM Heap size;

(2) 通過設定 -Xss64k 減少執行緒佔用的Stack size;

(3) 增加作業系統記憶體

相關文章