原創文章,轉載請標明出處!
html
相對於C/C++C程序員,Java程序員會相對輕鬆一些,由於Java虛擬機的內存管理機制會管理內存,不須要開發人員手動進行內存管理,也不容易出現內存泄露和內存溢出的。但若是不瞭解虛擬機如何管理內存,在內存出現問題時就會一籌莫展,因此學習虛擬機如何管理內存也是一件必要的事情。java
The Java Virtual Machine defines various run-time data areas that are used during execution of a program.
android
Java虛擬機定義了在程序運行期間的各類運行時數據區域。c++
在《Java虛擬機規範》中運行時數據區域會包括PC寄存器、Java虛擬機棧、堆、方法區、運行常量池、本地方法棧。由於運行時常量池是方法區的一部分,因此本篇文章將常量池放在方法區章節中的子節來說解。程序員
Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
算法
一部分數據區域與虛擬機進程同生共死,另外一部分數據區域與線程同生共死。
編程
The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own
pc(program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method ([§2.6](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)) for that thread. If that method is not
native, the
pcregister contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is
native, the value of the Java Virtual Machine's
pcregister is undefined. The Java Virtual Machine's
pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
json
Java虛擬機支持同時執行多個線程。每個Java虛擬機線程都有本身的PC寄存器。在任什麼時候刻,Java虛擬機一個線程都只在執行某一個單一函數代碼。若是函數不是native函數,PC寄存器中就包含當前正在被執行的Java虛擬機指令的地址。反之當前函數是native函數,pc寄存器中的值是undefined。pc寄存器的大小足夠存儲返回地址或native指針。數組
(1)PC寄存器並不是真正意義上的物理寄存器,pc寄存器是對物理寄存器的一種模擬;緩存
(2)PC寄存器是一塊較小的內存空間;
(3)能夠將其看作當前線程執行的字節碼指令的「行號指示器」;
(4)字節碼解釋器的工做就是改變pc寄存器的值來選取下一條須要執行的字節碼指令;
(5)PC寄存器的做用是存儲下一條指令地址,也就是即將要執行的指令代碼,而後由執行引擎讀取下一條指令;
(6)在Java虛擬機規範中,PC寄存器是線程私有的,其生命週期與線程生命週期保持一致;
(7)PC寄存器是Java虛擬機規範中沒有規定任何OutOtMemoryError的區域。
當單核處理器執行多線程代碼時,會爲每一個線程分配CPU時間片,CPU經過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。可是在切換前會保存上一個任務的狀態,以便於下次切換回任務時,能夠再次加載這個任務以前的狀態。因此任務從保存到再一次加載的過程就是一次「上下文切換」。CPU經過不停進行上下文切換,讓咱們以爲多個線程是同時執行的。
CPU時間片就是CPU分配給每一個線程的時間段。因爲CPU只有一個核數有限,只能同時處理程序的一部分任務,不能同時知足全部要求,爲了公平處理多線程問題,就引入的時間片的概念,爲每一個線程分配時間片,輪流執行。
因爲Java虛擬機多線程是經過線程上下文切換的方式來實現的。在任什麼時候刻,一個處理器只會執行一條程序中的指令,所以在上下文切換後爲了可以恢復到正確的執行位置,每一個線程都須要有一個獨立的PC寄存器,線程之間獨立存儲,互不影響。
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames ([§2.6](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
Java虛擬機棧是線程私有的,與線程同生共死。Java虛擬機中有一個個存儲棧幀。Java的棧可以存儲局部變量與部分返回結果,並參與函數的調用與返回。由於Java虛擬機棧只push或pop棧幀,不直接操做Java虛擬機棧,棧幀可能被堆分配。Java虛擬機棧的內存不須要連續。
Java虛擬機棧描述的是Java函數執行的線程內存模型。每一個函數被執行時,Java虛擬機會同步建立一個棧幀用於存儲局部變量標配、操做數棧、動態連接和函數返回地址等信息。函數被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機中入棧和出棧的過程。
(1)棧是線程私有,棧中的數據都已棧幀形式存在。棧幀是棧的基本單位;
(2)線程中正在執行的函數都有其對應的棧幀;
(3)棧幀是一個內存區塊,是一個數據集合,其中存儲着函數執行過程當中的數據信息;
(1)JVM直接對Java棧的操做只有兩個,就是對棧幀的壓棧和出棧。
(2)在同一線程同一時間下,只會有一個活動的棧幀,且該棧幀是當前正在執行的函數對應的棧幀,即棧頂棧幀也稱爲「當前棧幀」。
(3)執行引擎運行的全部字節碼指令只針對當前棧幀進行操做。
(4)若方法中調用了其餘方法,對應的新的棧幀就會被建立,放在棧頂,成爲新的當前棧幀。
(1)概述
1)局部變量表定義爲一個數組,主要用於存儲方法參數和定義在方法體內的局部變量。包括基本數據類型和對象引用以及returnAddress類型(指向特定指令地址的指針,例如pc寄存器中的值就是returnAddress類型)。
2)局部變量表所需容量在編譯期間已經肯定下下來,並保存在方法的Code屬性的maximun local variables數據項中。在函數運行期局部變量表大小不會改變。
(2)舉例代碼
public class Car { public static void main(String[] args) { Car car = new Car(); String name = "Boyce Car"; } }
(3)字節碼文件中的局部變量表
1)start pc表示字節碼指令的行號;
2) length是pc指針起始位置到結束位置的距離;
3) start pc與length共同描述變量的做用域範圍。
(4)Slot
1)局部變量表的最基本存儲單元Slot(變量槽)
2)在局部變量表中,32位之內的類型只佔一個Slot(包括returnAddress類型),64位類型(long和duble) 佔兩個Slot。
3)局部變量表中,每個Slot都會分配到一個訪問索引,經過這個索引能夠訪問到局部變量表中對應的局部變 量值。
4)當一個實例方法被調用時,它的方法參數和方法體內定義的局部變量將會按照順序被複制到局部變量表中 的Slot上。
5)當須要訪問局部變量表中的64位的局部變量值時,只須要使用2個Slot索引中的前一個索引便可。
6)若是當前幀由構造函數或實例方法建立的,那麼該對象引用this將存放在index爲0的Slot處。
(1)每一個棧幀都包含一個先進後出的操做數棧。
(2)操做數棧在方法執行過程當中,根據字節碼指令,往棧中寫入或讀取數據,即入棧或出棧。
(3)操做數棧的做用主要是用於保存計算過程的中間結果,同時做爲計算過程當中變量的臨時存儲空間。
(4)操做數棧根據push/pop進行操做,沒法使用索引方式訪問。
(5)若是一個函數帶有返回值,其返回值會被壓入當前棧幀的操做數棧中,並更新pc寄存器中的下一條字節碼指令。
(6)Java虛擬機的指令架構是基於棧式架構,其中的棧指的就是操做數棧。
(7)基於棧式結構計算過程的字節碼指令:
(1)操做數棧存儲於內存,頻繁操做進行IO操做影響執行效率。HotSpot虛擬機的設計者提出了棧頂緩存技術,將棧頂元素緩存在寄存器中,以此減小IO操做,提高運行效率。(處理器訪問任何寄存器和 Cache 等封裝之外的數據資源均可以當成 I/O 操做,包括內存,磁盤,顯卡等外部設備。)
(1)每一個棧幀內部都包含一個紙箱運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前代碼可以實現動態連接(Dynamic Linking)。例如:invokedynamic指令。
(2)Java源文件被編譯成字節碼文件時,字面量與符號引用都被保存至字節碼文件的常量池中。例如:當一個函數調用另外一個函數時,就經過常量池中指向的函數的符號引用來表示,動態連接的做用就是將這些符號引用轉換爲調用函數的直接引用(函數在實際運行時內存中的入口地址)。
(1)存放調用該函數的主函數的pc寄存器的值;
(2)一個函數的結束有兩種方式:1)正常執行完成;2)異常,非正常退出;
(3)不管經過哪一種方式退出,在函數退出後都返回到該函數被調用的位置,程序才能繼續執行。正常退出時,調用方的pc寄存器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而若是經過異常退出,返回地址是要經過異常表來肯定,棧幀中通常不會保存這部分信息。
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.
Java虛擬機的實現可使用傳統的堆棧,以支持本地方法,本地方法棧也能夠用於實現Java虛擬機指令集的解釋器。Java虛擬機不能加載本地方法且不提供本地方法棧。若是提供本地方法棧,則線程建立時爲每一個線程分配一個本地方法棧。
(1)本地方法棧與Java虛擬機棧類似,Java虛擬機棧用於管理Java函數的執行問題,而本地方法棧則是用於管理本地函數(Native)的執行問題。
(2)《Java虛擬機規範》中對本地方法棧沒有強制規定,所以虛擬機能夠根據需求自由實現。如Hot-Spot虛擬機就直接將本地方法棧和虛擬機棧合二爲一。
(3)棧深度溢出或棧擴展失敗時會分配拋出StackOverflowError和OutOfMemoryError異常。
(4)當線程調用本地方式時,它和虛擬機就有相同的權限,再也不受虛擬機的限制。
在安卓開發時,咱們須要調用C/C++代碼,咱們就須要用到JNI(Java Native Interface)。
package com.example.nativetest1; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = findViewById(R.id.sample_text); tv.setText(stringFromJNI()); } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ public native String stringFromJNI(); }
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativetest1_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.
(1)堆空間是線程共享的,類的對象實例與數組分配的內存都在堆空間中。
(2)堆空間在虛擬機啓動時建立,堆空間的內存由垃圾回收器進行回收,對象不顯示釋放。Java虛擬機不限定垃圾回收器,垃圾回收器技術能夠根據實現者的需求自行選擇。堆空間大小能夠是固定,也能夠是根據需求進行擴展的。堆的內存空間能夠物理上不連續。
(1)Java虛擬機實例中只有一個堆內存,堆是虛擬機管理的最大一塊內存,其被全部線程共享。
(2)Java堆存在爲惟一目的就是存儲對象實例。
(3)《Java虛擬機規範》中規定:「全部對象實例和數組都應該在堆上分配」。隨着Java語言的發展將來可能被打破,可是目前仍然沒有。
(4)《Java虛擬機規範》中規定:「堆能夠處於物理上不連續的內存空間中,但邏輯上它應該被視爲連續的」。
(5)《Java虛擬機規範》中並無對堆的劃分有任何要求,「新生代、老年代、Eden、Survivor」等名詞只是一部分垃圾回收器的設計,而非某一虛擬機固有的內存佈局。
(6)全部線程都共享堆空間,但仍是能夠劃分線程私有的緩衝區(Thread Local Allocation Buffer, TLAB)。
(7)數組和對象永遠不會存儲在棧上,由於棧幀中只保存引用,引用指向對象和數組在堆中存放的位置。
棧負責解決執行問題,堆負責解決數據存儲問題。
爲何會出現TLAB?
(1)堆空間是線程共享的。
(2)JVM中會頻繁建立對象實例,所以在併發環境下操做堆空間的內存區域是線程不安全的。
(3)當多線程同時操做同一地址時,就須要加上同步機制,這就會影響對象實例建立速度。
(4)如何解決解決這一問題呢?這就出現了TLAB。
什麼是TLAB?
(1) JVM爲每一個線程分配了一個私有的緩存區域。
(2)多線程同時分配內存時,使用TLAB能夠避免一系列的非線程安全問題,同時還可以提高內存分配的吞吐量,所以咱們能夠將這種分配方式稱爲「快速分配策略」。
(3)基於OpenJDK衍生出來的JVM都提供了TLAB的設計。
(4)TLAB默認棧堆空間(Eden區)的1%。
對象分配(開啓TLAB時)
(1)JVM將TLAB做爲內存分配的首選。
(2)默認詳情下,TLAB僅佔堆空間(Eden區)的1%。
(3)當對象在TLAB空間分配失敗時,JVM就會嘗試經過使用加鎖機制確保數據操做的原子性,從而直接在堆空間(Eden區)分配內存。
(1)逃逸分析的本質是分析對象動態做用域。
(2)當一個對象在方法中被定義後,對象只在方法內部使用,則認爲沒有發生逃逸。
(3)當一個對象在方法中被定義後,它被外部方法所引用時,則認爲發生逃逸。
(4)發生逃逸
public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer stringBuffer = new StringBuffer(); return stringBuffer; }
(5)未發生逃逸
public static String createStringBuffer(String s1, String s2) { StringBuffer stringBuffer = new StringBuffer(); return stringBuffer.toString(); }
判斷方式是new的對象實例是否能在方法外被調用。
使用逃逸分析後,編譯器能夠對代碼作以下優化
棧上分配
(1)將堆上分配轉化爲棧上分配。對象在程序中被分配,若是要使對象不會發生逃逸,對象能夠在棧上分配,而不是堆上分配。
(2)JIT編譯器在編譯期間會藉助逃逸分析來判斷對象是否逃逸出方法,若是沒有逃逸,就可能會被優化爲棧上分配,最後線程結束後棧空間被回收,局部變量對象也會被回收。
同步省略
(1)若是對象只能被一個線程訪問到,那麼這個對象能夠不考慮同步。
(2)場景:在動態編譯同步塊代碼時,JIT編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否只能被一個線程訪問。若是肯定只有一個線程能訪問到,JIT編譯器在編譯這個代碼塊時就會取消對這部分代碼的同步。這個過程叫「同步省略」也叫「鎖消除」。
標量替換
(1)有的對象可能不須要一個連續的內存結構存在也能夠被訪問到,那麼對象的部分(或所有)能夠不存儲在內存,而是存儲在CPU寄存器中。
(2)變量(Scalar)是指一個沒法再分解成爲更小的數據的數據。例如:Java中原始數據類型就是標量。
(3)聚合量(Aggregate)是指一個可以被分解成爲更小數據的數據。例如:Java對象。
(4)應用場景是在JIT階段,若是通過逃逸分析,發現一個對象不會被外界訪問,通過JIT優化,就會把這個對象拆解爲若干個成員變量來替代(變量)。這個過程就叫標量替換。
小結
(1)目前逃逸分析技術並不成熟。
(2)逃逸分析自己也耗費性能,沒法保證逃逸分析消耗的性能小於函數自己。
(3)目前只有變量替換被應用。
(4)目前堆是存儲對象實例的惟一選擇。
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods ([§2.9](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9)) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
(1)方法區是線程共享的,相似傳統語言用於存儲編譯代碼的存儲區。
(2)方法區用於存儲類結構信息,例如運行時常量池、域信息、函數數據、函數與構造函數代碼、類元信息和實例初始化、接口初始化中使用的特殊函數。
(3)Java虛擬機啓動時就會建立方法區,邏輯上方法區是堆空間的一部分(實際上不是)。方法區能夠不實現垃圾收集策略(Hotspot有實現)。方法區大小能夠固定,也能夠是根據計算需求擴展的。方法區內存物理上能夠不連續。
(1)在《Java虛擬機規範中》中方法區邏輯上是堆空間的一部分,但實際Hot-Spot虛擬機實現時,卻將堆空間與方法區作了區分,方法區還有一個別名叫作Non-Heap(非堆)。因此能夠將方法區看作獨立於堆的內存空間。
(2)方法區是線程共享的。
(3)方法區的大小和對空間同樣能夠經過參數設置,是能夠擴展的。
(4)方法區的大小決定可以保存多少類,若是類太多就會形成方法區內存溢出。
(5)關閉JVM方法區內存就會釋放。
(1)存儲已經被虛擬機加載的類型信息、常量、靜態變量、域信息、方法信息、即時編譯器編譯後的代碼緩存等。(JDK8以前)
(2)類型信息。對每一個加載的類型(類class、接口interface、枚舉enum、註解annotation)。JVM必須在方法區中存儲如下類型信息:
a)該類型的完整名稱(包名.類型)。
b)該類型直接父類的完整名稱(接口或Object類都沒有父類)。
c)該類型的修飾符(public、abstract、final)。
d)該類型直接接口的有序列表。
(3)域信息。
a)JVM必須在方法區中保存類型的全部域的相關信息和域的聲明順序。
b)域相關信息有域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient)
(4)方法信息。JVM必須保存全部方法的如下信息,同域信息同樣包括聲明順序:
a)方法名稱
b)方法的返回類型(或void)
c)方法參數的數量和類型(按順序)
d)方法的修飾符(public、private、protected、static、final、syschronized、native、abstract)
e)方法的字節碼(bytecodes)、操做數棧、局部變量表以及大小
f)異常表
(5)源代碼
/** * @author jianw.li * @date 2020/12/2 10:51 下午 * @Description: 方法區測試 */ public class MethodAreaTestDemo extends MethodAreaTest implements Serializable { public int num = 1; private static String str = "測試測試"; public void sub() { int i = 10; int j = 1; int k = i - j; System.out.println(k); } private static int add(int a, int b){ int c = a + b; return c; } public static void main(String[] args) { add(1, 2); } }
(6)字節碼文件
經過javap -v MethodAreaTestDemo.class查看
Classfile /Users/lijianwei/IdeaProjects/LeeBoyceJVMTest/out/production/LeeBoyceJVMTest/com/ljw/MethodAreaTestDemo.class Last modified 2020-12-2; size 985 bytes MD5 checksum f3555565267ef4cbb0c07bebb42263a6 Compiled from "MethodAreaTestDemo.java" //註釋:存放至方法區的類信息 public class com.ljw.MethodAreaTestDemo extends com.ljw.MethodAreaTest implements java.io.Serializable minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #9.#38 // com/ljw/MethodAreaTest."<init>":()V #2 = Fieldref #8.#39 // com/ljw/MethodAreaTestDemo.num:I #3 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream; #4 = Methodref #42.#43 // java/io/PrintStream.println:(I)V #5 = Methodref #8.#44 // com/ljw/MethodAreaTestDemo.add:(II)I #6 = String #45 // 測試測試 #7 = Fieldref #8.#46 // com/ljw/MethodAreaTestDemo.str:Ljava/lang/String; #8 = Class #47 // com/ljw/MethodAreaTestDemo #9 = Class #48 // com/ljw/MethodAreaTest #10 = Class #49 // java/io/Serializable #11 = Utf8 num #12 = Utf8 I #13 = Utf8 str #14 = Utf8 Ljava/lang/String; #15 = Utf8 <init> #16 = Utf8 ()V #17 = Utf8 Code #18 = Utf8 LineNumberTable #19 = Utf8 LocalVariableTable #20 = Utf8 this #21 = Utf8 Lcom/ljw/MethodAreaTestDemo; #22 = Utf8 sub #23 = Utf8 i #24 = Utf8 j #25 = Utf8 k #26 = Utf8 add #27 = Utf8 (II)I #28 = Utf8 a #29 = Utf8 b #30 = Utf8 c #31 = Utf8 main #32 = Utf8 ([Ljava/lang/String;)V #33 = Utf8 args #34 = Utf8 [Ljava/lang/String; #35 = Utf8 <clinit> #36 = Utf8 SourceFile #37 = Utf8 MethodAreaTestDemo.java #38 = NameAndType #15:#16 // "<init>":()V #39 = NameAndType #11:#12 // num:I #40 = Class #50 // java/lang/System #41 = NameAndType #51:#52 // out:Ljava/io/PrintStream; #42 = Class #53 // java/io/PrintStream #43 = NameAndType #54:#55 // println:(I)V #44 = NameAndType #26:#27 // add:(II)I #45 = Utf8 測試測試 #46 = NameAndType #13:#14 // str:Ljava/lang/String; #47 = Utf8 com/ljw/MethodAreaTestDemo #48 = Utf8 com/ljw/MethodAreaTest #49 = Utf8 java/io/Serializable #50 = Utf8 java/lang/System #51 = Utf8 out #52 = Utf8 Ljava/io/PrintStream; #53 = Utf8 java/io/PrintStream #54 = Utf8 println #55 = Utf8 (I)V { //註釋:存放至方法區的域信息 public int num; descriptor: I flags: ACC_PUBLIC public com.ljw.MethodAreaTestDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method com/ljw/MethodAreaTest."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field num:I 9: return LineNumberTable: line 10: 0 line 12: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/ljw/MethodAreaTestDemo; //註釋:存放至方法區的函數信息 public void sub(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: iconst_1 4: istore_2 5: iload_1 6: iload_2 7: isub 8: istore_3 9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 12: iload_3 13: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 16: return LineNumberTable: line 16: 0 line 17: 3 line 18: 5 line 19: 9 line 20: 16 LocalVariableTable: Start Length Slot Name Signature 0 17 0 this Lcom/ljw/MethodAreaTestDemo; 3 14 1 i I 5 12 2 j I 9 8 3 k I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iconst_1 1: iconst_2 2: invokestatic #5 // Method add:(II)I 5: pop 6: return LineNumberTable: line 28: 0 line 29: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 args [Ljava/lang/String; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #6 // String 測試測試 2: putstatic #7 // Field str:Ljava/lang/String; 5: return LineNumberTable: line 13: 0 } SourceFile: "MethodAreaTestDemo.java"
方法區、永久代以及元空間的關係
(1)方法區不等於永久代不等於元空間。
(2)永久代、元空間只是方法區的實現方式。
(3)永久代的使用,容易致使Java程序OOM(超過-XX:MaxPermSize上限)。
(4)JDK8將將方法區的實現方式由永久代改成元空間。
(5)元空間的本質與永久代相似,都是對Java虛擬機規範中方法區的實現。不過元空間與永久代最大的區別在於元空間不在虛擬機設置的內存中,而是使用本地內存。
方法區變化細節
版本 | 說明 |
---|---|
JDK6 | 永久代實現方法區。靜態變量、字符串常量池存放在永久代。 |
JDK7 | 永久代實現方法區。已經逐漸去除「永久代」,將字符串常量池、靜態變量移至堆中存儲。 |
JDK8 | 元空間實現方法區。類信息、域信息、函數信息、運行時常量池存儲至本地內存的元空間中。但字符串常量池和靜態變量仍然存放在堆中。 |
(1)運行時常量池是方法區的一部分。
(2)常量池是Class文件的一部分。常量池用於存放編譯期間的字面量和符號引用(字面量和符號引用後續文章講),這部份內容將在類加載後存放到方法區運行時常量池中。
(3)JVM爲每一個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組同樣是經過索引訪問的。
(4)運行時常量池中包含多種不一樣的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後才能獲取到的方法或字段引用。
(5)什麼是字面量?a)文本字符串;b)八種基本類型的值;c)被聲明爲final的常量等。
(6)什麼是符號引用?a)類和方法的全限定名;b)字段名稱和描述符;c)方法名稱和描述符。
(7)爲何須要運行時常量池?Java的字節碼須要使用數據支持,這些數據不可以直接存儲在字節碼文件中。爲了字節碼文件中可使用到數據,能夠將數據存放在常量池中,再由字節碼文件中存放的「指向常量池的引用」指向常量池中的數據。
(1)官方解釋
因爲JRockit虛擬機與Hotspot虛擬機融合,JRockit虛擬機虛擬機的用戶不須要也不習慣去設置永久代。因此融合以後索性就去掉了永久代。
一部分類元數據存放在本地內存中,而字符串常量池與靜態變量則存放置堆空間中。類元數據僅受限於可以使用的本地內存大小,而不是-XX:MaxPermSize
。
(2)永久代的空間大小難以設置
若是動態加載的類過多,容易形成內存溢出(OOM),元空間相對於永久代的好處是使用本地內存而非虛擬機內存,默認狀況下元空間大小僅受本地內存限制。
(3)永久代調優困難
對方法區的垃圾回收困難。對於類信息的回收須要同時知足3個條件:1)該類的全部勢力都已經被回收,堆中不在存在任何該類和其派生子類的實例;2)該類的類加載器已經被回收;3)該類的對象再也不被引用,且沒法經過反射訪問該類的函數。須要同時知足以上條件類纔可以「容許」被回收。
JDK7中將StringTable放在堆空間中。由於永久代的回收效率很低,只有在full gc時纔會觸發。而full gc只有在老年代、永久代空間不足時纔會觸發。實際開發過程當中,會建立大量字符串,放在堆空間相對於方法區回收效率更高。
(1)《Java虛擬機規範》中提到能夠不要求虛擬機在方法區中實現垃圾收集。
(2)方法區垃圾收集主要2部份內容:1)常量池中廢棄的常量;2)不在使用的類。
(3)方法區中的常量池中主要存放兩大類常量:1)字面量;2)符號引用。
(4)常量池中的常量沒有任何地方引用就能夠被回收。
(5)類的回收條件很是苛刻,必須同時知足3個條件。(能夠看上文方法區調優困難中對方法區垃圾回收困難的描述)。
(1)直接內存並非虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域。
(2)直接內存是在Java堆外內存,是直接想系統申請的堆外內存空間。
(3)本機直接內存不受Java堆大小限制。
(4)訪問直接內存的速度要高於Java堆的速度。讀寫性能要求高時能夠考慮直接內存。
(5)Java的NIO庫容許Java程序使用直接內存,用於數據緩衝區。
(6)直接內存的缺點是回收成本高,不受JVM回收機制管理。
(7)直接內存能夠經過參數設置大小,默認與堆的最大參數值一致。
(1)對象建立方式
1)new
2)反射
3)clone()
4)反序列化
(2)建立對象步驟
1)判斷對象對應類是否加載、鏈接、初始化。
當虛擬機遇到new指令時,首先會去檢查這個指令的參數可以在Metaspace的常量池中定位到類的符號引用,並檢查這個符號引用表明的類是否已經被加載、解析和初始化(類元信息是否存在)。
2)對象內存分配
a.計算對象佔用空間大小,而後在堆中劃分一塊內存給新對象。
b.指針碰撞。若是能內存規整,只須要使用一個指針做爲分界點的指示器,分配的內存就是將指針移動一段與對象大小相等的距離。若是垃圾收集器基於壓縮算法,具有整理過程的收集器,虛擬機就會使用這種方式分配內存
c.空閒列表。若是內存不規整,虛擬機須要維護一個列表, 記錄哪些內存塊可使用,哪些內存塊已經被使用,在分配時在列表中找到一塊足夠大小的空間分配給對象實例,並更新列表上的內容。
d.採用哪一種方式分配內存,取決於垃圾收集器是否具有整理(compact)功能。
3)併發問題
a.採用cas配上失敗重試保證操做的原子性。
b.每一個線程預先分配TLAB。
4)初始化
爲全部屬性設置默認值。例如int類型變量設置默認值爲0,String變量設置默認值爲null等。
5)設置對象頭
a.運行時元數據(MarkWord)。哈希值、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時 間戳
b.類型指針。指向類元信息,肯定該對象所屬的類型。
6)執行init方法進行初始化
a.顯示初始化或代碼塊中初始化
b.構造器中初始化
c.初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量。
7)小結
整理流程:加載類元信息->對象內存分配->處理併發問題->屬性默認值初始化->(零值初始化)->設置對象頭信息->屬性顯性初始化、代碼塊初始化、構造器初始化->實例化完成。
(1)示例代碼
public class Car { private int price = 300000; private String brand = "BMW"; private Plant plant; public Car() { this.plant = new Plant(); } public static void main(String[] args) { Car car = new Car(); } }
public class Plant { }
(2)內存佈局圖
(1)句柄訪問
(2)直接指針訪問
[1]《The Java Virtual Machine Specification》Java SE 8 Edition
[2]《深刻理解Java虛擬機》第二版、第三版
[3]《宋紅康JVM教程》
[4]《Java併發編程藝術》
[5] 《JEP 122: Remove the Permanent Generation》
懂得很少,作得不多。若是文章有不足之處,歡迎留言討論。
原創文章,轉載請標明出處!