其中線程共享區域:方法區 、 堆java
線程私有區域:虛擬機棧、本地方法棧、程序計數器程序員
程序計數器是一塊很是小的內存空間,主要是用來對當前線程所執行的字節碼的行號指示器;
並且程序計數器是內存區域中惟一一塊不存在OutOfMemoryError的區域
程序員常常說「堆棧」,其中的棧就是虛擬機棧,更確切的說,你們談的棧是虛擬機中的局部變量表部分;數組
虛擬機棧描述的是:Java方法執行的內存模型;(說白了就是:緩存
虛擬機棧就是用來存儲:局部變量、操做棧、動態鏈表、方法出口這些東西;服務器
這些東西有個特色:都是線程私有的,因此虛擬機棧是線程私有的)併發
由於虛擬機棧是私有的,當線程調用某一個方法再到這個方法執行結束;其實就是對應着一個棧幀在虛擬機入棧到出棧的過程;框架
對於虛擬機棧可能出現的異常有兩種:socket
1:若是線程請求的棧深度大於虛擬機棧容許的最大深度,報錯:StackOverflowError
(這種錯誤常常出如今遞歸操做中,無限制的反覆調用方法,最終致使壓棧深度超過虛擬機容許的最大深度,就會報錯)
2:java的虛擬機棧能夠進行動態擴展,但隨着擴展會不斷的申請內存,當沒法申請足夠內存的時候就會報錯:OutOfMemoryError
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務(好比C語言寫的程序和C++寫的程序)
Java堆是Java虛擬機管理內存最大的一塊區域;而且Java堆是被全部線程共享的一塊內存區域(最大的區域);函數
對於堆內存惟一的目的就是用來存放對象實例的,並且幾乎全部的對象實例都是堆內存中分配的內存(可能會有一些對象逃逸分析技術會致使對象實例不是在Java堆中進行內存分配的)高併發
常常會聽到一些程序說「調優」,其中調優的95%部分都是在跟Java堆有關係的;
由於Java堆是垃圾收集器管理的主要區域;
堆是JVM管理的內存最大的一塊,隨着虛擬機的啓動而建立的。全部數組的內存和new出來的對象都是從這裏分配的內存。簡單說就是程序員的代碼可觸及的地方.
並且一個JVM實例運行的時候只有一個Heap區域,這個區域會被全部的線程共享
堆裏面又分了區域
1、新生代
二、老年代
新生代
剛建立的對象都是在新生代的,而後垃圾回收的線程掃一遍,若是是垃圾就回收,不是垃圾就年齡+1,新生代和老年代的觸發點是能夠人爲配置的
老年代
新生代的對象進入老年代之後,仍然須要垃圾回收的線程掃描,若是發現垃圾了就回收,若是沒有垃圾的話,就年齡+1. 最後對象在無用後會被GC掉
調整參數:
-Xms: 2G
-Xmx:2G
-XX:NewSize和-XX:MaxNewSize用於設置年輕代的大小,建議設爲整個堆大小的1/3或者1/4,兩個值設爲同樣大。 -XX:SurvivorRatio 用來控制Eden和survivor Ratio比例 -Xms:JVM初始分配的內存由-xms決定。系統默認給-xms的大小是服務器物理內存的1/64 -Xmx:JVM最大分配的內存由-xmx決定。系統默認給-xmx的大小是服務器物理內存的1/4
注意:
當咱們服務器的空餘堆內存小於40%的時候,JVM就會增大堆內存,一直增長到-xmx限定的內存大小。 當咱們的服務器空餘內存大於70%的時候,,JVM就會減少堆內存,直到達到-xms限制的內存大小。 優化:所以,咱們通常都是把JVM的-xms和-xmx都設置成相等。避免每次GC夠調整堆的大小,形成內存抖動。
方法區和java堆同樣,是線程共享的區域;
方法區的做用的就是用來存儲:已經被虛擬機加載的類信息、常量、靜態變量等;
並且方法區還有另外一種叫法:【非堆】,也有人給方法區叫作永久代
當方法區存儲信息過大時候,也就是沒法知足內存分配的時候報錯
運行時常量池是方法區中的一部分,主要是用來存放程序編譯期生成的各類字面量和符號引用,也就是在類加載以後會進入方法區的運行時常量池中存放
直接內存(Direct Memory)並非運行時數據區中的部分;可是這塊兒每每會被大多數程序員忽略,不當心也會致使OOM的錯誤;
這是由於在JDK1.4以前java操做數據過程當中使用的IO操做是原始的socket IO
傳統的IO,經過socket的方式發送給服務端,須要幹些什麼事情:
1、先把文件讀到操做系統的緩存當中 2、再把操做系統的緩存中內容讀到應用中 3、再從應用的緩存當中讀到發送的socket緩存當中 四、在從socket緩存當中把數據發出去
總共作了4次的拷貝:
NIO:
NIO比較傳統IO的話,系統中的buffer再也不須要拷貝給應用了而是read buffer 直接拷貝給socket buffer咱們的應用只須要在兩個buffer之間創建一個管道的這樣省略了兩次的copy。速度就快了不少
NIO能夠直接使用Native(本地方法棧)函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲堆外內存的引用進行操做;
Vm Options添加:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 出現OOM錯誤的時候產生堆棧日誌:
使用MAT分析堆棧日誌:
打開堆內存的樹信息:
點開樹結構:
能夠看到出現OOM是由於建立了太多的OOM_demo這個類形成的,這樣經過代碼查找就能找到問題所在的根源了;
注意:大數據框架的堆棧信息也是經過以上方法搞定的;
Java虛擬機規範中描述了兩種異常:
1:若是線程請求的棧深度大於虛擬機容許的最大深度,則報錯:StackOverFlowError
2:若是虛擬機在擴展時沒法申請到足夠的內存空間,則拋出OutOfMemoryError
StackOverFlowError
-Xss128K
public class StackOverFlow { public static void main(String[] args) { new StackOverFlow().pushStack(); } int index = 0; public void pushStack(){ System.out.println("壓棧第 : "+index++); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread().getState(); pushStack(); } }
stackOOM
-Xss2m
public class Stack_OOM { public static volatile int index = 0; public static void main(String[] args) { new Stack_OOM().addThread(); } public void dont_stop(){ while (true){ } } public void addThread(){ while (true){ Thread thread = new Thread(new Runnable() { public void run() { System.out.println(index++); dont_stop(); } }); thread.start(); } } } public class Stack_OOM { public static volatile int index = 0; public static void main(String[] args) { new Stack_OOM().addThread(); } public void dont_stop(){ while (true){ } } public void addThread(){ while (true){ Thread thread = new Thread(new Runnable() { public void run() { System.out.println(index++); dont_stop(); } }); thread.start(); } } }
每一個方法在執行的時候都會建立一個棧幀,這個棧幀伴隨着方法從建立到執行完成;
對於虛擬機來講:總內存 –Xmx - MaxPermSize-程序計數器消耗內存(能夠忽略) = 虛擬機棧+本地方法棧
也就意味着說,若是每一個線程分配的內存越大,能夠建立的線程數就很是少,創建線程的時候就特別容易把內存吃光;
因此在出現建立過多的線程致使OOM的時候,咱們能夠減小最大堆外內存或者減小棧容量(-Xss)來提升線程的數量;這樣在高併發狀況下能夠解決由於建立過多的線程致使的OOM
方法區和運行時常量池溢出:
在方法區中有個小塊區域叫作「常量池」。
例子1:
String a = "123"; String b = "123"; System.out.println(a == b);
上面代碼的結果爲true,由於String + 變量 是將變量放在常量池的;
==操做比較的是兩個變量的值是否相等
對於引用型變量表示的是兩個變量在堆中存儲的地址是否相同,即棧中的內容是否相同。
equals操做表示的兩個變量是不是對同一個對象的引用,即堆中的內容是否相同
例子2:
String aa = new String("123"); String bb = new String("123"); System.out.println(aa == bb);
經過new生成的對象是放在堆區的,使用==比較對象的地址,因此結果爲false;
String c = "c"; String cc = new String("c"); System.out.println(c == cc);
上面的列子中String c = "c";是將變量值存放在常量池的;
String cc = new String("c");是將對象存放在堆中的,因此兩個在==號下是false
例子4:
String d = "d"; String dd = new String("d").intern(); System.out.println(d == dd);
String dd = new String("d")是將值存儲在對象中的
可是使用intern()方法以後,d == dd就變成true了
當調用intern()方法時,無論使用什麼方式定義一個字符串,都會首先在常量池中查找是否有相應的字符串存在,若是有,直接返回常量池中的引用,不然,在常量池中生成相應的字符串並返回引用
因此每每在生產上會在這方面作一些優化:
由於使用intern()比不使用intern()消耗的內存更少;
注意:使用intern()方法後程序運行時間有所增長,這是由於程序中每次都是用了new String後又進行intern()操做的耗時時間,可是不使用intern()佔用內存空間致使GC的時間是要遠遠大於這點時間的
但是若是有很是多的值經過intern存儲到常量池中的時候,常量池的空間或者說方法區的空間沒法在繼續添加值的時候,就會報錯:OutOfMemoryError:PermGen space
例子:
VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
List<String> list = new ArrayList<String>(); int i=0; while (true){ list.add(String.valueOf(i).intern()); }
注意:在JDK1.8之後,正式移除了永久代!,取而代之的是【元空間】
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存[因此元空間Metaspace仍然在非堆中]。所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集
因此上面的VM Args的參數稍微修改:
-XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m
public class Demo { public static void main(String[] args) { int index = 1; List<String> list = new ArrayList<String>(); while (true){ String str = "寫個長一點的字符串好報錯" + index++; list.add(str.intern()); } } }
報錯:
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
直接內存溢出
當程序中使用NIO存儲數據,存儲的數據容量超過了本地方法棧容許的容量的時候,就會報錯: java.lang.OutOfMemoryError: Direct buffer memory
-Xmx 20m -XX:MaxDirectMemorySize=10m -XX:+PrintGCDetails
代碼:
public class DirectMemOOM { private static final int _1m = 1024*1024; public static void main(String[] args) throws IllegalAccessException { ByteBuffer.allocateDirect(11*_1m); } }