日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

运行时错误7内存溢出_JVM运行时内存数据区域

發布時間:2025/4/16 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 运行时错误7内存溢出_JVM运行时内存数据区域 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

閱讀本文大概需要5分鐘

作者:AI喬治出處:https://my.oschina.net/u/3611782/blog/4530512

1 討論背景

周志明老師寫的《深入理解Java虛擬機》應該很多程序員都讀過,第二章中闡述了Java虛擬機在執行Java程序的過程中是如何管理內存的,以及這些內存是如何被劃分成更細的邏輯區域的。如下圖所示,按照書中的論述JVM運行時數據區域包含以下幾個數據區[1]。

按照《Java虛擬機規范(Java SE 7版)》,各區域的功能簡要介紹如下:

  • 程序計數器:各線程私有。用于記錄每個線程下一條待執行的字節碼指令以及相關信息。這是唯一的不會拋出OOM異常的區域。

  • Java虛擬機棧:各線程私有。虛擬機棧由一個個的棧幀組成,每個棧幀包含了對應方法執行所需要的信息,具體包括:局部變量表、操作數棧(類似于編譯型語言體系下的數據寄存器)、動態鏈接(某些接口符號可能會動態的指向不同的目標方法)、函數返回地址以及其他一些相關信息。理論上當函數調用鏈超過棧的深度時就會觸發StackOverflow,當該區域設置為動態擴展時,虛擬機無法為棧申請到更多內存時就會觸發OOM。事實中基本上不管哪種情況,結果都很可能會是StackOverflow,因為棧容量和棧幀的大小決定了棧的深度(棧幀大小*深度<=棧容量),所以當OOM時,棧深度一定也已經不夠用了,所以拋出StackOverflow異常也無可厚非。可以通過“-Xss”來配置虛擬機棧固定大小。

  • Java堆:各線程公有。虛擬機工作的主要內存區域(大部分情況下也是最大的),絕大部分對象實例的內存分配都在這里進行。Java 7和之前的Java堆細分為:新生代(伊甸區、存活區0、存活區1)、年老代和永久代。Java 8去除了永久代,替換以Metaspace。在JVM的運行中,大部分情況下,GC主要就發生在堆區域,

  • 方法區:各線程公有。用于存放類定義、常量池、靜態變量(static修飾)、編譯后的字節碼等。方法區實際上是從堆上劃分出來的一塊區域,但是其GC機制是單獨的,與堆不同,所以為了區分方法區和堆,通常又把方法區叫做“非堆”。方法區對應了堆中的永久代。因此在Java8以及之后版本中,永久代被抹除了,方法區也移到了元數據空間(metaspace)中。

  • 運行時常量池:各線程公有。用于存放類信息中的常量(字面量、符號引用等),每個類編譯后的信息中的都有一個常量池,可以通過javap -vebose xxxx.class命令來查看。

  • 直接內存:進程間公有。直接內存不屬于Java虛擬機運行時數據區的一部分,它是指操作系統分配給虛擬機以及其他進程所運行的那塊內存區域,之所以這么說,是因為很多服務器都是虛擬機(操作系統級別),對于物理機來說,這塊內存就是指操作系統所管控的物理內存。通過在堆中創建一個DirectByteBuffer實例來對直接內存進行訪問。

