十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
一、概述
Java虛擬機規(guī)范規(guī)定的java虛擬機內(nèi)存其實就是java虛擬機運行時數(shù)據(jù)區(qū),其架構(gòu)如下:
其中方法區(qū)和堆是由所有線程共享的數(shù)據(jù)區(qū)。
虛擬機棧,本地方法棧和程序計數(shù)器是線程隔離的數(shù)據(jù)區(qū)。
二、詳解
下面來具體介紹這幾個數(shù)據(jù)區(qū)。
1、程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,其作用可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解析器工作時通過改變程序計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。程序的分支、循環(huán)、跳轉(zhuǎn)、異常處理以及線程恢復(fù)等基礎(chǔ)功能都是依賴程序計數(shù)器來完成。
Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間片來實現(xiàn),在任何一個時刻,一個處理器只會執(zhí)行一條線程指令。因此,為了確保線程切換之后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個獨立的程序計數(shù)器,因此程序計數(shù)器是線程私有的內(nèi)存。
如果線程正在執(zhí)行一個java方法,則計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的是Native方法( 簡單地講,一個Native Method就是一個java調(diào)用非java代碼的接口。該方法的實現(xiàn)由非java語言實現(xiàn),比如C。這個特征并非java所特有,很多其它的編程語言都有這一機制,比如在C++中,你可以用extern "C"告知C++編譯器去調(diào)用一個C的函數(shù)。),則計數(shù)器的值為空。程序計數(shù)器是java虛擬機中唯一一個沒有規(guī)定任何內(nèi)存溢出OutOfMemoryError情況的內(nèi)存區(qū)域。
2、java虛擬機棧
Java虛擬機棧也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行時都會同時創(chuàng)建一個棧幀用于存放局部變量表、操作數(shù)棧、動態(tài)連接和方法出口等信息。每個方法被調(diào)用直至執(zhí)行完成過程,就對應(yīng)著一個棧幀在虛擬機中從入棧到出棧的過程。
Java虛擬機棧的局部變量表存放了編譯器可知的8種java基本類型數(shù)據(jù)(boolean、byte、char、short、int、float、long、double)、對象引用refrence(注意不是對象實例本身)、方法返回地址returnAddress(指向了一條字節(jié)碼指令的地址)。
Java虛擬機棧的局部變量表空間單位是槽(Slot),其中64位長度的double和long類型會占用兩個slot,其余的數(shù)據(jù)類型只占用一個slot。局部變量表所需內(nèi)存空間在編譯期間完成分配,當進入一個方法時,該方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
Java虛擬機棧有兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的大深度時,拋出StackOverflowError異常;如果虛擬機??梢詣討B(tài)擴展,當擴展時無法申請到足夠內(nèi)存時會拋出OutOfMemoryError異常。
3、本地方法棧
本地方法棧與java虛擬機棧作用非常類似,其區(qū)別是:java虛擬機棧是為虛擬機執(zhí)行java方法服務(wù),而本地方法棧是為虛擬機調(diào)用的操作系統(tǒng)本地方法服務(wù)(如Native方法)。
Java虛擬機規(guī)范沒有對本地方法棧的實現(xiàn)和數(shù)據(jù)結(jié)構(gòu)做強制規(guī)定,Sun HotSpot虛擬機直接把java虛擬機棧和本地方法棧合二為一。
與java虛擬機棧類似,本地方法棧也會拋出StackOverflowError異常和OutOfMemoryError異常。
4、堆
堆是java虛擬機所管理的內(nèi)存區(qū)域中大一塊,java堆是被所有線程所共享的一塊內(nèi)存區(qū)域,在java虛擬機啟動時創(chuàng)建,堆內(nèi)存的唯一目的就是存放對象實例。幾乎所有的對象實例(包括數(shù)組)都是在堆分配內(nèi)存。
Java堆是垃圾收集器管理的主要區(qū)域,從垃圾回收的角度看,由于現(xiàn)在的垃圾收集器基本都采用的是分代收集算法,因此java堆還可以初步細分為新生代和年老代。
Java虛擬機規(guī)范規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。在實現(xiàn)上即可以是固定大小的,也可以是可動態(tài)擴展的。如果在堆中沒有內(nèi)存完成實例分配,并且堆大小也無法在擴展時,將會拋出OutOfMemoryError異常。
堆設(shè)置的常用參數(shù)如下:??
-Xms :初始堆大小??
-Xmx :大堆大小,當Xms和Xmx設(shè)置相同時,堆就無法進行自動擴展。
-XX:NewSize=n :設(shè)置年輕代大小??
-XX:NewRatio=n: 設(shè)置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4??
-XX:SurvivorRatio=n :年輕代中Eden區(qū)與Survivor區(qū)的比值。注意Survivor區(qū)有兩個。如:XX:SurvivorRatio=3,表示Eden區(qū)的大小是一個Survivor區(qū)的三倍,但Survivor區(qū)有兩個,那么Eden:Survivor=3:2,一個Survivor區(qū)占整個年輕代的1/5。?
-XX:MaxPermSize=n :設(shè)置持久代大小??
5、方法區(qū)
方法區(qū)與堆一樣,是被各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯后的代碼等數(shù)據(jù)。雖然java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是方法區(qū)卻有一個別名叫Non-Heap(非堆)。
Sun HotSpot虛擬機把方法區(qū)叫永久代(Permanent Generation),方法區(qū)中最重要的部分是運行時常量池。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面變量、符號引用、直接引用等,這些內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中,另外在運行期間也可以將新的常量存放到常量池中,如String的intern()方法。當調(diào)用 intern 方法時,如果常量池已經(jīng)包含一個等于此 String 對象的字符串(該對象由 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,并且返回此 String 對象的引用。
方法區(qū)和運行時常量池在無法滿足內(nèi)存分配時,也會拋出OutOfMemoryError異常。
6、直接內(nèi)存
直接內(nèi)存并不是java虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但是在java開發(fā)中還是會使用到。
JDK1.4中新引入的NIO(new I/O),引入了一種基于通道(Channel)和緩沖區(qū)(Buffer)的I/O方式,可以使用操作系統(tǒng)本地方法庫直接分配堆外內(nèi)存,然后通過一個存儲在java堆里面的DirectByteBuffer對象作為堆外直接內(nèi)存的引用進行操作,避免了java堆內(nèi)存和本地直接內(nèi)存間的數(shù)據(jù)拷貝,可以顯著提高性能。
雖然直接內(nèi)存并不直接收到j(luò)ava虛擬機內(nèi)存影響,但是如果java虛擬機各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制,從而導(dǎo)致直接內(nèi)存不足,動態(tài)擴展時也會拋出OutOfMemoryError異常。
三、實例演示
下面通過簡單的小例子程序,演示Java虛擬機各部分內(nèi)存溢出情況:
1、java堆溢出
Java堆用于存儲實例對象,只要不斷創(chuàng)建對象,并且保證GC Roots到對象之間有引用的可達,來避免垃圾收集器回收實例對象,就會在對象數(shù)量達到堆大容量時產(chǎn)生OutOfMemoryError異常。
想要方便快速地產(chǎn)生堆溢出,要使用如下java虛擬機參數(shù):-Xms=10m(最小堆內(nèi)存為10MB),-Xmx=10m(大堆內(nèi)存為10MB,最小堆內(nèi)存和大堆內(nèi)存相同是為了避免堆動態(tài)擴展),-XX:+HeapDumpOnOutOfMemoryError可以讓java虛擬機在出現(xiàn)內(nèi)存溢出時產(chǎn)生當前堆內(nèi)存快照以便進行異常分析。
例子代碼如下:
public class HeapOOM{??
? static class OOMObject{??
}??
public static void main(String[] args){??
? List
? while(true){??
? list.add(new OOMObject());??
}??
}??
}??
運行一段時間就會發(fā)現(xiàn)產(chǎn)生OutOfMemoryError異常,并且產(chǎn)生了堆內(nèi)存異常dump文件。
2、java虛擬機棧和本地方法棧溢出
由于Sun的HotSpot虛擬機不區(qū)分java虛擬機棧和本地方法棧,因此對于HotSpot虛擬機來說-Xoss參數(shù)(設(shè)置本地方法棧大小)雖然存在,但是實際上是無效的,棧容量只能由-Xss參數(shù)設(shè)定。
由于Java虛擬機棧會出現(xiàn)StackOverflowError和OutOfMemoryError兩種異常,所以分別使用兩個例子演示這兩種情況:
(1)java虛擬機棧深度溢出
單線程的環(huán)境下,無論是由于棧幀太大,還是虛擬機棧容量太小,當內(nèi)存無法再分配的時候,虛擬機總拋出StackOverflowError異常。使用-Xss128k將java虛擬機棧大小設(shè)置為128kb,例子代碼如下:
public class JavaVMStackOF{??
? private int stackLength = 1;??
? public void stackLeak(){??
? statckLength++;??
? stackLeak();??
}??
public static void main(String[] args){??
? JavaVMStackOF oom = new JavaVMStackOF();??
oom.stackLeak();??
}??
}??
運行一段時間后,產(chǎn)生StackOverflowError異常。Java虛擬機棧溢出一般會產(chǎn)生在方法遞歸調(diào)用過多而java虛擬機棧內(nèi)存不夠的情況下。
(2)java虛擬機棧內(nèi)存溢出
多線程環(huán)境下,能夠創(chuàng)建的線程大內(nèi)存=物理內(nèi)存-大堆內(nèi)存-大方法區(qū)內(nèi)存,在java虛擬機棧內(nèi)存一定的情況下,單個線程占用的內(nèi)存越大,所能創(chuàng)建的線程數(shù)目越小,所以在多線程條件下很容易產(chǎn)生java虛擬機棧內(nèi)存溢出的異常。
使用-Xss2m參數(shù)設(shè)置java虛擬機棧內(nèi)存大小為2MB,例子代碼如下:
public class JavaVMStackOOM{??
? private void dontStop(){??
? while(true){??
}??
}??
public void stackLeakByThread(){??
? while(true){??
? Thread t = new Thread(new Runnable(){??
? public void run(){??
? dontStop();??
}??
});??
t.start();??
}??
}? ?
public static void main(String[] args){??
? JavaVMStackOOM oom = new JavaVMStackOOM();??
? oom. stackLeakByThread();.??
}??
}??
運行一段時間之后,java虛擬機棧就會因為內(nèi)存太小無法創(chuàng)建線程而產(chǎn)生OutOfMemoryError。
3、運行時常量池溢出
運行時常量池屬于方法區(qū)的一部分,可以使用-XX:PermSize=10m和-XX:MaxPermSize=10m將永久代大內(nèi)存和最小內(nèi)存設(shè)置為10MB大小,并且由于永久代大內(nèi)存和最小內(nèi)存大小相同,因此無法擴展。
String的intern()方法用于檢查常量池中如果有等于此String對象的字符串存在,則直接返回常量池中的字符串對象,否則,將此String對象所包含的字符串添加到運行時常量池中,并返回此String對象的引用。因此String的intern()方法特別適合演示運行時常量池溢出,例子代碼如下:
public class RuntimeConstantPoolOOM{??
? public static void main(String[] args){??
List
? int i = 0;??
? while(true){??
? list.add(String.valueOf(i++).intern());??
}??
}??
}??
運行一段時間,永久代內(nèi)存不夠,運行時常量池因無法再添加常量而產(chǎn)生OutOfMemoryError。
4、方法區(qū)溢出
運行時常量池是方法區(qū)的一部分,他們都屬于HotSpot虛擬機中的永久代內(nèi)存區(qū)域。方法區(qū)用于存放Class的相關(guān)信息,Java的反射和動態(tài)代理可以動態(tài)產(chǎn)生Class,另外第三方的CGLIB可以直接操作字節(jié)碼,也可以動態(tài)產(chǎn)生Class,實驗通過CGLIB來演示,同樣使用-XX:PermSize=10m和-XX:MaxPermSize=10m將永久代大內(nèi)存和最小內(nèi)存設(shè)置為10MB大小,并且由于永久代大內(nèi)存和最小內(nèi)存大小相同,因此無法擴展。例子代碼如下:
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(){??
? public Object intercept(Object obj, Method method, Object[] args,? ?
MethodProxy proxy)throws Throwable{??
? return proxy.invokeSuper(obj, args);??
}??
});??
enhancer.create();??
}??
}??
class OOMObject{??
}? ?
}??
運行一段時間之后,永久代內(nèi)存不夠,方法區(qū)無法再存放CGLIB創(chuàng)建處理的Class信息,產(chǎn)生方法區(qū)OutOfMemoryError。
5、本機直接內(nèi)存溢出
Java虛擬機可以通過參數(shù)-XX:MaxDirectMemorySize設(shè)定本機直接內(nèi)存可用大小,如果不指定,則默認與java堆內(nèi)存大小相同。JDK中可以通過反射獲取Unsafe類(Unsafe的getUnsafe()方法只有啟動類加載器Bootstrap才能返回實例)直接操作本機直接內(nèi)存。通過使用-XX:MaxDirectMemorySize=10M,限制大可使用的本機直接內(nèi)存大小為10MB,例子代碼如下:
public class DirectMemoryOOM{??
? private static final int _1MB = 1024* 1024 * 1024;??
? publc static void main(String[] args) throws Exception{??
? Field unsafeField = Unsafe.class.getDeclaredFields()[0];??
? unsafeField.setAccessible(true);??
? Unsafe unsafe = (Unsafe) unsafeField.get(null);??
? while(true){??
? //unsafe直接想操作系統(tǒng)申請內(nèi)存??
? unsafe.allocateMemory(_1MB);??
}??
}??
}??
當運行一段時間之后,10MB的本機直接內(nèi)存被分配光,無法在進行直接內(nèi)存分配時,產(chǎn)生OutOfMemoryError。
三、總結(jié)
java虛擬機內(nèi)存結(jié)構(gòu)中的程序計數(shù)器、虛擬機棧和本地方法棧這三個區(qū)域隨線程創(chuàng)建而生,隨線程銷毀而滅,因此這三個區(qū)域的內(nèi)存分配和回收是確定的,java垃圾收集器重點關(guān)注的是java虛擬機的堆內(nèi)存和方法區(qū)內(nèi)存。
歡迎加入Java技術(shù)交流群:659270626
群內(nèi)提供免費的學(xué)習(xí)指導(dǎo),提供Spring源碼、MyBatis、Netty、Redis,Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx、分布式、高并發(fā)、性能調(diào)優(yōu)等架構(gòu)技術(shù)架構(gòu)資料以及免費的解答
不懂的問題都可以在本群提出來,之后還會有職業(yè)生涯規(guī)劃以及面試指導(dǎo) 。
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國云服務(wù)器,動態(tài)BGP最優(yōu)骨干路由自動選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機房獨有T級流量清洗系統(tǒng)配攻擊溯源,準確進行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動現(xiàn)已開啟,新人活動云服務(wù)器買多久送多久。