這個面試題是一個朋友在面試的時候碰到的,什么時候會拋出OutOfMemery異常呢?初看好像挺簡單的,其實深究起來考察的是對整個JVM的了解,而且這個問題從網上可以翻到一些亂七八糟的答案,其實在總結下來基本上4個場景可以概括下來。
堆內存溢出
堆內存溢出太常見,大部分人都應該能想得到這一點,堆內存用來存儲對象實例,我們只要不停的創建對象,並且保證GC Roots和對象之間有可達路徑避免垃圾回收,那么在對象數量超過最大堆的大小限制后很快就能出現這個異常。
寫一段代碼測試一下,設置堆內存大小2M。
public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<>(); while (true) { list.add(new HeapOOM()); } } }
運行代碼,很快能看見OOM異常出現,這里的提示是Java heap space堆內存溢出。
一般的排查方式可以通過設置-XX: +HeapDumpOnOutOfMemoryError在發生異常時dump出當前的內存轉儲快照來分析,分析可以使用Eclipse Memory Analyzer(MAT)來分析,獨立文件可以在官網下載。
另外如果使用的是IDEA的話,可以使用商業版JProfiler或者開源版本的JVM-Profiler,此外IDEA2018版本之后內置了分析工具,包括Flame Graph(火焰圖)和Call Tree(調用樹)功能。
方法區(運行時常量池)和元空間溢出
方法區和堆一樣,是線程共享的區域,包含Class文件信息、運行時常量池、常量池,運行時常量池和常量池的主要區別是具備動態性,也就是不一定非要是在Class文件中的常量池中的內容才能進入運行時常量池,運行期間也可以可以將新的常量放入池中,比如String的intern()方法。
我們寫一段代碼驗證一下String.intern(),同時我們設置-XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m 元空間大小。由於我使用的是1.8版本的JDK,而1.8版本之前方法區存在於永久代(PermGen),1.8之后取消了永久代的概念,轉為元空間(Metaspace),如果是之前版本可以設置PermSize MaxPermSize永久代的大小。
private static String str = "test"; public static void main(String[] args) { List<String> list = new ArrayList<>(); while (true){ String str2 = str + str; str = str2; list.add(str.intern()); } }
運行代碼,會發現代碼報錯。
再次修改配置,去除元空間限制,修改堆內存大小-Xms20m -Xmx20m,可以看見堆內存報錯。
這是為什么呢?intern()本身是一個native方法,它的作用是:如果字符串常量池中已經包含一個等 於此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,並且返回String對象的引用。
而在1.7版本之后,字符串常量池已經轉移到堆區,所以會報出堆內存溢出的錯誤,如果1.7之前版本的話會看見PermGen space的報錯。
直接內存溢出
直接內存並不是虛擬機運行時數據區域的一部分,並且不受堆內存的限制,但是受到機器內存大小的限制。常見的比如在NIO中可以使用native函數直接分配堆外內存就容易導致OOM的問題。
直接內存大小可以通過-XX:MaxDirectMemorySize指定,如果不指定,則默認與Java 堆最大值-Xmx一樣。
由直接內存導致的內存溢出,一個明顯的特征是在Dump文件中不會看見明顯的異常,如果發現OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。
棧內存溢出
棧是線程私有,它的生命周期和線程相同。每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,方法調用的過程就是棧幀入棧和出棧的過程。
在java虛擬機規范中,對虛擬機棧定義了兩種異常:
- 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常
- 如果虛擬機棧可以動態擴展,並且擴展時無法申請到足夠的內存,拋出OutOfMemoryError異常
先寫一段代碼測試一下,設置-Xss160k,-Xss代表每個線程的棧內存大小
public class StackOOM { private int length = 1; public void stackTest() { System.out.println("stack lenght=" + length); length++; stackTest(); } public static void main(String[] args) { StackOOM test = new StackOOM(); test.stackTest(); } }
測試發現,單線程下無論怎么設置參數都是StackOverflow異常。
嘗試把代碼修改為多線程,調整-Xss2m,因為為每個線程分配的內存越大,棧空間可容納的線程數量越少,越容易產生內存溢出。反之,如果內存不夠的情況,可以調小該參數來達到支撐更多線程的目的。
public class StackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { new Thread(() -> dontStop()).start(); } } public static void main(String[] args) throws Throwable { StackOOM stackOOM = new StackOOM(); stackOOM.stackLeakByThread(); } }