很多讀者了解完這些后還是云里霧里,各論壇還是會出現各種沒有定論的問題,比如

  • 字符串常量池屬于哪個數據區?書中對字符串常量池和運行時常量池描述的相當晦澀和模糊。

  • Java6、Java7和Java8的運行時內存數據區域到底有何不一樣?

  • 什么是字面量,什么又是字符串常量?

  • 什么是本地內存?他和直接內存相同嘛?什么又是堆外內存?

  • 下面我們圍繞這幾個問題做一些討論和引申,從而幫助我們更好的理解運行時數據區域劃分。

    2 字符串常量池

    我們先來回答第一和第二個問題。

    2.1 字符串常量池在哪

    在不同的Java版本中,規范規定的字符串常量池的位置也不一樣。以下三張圖分別代表了Java6、Java7和Java8體系下的Java虛擬機與運行時數據區域劃分,哪些是線程私有,哪些是線程公有,哪些又是進程間公有都比較清晰了。

    2.1.1 Java 6 虛擬機運行數據區

    當我們聽到“字符串常量池也是方法區的一部分”的時候,我們要知道他大概暗指的是Java 6或者之前的版本。如上圖所示,在Java 6虛擬機規范中,字符串常量池確實是方法區的一部分,受永久代內存區大小的限制。當頻繁使用Spring.intern()時,可能會引發OOM(PermGen space)。

    2.1.2 Java 7 虛擬機運行數據區

    從Java 7 開始,規范將字符串常量池遷移到了Java堆中,受Java堆大小的限制。當頻繁大量使用String.intern()時,可能會引發OOM(Java heap space)。

    2.1.3 Java 8 虛擬機運行數據區

    Java 8 虛擬機規范徹底移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的則是元空間(Metaspace)。字符串常量池仍然在Java堆中,但方法區已經遷移到了元空間中。這時候由于濫用 String.intern()引發的OOM依舊在Java堆中。

    2.2 字符串常量池是啥

    那么字符串常量池的數據結構是怎么實現的呢?答案是HashMap,每個字符串常量池對應了一個StringTable的數據結構,其本質并不是Table,而是一個HashMap。這個HashMap的容量是固定的(默認1009),可以通過-XX:StringTableSize來設置,注意這個值是指哈希表中桶的數量,不是占用內存的大小。所以這個值最好是一個質數,并且要大于默認的1009[2]。

    3 字面量和字符串常量

    如以下代碼:

    String str = "123";

    其中”123”就是我們經常看到的“字面量”。字面量是隨著Class信息等在類被加載完畢后一起進入運行時常量池的。而

    String str2 = str.intern();

    這句代碼則嘗試將str的值放入字符串常量池,然而”123”已經在類信息的常量池中了,所以StringTable實際記錄的是類信息常量池中該字符串的引用。

    對于語句:

    String str = new StringBuilder("hello").append(" world").toString().intern();

    這會將新創建的“hello world”的堆內對象引用(str)放入到字符串常量池中,因為這是第一次出現,沒有其他地方存在該值的引用。

    4 本地內存和直接內存

    首先需要說明的是,本地內存(Native Memory)和堆外內存(Off-heap Memory)的含義是一樣的。而關于直接內存和本地內存的關系,StackOverflow上也沒有說清楚的帖子,第二部分中的三張圖已經可以很好的說明直接內存和本地內存的關系了,所謂的本地內存是操作系統分配給JVM虛擬機(作為一個進程)使用的內存塊中除去堆的那一部分。而直接內存則是所有進程共享的操作系統所控制的內存。所以可以這么說:本地內存和直接內存的關系就像“蘋果”和“水果”的關系,蘋果屬于水果,是水果更具體的限定。Java8中的元空間就屬于本地內存空間,而他們都是直接內存的一部分。通過DirectByteBuffer分配的內存區域一定在本地內存中,它也受直接內存大小的限制。本地內存的大小也有限制,比如Window中對每個程序運行所需的內存大小做了2G的默認限制,這只時候其上運行的JVM的本地內存大小≈2G-JVM堆內存大小。

    5 字符串常量池所屬數據區的具體說明

    下面我們舉2個例子討論下在Java6和Java7(含之后版本)下字符串常量池遷移帶來的變化

    5.1 例子1

    請給出以下代碼拋出異常的類型:

    import java.util.ArrayList;
    import java.util.List;

    public class Test {
    public static void main(String[] args){
    Listlist = new ArrayList();int i = 0;while(true) {list.add( String.valueOf(i++).intern());
    }
    }
    }

    然后啟動參數中我們加上:

    -XX:PermSize=10M -XX:MaxPermSize=10M

    分析下這個代碼,其意圖在于不斷的產生新的字符串,并且放入字符串常量池中,試圖撐爆永久代。然而這只會在Java 6 中發生,對于Java7和Java8來說,字符串常量池已經遷移到了Java堆中,如果這時候我們添加以下虛擬機參數:

    -Xms10M -Xmx10M

    則會引發:java.lang.OutOfMemoryError: GC overhead limit exceeded 這樣的錯誤,這個異常的本質與 OOM(Heap space)一直,都是堆內存溢出。

    5.2 例子2

    以下代碼在Java6和Java7中輸出也不相同:


    public class TestStringConstantPool {

    public static String hello = "Hello Java";

    public static void main(String[] args) {

    String str1 = new StringBuilder("Hello ").append("World").toString();
    System.out.println(str1.intern() == str1);

    String str2 = new StringBuilder("Hello ").append("Java").toString();
    System.out.println(str2.intern() == str2);
    }
    }

    在Java6中會輸出:

    false
    false

    在Java7中則輸出:

    true
    false

    首先我們分析下Java6中的場景,Java6中字符串常量池還是運行時常量池的一部分,所以使用String.intern()時,會把堆中的字符串復制到方法區中,返回的是方法區中的對象引用。所以不管如何,堆中對象和方法區中對象應用都不會想等。而在Java7中,這個情況發生了變化,字符串常量池轉移到了堆中,對于str1來說,字符串常量池StringTable會記錄其在堆中的引用(即str1)。所以str1.intern() == str1成立。而str2情況則不一樣了,因為“Hello Java”字符串已經存在于方法區的運行時常量池中,所以intern()返回的是方法區中的對象引用。所以str2.intern() == str2不成立。

    國民程序員

    總結

    以上是生活随笔為你收集整理的运行时错误7内存溢出_JVM运行时内存数据区域的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。