運行時數據區域java
Java虛擬機在執行Java程序的過程當中會把它關聯的內存劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時的數據區域。如圖所示:多線程
1.1程序計數器jvm
程序計數器是一塊較小的內存空間,它能夠當作是當前線程所執行的字節碼的行號指示器。程序計數器記錄線程當前要執行的下一條字節碼指令的地址。因爲Java是多線程的,因此爲了多線程之間的切換與恢復,每個線程都須要單獨的程序計數器,各線程之間互不影響。這類內存區域被稱爲「線程私有」的內存區域。 因爲程序計數器只存儲一個字節碼指令地址,故此內存區域沒有規定任何OutOfMemoryError狀況。ide
1.2虛擬機棧函數
Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行時都會建立一個棧幀(Stack Frame)用於存儲信息以下:
局部變量表
返回值
操做數棧
當前方法所在的類的運行時常量池引用
每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。咱們平時所說的「局部變量存儲在棧中」就是指方法中的局部變量存儲在表明該方法的棧幀的局部變量表中。而方法的執行正是從局部變量表中獲取數據,放至操做數棧上,而後在操做數棧上進行運算,再將運算結果放入局部變量表中,最後將操做數棧頂的數據返回給方法的調用者的過程。性能
虛擬機棧可能出現兩種異常:
由線程請求的棧深度過大超出虛擬機所容許的深度而引發的StackOverflowError異常;
以及由虛擬機棧沒法提供足夠的內存而引發的OutOfMemoryError異常。測試
局部變量存放數據以下:
boolean
byte
char
long
short
int
float
double
reference(對象引用)
returnAddress(指向了一條字節碼指令的地址)
在局部變量表裏,除了long和double,全部類型都是佔了一個槽位,它們佔了2個連續槽位,由於他們是64位寬度。spa
1.3本地方法棧.net
本地方法棧與虛擬機棧相似,他們的區別在於:本地方法棧用於執行本地方法(Native方法);虛擬機棧用於執行普通的Java方法。在HotSpot虛擬機中,就將本地方法棧與虛擬機棧作在了一塊兒。 本地方法棧可能拋出的異常同虛擬機棧同樣。線程
1.4Java堆
Java堆是被全部線程共享的一塊內存區域,此內存區域的惟一目的就是存放對象實例。
爲了支持垃圾收集,堆被分爲三個部分:
年輕代
經常又被劃分爲Eden區和Survivor(From Survivor To Survivor)區
老年代
永久代 (jdk 8已移除永久代,後續會詳細講解)
1.5方法區
方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的代碼等數據。這也是開發者常說的永久代。具體存放信息以下:
類加載器引用
運行時常量池
全部常量
字段引用
方法引用
屬性
字段數據
每一個方法
名字
類型
修飾符
屬性
方法數據
每一個方法
名字
返回類型
參數類型(按順序)
修飾符
屬性
方法代碼
每一個方法
字節碼
操做數棧大小
局部變量大小
局部變量表
異常表
每一個異常處理
開始位置
結束位置
代碼處理在程序計數器中的偏移地址
被捕獲的異常類的常量池索引
1.6直接內存
JDK1.4中引用了NIO,並引用了Channel與Buffer,可使用Native函數庫直接分配堆外內存,並經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。
Java8以及以後的版本中方法區已經從原來的JVM運行時數據區中被開闢到了一個稱做元空間的直接內存區域。
JDK 6,7,8 方法區的區別
在Java7以前,HotSpot虛擬機中將GC分代收集擴展到了方法區,使用永久代來實現了方法區。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,可是在以後的HotSpot虛擬機實現中,逐漸開始將方法區從永久代移除。Java7中已經將運行時常量池從永久代移除,在Java堆(Heap)中開闢了一塊區域存放運行時常量池。而在Java8中,已經完全沒有了永久代,將方法區直接放在一個與堆不相連的本地內存區域,這個區域叫元空間。
實戰OutOfMemoryError異常
2.1Java堆溢出
Java堆用來存儲對象實例,只要不斷的建立對象,而且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象到達最大堆的容量限制後就會產生內存溢出異常。
java.lang.OutOfMemoryError:Java heap space 示例:
設置 -Xms20m -Xmx20m
package com.tlk.jvm;
import java.util.ArrayList;
import java.util.List;
/**
* 一直建立對象,堆內存溢出
* VM Args: -Xms20m -Xmx20m
* Created by tanlk on 2017/8/30 21:16.
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args){
List<Object> list = new ArrayList<Object>();
while (true){
list.add(new OOMObject());
}
}
}
2.2虛擬機棧和本地方法棧溢出
HotSpot 虛擬機中並不區分虛擬機棧和本地方法棧。棧容量只由-Xss參數設定。
若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常。
若是虛擬機在擴展棧時,沒法申請到足夠的內存空間,則將拋出OutOfMemoryError異常。
(本地測試了一個OOM,但沒有出現)
package com.tlk.jvm;
/**
* 棧溢出
* VM Args:-Xss128k
*/
public class StackErrorMock {
private static int index = 1;
public void call(){
index++;
call();
}
public static void main(String[] args) {
StackErrorMock mock = new StackErrorMock();
try {
mock.call();
}catch (Throwable e){
System.out.println("Stack deep : "+index);
e.printStackTrace();
}
}
}
當棧調用深度大於JVM所容許的範圍,會拋出StackOverflowError的錯誤,不過這個深度範圍不是一個恆定的值
2.3方法區和運行時常量池溢出
package com.tlk.jvm;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* jdk 1.6
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* Created by tanlk on 2017/9/1 15:48.
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
其實,移除永久代的工做從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒徹底移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。咱們能夠經過一段程序來比較 JDK 1.6 與 JDK 1.7及 JDK 1.8 的區別,以字符串常量爲例:
這段程序以2的指數級不斷的生成新的字符串,這樣能夠比較快速的消耗內存。咱們經過 JDK 1.六、JDK 1.7 和 JDK 1.8 分別運行:
package com.tlk.jvm;
import java.util.ArrayList;
import java.util.List;
/**
*
* VM Args: -Xmx200M -XX:PermSize=10M -XX:MaxPermSize=10M
* Created by tanlk on 2017/9/1 21:48.
*/
public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
JDK1.6運行結果:
JDK1.7運行結果:
JDK1.8運行結果:
從上述結果能夠看出,JDK 1.6下,會出現「PermGen Space」的內存溢出,而在 JDK 1.7和 JDK 1.8 中,會出現堆內存溢出,而且 JDK 1.8中 PermSize 和 MaxPermGen 已經無效。
另外JDK8 設置了-XX:MaxMetaspaceSize=10m,結果也是報Java heap space
而且筆者用jconsole監控發現堆內存使用很是大。
所以,能夠大體驗證 JDK 1.7 和 1.8 將字符串常量由永久代轉移到堆中,而且 JDK 1.8 中已經不存在永久代的結論。
上訴測試代碼簡單解釋:
運行時常量池在JDK1.6及以前版本的JVM中是方法區的一部分,而在HotSpot虛擬機中方法區放在了」永久代(Permanent Generation)」。因此運行時常量池也是在永久代的。
可是JDK1.7及以後版本的JVM已經將運行時常量池從方法區中移了出來,在Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
String.intern()是一個Native方法,它的做用是:若是運行時常量池中已經包含一個等於此String對象內容的字符串,則返回常量池中該字符串的引用;若是沒有,則在常量池中建立與此String內容相同的字符串,並返回常量池中建立的字符串的引用。
JDK1.7改變
當常量池中沒有該字符串時,JDK7的intern()方法的實現再也不是在常量池中建立與此String內容相同的字符串,而改成在常量池中記錄java Heap中首次出現的該字符串的引用,並返回該引用。
Metaspace(元空間)
3.1元空間是什麼?
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集
3.2元空間內存溢出的例子
package com.tlk.jvm;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* -XX:MaxMetaspaceSize=10m
* jdk1.8沒有永久代了,取而代之的是Metaspace
* Created by tanlk on 2017/9/4 20:48.
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
java.lang.OutOfMemoryError: Metaspace
3.3元空間總結:
經過上面分析,你們應該大體瞭解了 JVM 的內存劃分,也清楚了 JDK 8 中永久代向元空間的轉換。不過你們應該都有一個疑問,就是爲何要作這個轉換?因此,最後給你們總結如下幾點緣由:
一、字符串存在永久代中,容易出現性能問題和內存溢出。
二、類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。
三、永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
四、Oracle 可能會將HotSpot 與 JRockit 合二爲一。
相關文章:
Java永久代去哪了:http://www.infoq.com/cn/articles/Java-PERMGEN-Removed?utm_campaign=infoq_content&
JVM內部原理:http://ifeve.com/jvm-internals/
瞭解String intern():http://blog.csdn.net/seu_calvin/article/details/52291082