本篇文章帶給大家的內容是關於什麼是堆?什麼是方法區? JVM記憶體模型中堆與方法區的介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
# 堆是用來存放物件的記憶體區域。因此,它是垃圾收集器(GC)管理的主要目標。其具有以下特點:
堆在邏輯上劃分為「新生代」和「老年代」。由於JAVA中的物件大部分是朝生夕滅,還有一小部分能夠長期的駐留在記憶體中,為了對這兩種物件進行最有效的回收,將堆劃分為新生代和老年代,並且執行不同的回收策略。不同的垃圾收集器對這2個邏輯區域的回收機制不盡相同,在後續的章節中我們將詳細的講解。
堆佔用的記憶體並沒有要求物理連續,只需要邏輯連續即可。
堆一般實現成可擴展內存大小,使用“-Xms”與“-Xmx”控制堆的最小與最大內存,擴展動作交由虛擬機執行。但由於此行為比較消耗性能,因此一般將堆的最大最小記憶體設為相等。
堆是所有執行緒共享的記憶體區域,因此每個執行緒都可以拿到堆上的同一個物件。
堆的生命週期是隨著虛擬機器的啟動而創建。
當堆疊無法分配物件記憶體且無法再擴充時,會拋出OutOfMemoryError例外。
一般來說,堆無法分配物件時會進行一次GC,如果GC後仍無法分配對象,才會報記憶體耗盡的錯誤。可以透過不斷產生新的物件但不釋放引用來模擬這種情況:
/** * java堆溢出demo * JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * Created by chenjunyi on 2018/4/25. */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); //不断创建新对象,使得Heap溢出 while (true) { list.add(new OOMObject()); } } }
上述程式碼中物件不斷的被創建而不進行引用釋放,導致GC無法回收堆內存,最終OutOfMemoryError ,錯誤訊息:
java.lang.OutOfMemoryError: Java heap space
方法區,也稱為非堆疊(Non- Heap),又是一個被執行緒共享的記憶體區域。其中主要儲存載入的類別字節碼、class/method/field等元資料物件、static-final常數、static變數、jit編譯器編譯後的程式碼等數據,。另外,方法區包含了一個特殊的區域“運行時常數池”,它們的關係如下圖所示:
(1)載入的類字節碼:要使用一個類,首先需要將其字節碼載入到JVM的記憶體中。至於類別的字節碼來源,可以多種多樣,如.class檔案、網路傳輸、或cglib字節碼框架直接產生。
(2)class/method/field等元資料對象:字節碼載入之後,JVM會根據其中的內容,為這個類產生Class/Method/Field等對象,它們用來描述一個類,通常在反射中用的比較多。不同於儲存在堆中的java實例對象,這兩種對象儲存在方法區中。
(3)static-final常數、static變數:對於這兩種類型的類別成員,JVM會在方法區為它們建立一份數據,因此同一個類別的static修飾的類別成員只有一份;
(4)jit編譯器的編譯結果:以hotspot虛擬機器為例,在執行時會使用JIT即時編譯器對熱點程式碼進行最佳化,最佳化方式為將字節碼編譯成機器碼。通常情況下,JVM使用「解釋執行」的方式執行字節碼,即JVM在讀取到一個字節碼指令時,會將其按照預先定好的規則執行堆疊操作,而堆疊操作會進一步映射為底層的機器操作;經過JIT編譯後,執行的機器碼會直接和底層機器打交道。如下圖所示:
在2.1小節中,我們了解到類別的字節碼在載入時會被解析並產生不同的東西存入方法區。類別的字節碼中不僅包含了類別的版本、欄位、方法、介面等描述訊息,還包含了一個常數池。常數池用於存放在字節碼中使用到的所有字面量和符號引用(如字串字面量),在類別載入時,它們進入方法區的運行時常數池存放。
運行時常數池是方法區中一個比較特殊的部分,具備動態性,也就是說,除了類別載入時將常數池寫入其中,java程式運行期間也可以向其中寫入常數:
1 //使用StringBuilder在堆上创建字符串abc,再使用intern将其放入运行时常量池 2 String str = new StringBuilder("abc"); 3 str.intern(); 4 //直接使用字符串字面量xyz,其被放入运行时常量池 5 String str2 = "xyz";
方法區的實現,虛擬機器規範中並未明確規定,目前有2種比較主流的實現方式:
(1)HotSpot虚拟机1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作为实现,即将GC分代收集扩展至方法区。这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有-XX:MaxPermSize的上限)。在JDK1.7+之后,HotSpot逐渐改变方法区的实现方式,如1.7版本移除了方法区中的字符串常量池。
(2)HotSpot虚拟机1.8+:1.8版本中移除了方法区并使用metaspace(元数据空间)作为替代实现。metaspace占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。
我们思考一个问题,为什么使用“永久代”并将GC分代收集扩展至方法区这种实现方式不好,会导致OOM?首先要明白方法区的内存回收目标是什么,方法区存储了类的元数据信息和各种常量,它的内存回收目标理应当是对这些类型的卸载和常量的回收。但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。因此,回收方法区内存不是一件简单高效的事情,往往GC在做无用功。另外随着应用规模的变大,各种框架的引入,尤其是使用了字节码生成技术的框架,会导致方法区内存占用越来越大,最终OOM。
在2.3一节中,我们了解到方法区的2种实现方式最终都会有一个最大值上限,因此若方法区(含运行时常量池)占用内存到达其最大值,且无法再申请到内存时,便会抛出OutOfMemoryError。
在下面的例子中,我们将使用cglib字节码生成框架不断生成新的类,最终使方法区内存占用满,抛出OutOfMemoryError:
/** * java方法区溢出OutOfMemoryError(JVM参数适用于JDK1.6之前,借助CGLIB) * JVM参数:-XX:PermSize=10M -XX:MaxPermSize=10M * Created by chenjunyi on 2018/4/26. */ public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(objects, args)); enhancer.create(); } } static class OOMObject { } }
报错信息为:
1 Caused by: java.lang.OutOfMemoryError: PermGen space 2 at java.lang.ClassLoader.defineClass1(Native Method) 3 ···
其实,在日常开发中,不仅仅使CGlib字节码生成框架会产生大量的class信息,动态语言、JSP、基于OSGI的应用都会在方法区额外产生大量的类信息。
以上是什麼是堆?什麼是方法區? JVM記憶體模型中堆與方法區的介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!