1、jvm內存結構
靜態編譯:把java源文件編譯成字節碼文件class,這個時候class文件以靜態方式存在。
類加載器:把java字節碼文件加載到內存中
方法區:將字節碼放到方法區作為元數據(簡單名字+描述符)。
堆:對象(類的實例)
方法區和堆:運行時數據區在所有線程間共享
虛擬機棧、本地方法棧、程序計數器:運行時數據區線程私有
2、堆
(1)對於大多數應用來說,java堆是java虛擬機所管理的內存中的最大的一塊
(2)java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建,需要考慮線程安全的問題
(3)此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例對在這里分配內存
new Person();
(4)如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,OutOfMemoryError異常
String [] str =new String[1000000];
如果再分配數組的內存之前將jvm的數值調小后便會發生此異常。
(5)是垃圾收集器管理的主要區域
堆內存溢出(OutOfMemoryError)問題:
堆既然有垃圾回收機制,為什么還會有內存溢出的問題呢?
因為垃圾回收器回收的是不被使用的對象,如果不停的產生對象而對象又都在被使用就會出現內存溢出
(6)JDK1.7開始,將StringTable從常量池移動到了堆中,String table又稱為StringPool(字符串常量池)
永久帶的內存回收效率較低,full GC才會觸發,也就是老年代的空間不足才會觸發。但是移動到堆中之后,只需要Minor GC即可觸發垃圾回收,大大減輕了字符串對內存的占用
JVM中的堆一般分為三大部分(jdk1.7):新生代、老年代、永久代(java8以后永久代被元空間代替,元空間使用本地內存,是不與堆內存相連的。因此,默認情況下,元空間的大小僅受本地內存的限制)
(7)引起堆內存溢出的情況
死循環或不停地重復創建大量對象
3、方法區
是各個線程共享的內存區域,虛擬機啟動的時候創建,jdk1.8之前屬於堆的永久區的一部分,會被垃圾回收機制所回收只不過回收的條件較為苛刻,也就造成了永久區較難被回收。1.8開始,將方法區從永久區剝離了出來,取而代之的是元空間,相較於永久區它使用的是物理內存,默認大小是物理內存的大小,但是也可以進行配置
(1)存放的信息
已經被jvm加載的類的信息
常量
靜態變量
及時編譯器編譯后的代碼(JIT)
(2)JIT:熱點代碼編譯后存儲到方法區
for(int i=0;i<100;i++){ add(); }
上面的代碼編譯后存放起來,避免反復編譯
(3)編譯的過程
(4)運行時常量池
常量池:是一個常量表
運行時常量池:
常量池是*.class文件中的,當該類被加載,它的常量池信息就會放入到運行時常量池,並將里面的符號地址變為真實地址
4、程序計數器(PC Register)
(1)一塊較小的內存空間,它的作用是當前線程所執行的字節碼行號指示器(記錄下一條jvm指令的執行地址)
(2)一個處理器只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器(線程私有)
(3)唯一一個在jvm中沒有規定任何OutOfMemoryError的區域(java的規范所規定)
5、本地方法棧
為本地方法(不是由java代碼編寫的方法)的運行提供的內存空間
protected native Object clone() throws CloneNotSupportedException;
該方法沒有方法實現,底層是c或c++實現的,是C或c++程序提供給java程序的接口,也存在兩種異常。
虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。
(1)棧是有深度的:
private Long aLong=1l; public void test(int a,int b){ aLong++; System.out.println(aLong); test(a,b); } public static void main(String[] args) { Test1 test1=new Test1(); test1.test(0,0); }
每調用一次占用一個棧幀:
棧的默認大小為5248,默認1M。
函數的調用過程:
6、虛擬機棧(每一個線程運行的時候所需要的內存)
每一個方法在執行的時候都會創建一個棧幀(一個棧幀對應一個方法的調用,棧幀即每一個方法需要的內存)用於存儲分配基本類型和自定義對象的引用,用於存放,局部變量表、操作數棧、動態鏈接\方法的返回地址
每一個線程只有一個活動棧幀,對應着當前線程正在執行的那個方法
垃圾回收不涉及棧內存
可以通過指令來指定棧的大小,但是並不是棧的內存越大越好,棧的內存變大可能引起線程數量的減少程序反而會變慢
局部變量沒有逃離方法的作用域是線程安全的(線程私有),當作為參數傳遞的變量或是局部變量作為返回值都不是線程安全的,因為可能被別的線程訪問到
(1)一個棧中的多個棧幀
棧幀1先入棧,然后是棧幀2、棧幀3,相當於方法1調用方法2,方法2又調用了方法3,出棧的時候棧幀3先出棧,然后是棧幀2和棧幀3
(2)異常
StackOverflowError:
線程請求的深度大於虛擬機棧的深度(棧內存溢出),無限制的方法的遞歸調用容易出現
棧幀過大,直接將棧內存存滿,發生的情況極其少
例如:定義一個學生類和一個班級類,一個學生只屬於一個班級,在學生類里面有一個班級編號屬性,一個班級有多個學生,班級類里面可以定義一個集合代表多個學生,在進行JSON轉換的時候就會出現循環引用的現象,即一個學生對應一個班級,一個班級又有多個學生......,要把學生和班級的引用改為單向的,就不會出現循環引用的現象了。
OutOfMemoryError:擴展時無法申請到足夠的內存
7、堆、棧、方法區
(1)字符串相關:
JDK1.7開始,將StringTable從常量池移動到了堆中
String string="q"+"w"+"3";
后面的三個字符只創建了一個對象,因為存在字符串的折疊
String string=new String("hello");
當常量池中已經有了“hello”字符串后在常量池中就不必再創建了,只需在棧內存中創建一個對象即可;但是,如果在常量池中沒有“hello”字符串的話,就需要創建兩個字符串對象了。
(2)JVM執行流程
public class Person { private String name; public void sayhello(String name){ System.out.println("hello"+name); } public static void main(String[] args) { Person person=new Person(); person.sayhello("Tom"); } }
JVM去方法區尋找Person類信息如果我不到,Classloader加載Person類信息進入內存方法區
在堆內存中創建Person對象,並持有方法區中Person類的類型信息的引用
把person添加到執行main0方法的主線程java調用棧中,指向堆空間中的內存對象
執行person.sayHello0時,JVM根據person定位到堆空間的Person實例
根據Person實例在方法區持有的引用,定位到方法區Person類型信息,獲得sayHello0字節碼,執行此方法執行,打印出結果。
8、局部變量表
(1)存放了各種基本數據類型、對象引用和returnAddress類型(指向了一條字節碼指令的地址)
(2)long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個
9、常量池與運行時常量池、字符串常量池
(1)常量池存儲字面量和符號引用(是class文件中的常量池,編譯的時候產生)
字面量:字符串、被聲明為final的常量值、基本數據類型等
符號引用:類的完全限定名、字段名稱和描述符、方法名稱和描述符
public static void main(String[] args) { int a=123; String string="abc"; final int num=123; }
程序中的數字並未放入到常量池中,這是因為只有數字超過一定的值以后才會放入到常量值中。常量池在堆中
(2)運行時常量池(類加載到內存中后產生)
將符號引用轉換為實際的地址
將class加載到內存之后經過驗證、鏈接等之后,將符號替換為真正的地址。jdk1.8放在元空間里面,和堆相獨立
(3)字符串常量池(編譯時)
存儲字符串,在堆中
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。常量池中所有相同的字符串常量被合並,只占用一個空間,節省了空間。