關於java內存區域部分的堆,棧,方法區三個部分而言,我總結了一下大概爲一下這張圖。java
如下是我第一次初學堆,棧,方法區的一些心得,若有不足,但願各位大佬們指點一下linux
java虛擬機的基本架構就是採用棧來進行設計的。當一個程序須要運行的時候,因爲要預先內存空間和運行的生命週期,因此須要進行指針的變更,來進行內存大小的分配。是的,因爲這個操做會對程序的執行帶來必定的不方便,因此通常棧被用來存放一些基本的變量類型或者引用對象的地址,而對於存儲數據量較爲龐大的java對象責備存儲在了堆裏面了。程序員
我對於硬件部分也不是很理解,因此這裏也只好從軟件方面來分析,總結分析以後得出緣由有如下幾個:
1.棧裏面的內存大小通常都是程序啓動的時候由系統分配好的。
2.堆的內存大小須要在使用的時候纔回去申請,並且每次對於內存大小的申請和歸還都會比較消耗性能,開銷較大。
3.cpu裏面會有專門的寄存器來操做棧,堆裏面都是使用間接尋址的方式來進行對象查找的,因此棧會快一些。面試
這個緣由咱們能夠舉一個例子來講吧:
假設有這麼一段代碼:
int j=10;
int k=10;
j=12;
對於這段簡單的代碼而言,虛擬機裏面的棧又是如何處理的呢?其實比較簡單的理解就是以下所示:
首先在棧裏面會先查找是否存在一個區域存放有10,若是沒有就建立一個區域存放10。而後將j引用到這個變量裏面去。接着當程序執行到第二句的時候,虛擬機會先到棧裏面去查找一下是否存在一個值爲10的區域,若是有就直接讓k指向10。因此基本以下圖所示:小程序
當後期對於j變量的值進行修改成12之後,棧就會重複性的先去查找是否有12,若是沒有開闢新的空間用於存儲12,而後讓j指向這個值windows
這裏面的10這個值,被k,j共同引用的過程就叫作數據共享。數組
關於這個問題,我在《深刻理解jvm&gc》這本書裏面看到了一個比較好的解釋,棧裏面存儲了不少小項,這裏面咱們稱之爲棧幀,這是一種數據結構用於存儲方法的局部變量表,操做數棧,動態連接方法還有上下文數據等信息。
那麼咱們怎麼來理解這個棧幀呢?每一個棧幀裏面都會存儲有相應的一下內容:
1.局部變量表
2.操做數棧
3.動態連接
4.返回地址
這些東西第一次據說通常都會感受比較陌生,那麼咱們來逐個逐個一一講解:
局部變量表:
這裏面的做用主要是存儲一系列的變量信息,並且這些變量都是以數字數組的形式來存儲的,通常而言byte,short,char,類型的數據在存儲的時候會變爲int類型,boolean類型也是存儲爲數字類型,long,double則是轉換爲雙字節大小的控件存儲在棧裏面。
操做數棧:
關於這個小編也是有點不太瞭解,看了幾篇博客以後也只是知道這個東西是能夠將指令在棧裏面進行push和pop操做,也是一個數字數組類型。
關於詳情的分析能夠去閱讀如下這篇博客:
https://blog.csdn.net/qq_28666081/article/details/85269879
動態連接:
動態連接的做用主要仍是提供棧裏面的對象在進行實例化的時候,可以查找到堆裏面相應的類地址,並進行引用。這一整個過程,咱們稱之爲動態連接。
返回地址:
某個子方法執行完畢以後,須要回到主方法的原有位置繼續執行程序,方法出口主要就是記錄該信息數據結構
1.堆裏面存放的內容主要仍是new出來的對象和一些數組信息。
2.java的虛擬機不須要知道從堆內存裏面存放多少空間大小的變量信息,也不須要知道每一個對象的生命週期,因此通常程序運做的靈活性很高。
3.堆區裏面存放了大量的對象很信息,因此也成爲了gc重點回收的一個區域模塊,因此當大量內存被佔用的時候,gc的垃圾回收就會成爲整個系統的性能瓶頸。因而隨着jdk的不斷更新,新的技術也對於jvm的內存分配這一塊進行必定的優化改善,實現了off-heap。
好了,知道這些內容以後,基本簡單的面試問題均可以幹掉了,若是要深刻了解的話能夠從如下幾個方面入手:
關於垃圾回收的原理和分類?
能夠看看個人另外一篇文章:https://blog.csdn.net/Danny_idea/article/details/87475188
什麼是指針逃逸?如何對此進行優化?
能夠參考個人公衆號文章:https://mp.weixin.qq.com/s/tyF9Ve40C_uu4rwcs4tUsg多線程
關於這個概念也能夠稱之爲靜態區,靜態區和堆很類似,裏面存放的信息也是線程共享的,它包含的信息以下圖所示:架構
方法區中包含的都是在整個程序中永遠惟一的元素,如class,static變量。
方法區又被稱爲靜態區,是程序中永遠惟一的元素存儲區域。和堆同樣,是各個線程共享的內存區域。它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
Java虛擬機規範對方法區的限制很是寬鬆,除了和Java堆同樣 不須要連續的內存和能夠選擇固定大小或者可擴展以外,還能夠選擇不實現垃圾回收。
這區域的內存回收目標主要是針對常量池的回收和類型的卸載,通常而言,這個區域的內存回收比較難以使人滿意,尤爲是類型的回收,條件至關苛刻,可是這部分區域的內存回收確實是必要的。
不少開發者更願意把方法區稱爲「永久代」(Perm Gen)(Permanent Generation)「老是存放不會輕易改變的內容」。在目前已經發布的JDK 1.7 的HotSpot中,已經把本來放在永久代的字符串常量池移至堆中。
運行時常量池(Runtime Constant Pool)是方法區的一部分。
JDK 1.8 中,已經沒有方法區(永久代),而是將方法區直接放在一個與堆不相連的本地內存區域(Native Memory),這個區域被叫作元空間。
基礎數據類型直接在棧空間分配, 方法的形式參數,直接在棧空間分配,當方法調用完成後從棧空間回收。 引用數據類型,須要用new來建立,既在棧空間分配一個地址空間,又在堆空間分配對象的類變量 。 方法的引用參數,在棧空間分配一個地址空間,並指向堆空間的對象區,當方法調用完成後從棧空間回收。局部變量 new 出來時,在棧空間和堆空間中分配空間,當局部變量生命週期結束後,棧空間馬上被回收,堆空間區域等待GC回收。 方法調用時傳入的 literal 參數,先在棧空間分配,在方法調用完成後從棧空間分配。字符串常量在 DATA 區域分配 ,this 在堆空間分配 。數組既在棧空間分配數組名稱, 又在堆空間分配數組實際的大小!
哦 對了,補充一下static在DATA區域分配。
從Java的這種分配機制來看,堆棧又能夠這樣理解:堆棧(Stack)是操做系統在創建某個進程時或者線程(在支持多線程的操做系統中是線程)爲這個線程創建的存儲區域,該區域具備先進後出的特性。
每個Java應用都惟一對應一個JVM實例,每個實例惟一對應一個堆。應用程序在運行中所建立的全部類實例或數組都放在這個堆中,並由應用全部的線程共享.跟C/C++不一樣,Java中分配堆內存是自動初始化的。Java中全部對象的存儲空間都是在堆中分配的,可是這個對象的引用倒是在堆棧中分配,也就是說在創建一個對象時從兩個地方都分配內存,在堆中分配的內存實際創建這個對象,而在堆棧中分配的內存只是一個指向這個堆對象的指針(引用)而已。
這兩天看了一下深刻淺出JVM這本書,推薦給高級的java程序員去看,對你瞭解JAVA的底層和運行機制有
比較大的幫助。
廢話不想講了.入主題:
先了解具體的概念:
JAVA的JVM的內存可分爲3個區:堆(heap)、棧(stack)和方法區(method)
1.存儲的所有是對象,每一個對象都包含一個與之對應的class的信息。(class的目的是獲得操做指令)
2.jvm只有一個堆區(heap)被全部線程共享,堆中不存放基本類型和對象引用,只存放對象自己
1.每一個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中
2.每一個棧中的數據(原始類型和對象引用)都是私有的,其餘棧不能訪問。
3.棧分爲3個部分:基本類型變量區、執行環境上下文、操做指令區(存放操做指令)。
1.又叫靜態區,跟堆同樣,被全部的線程共享。方法區包含全部的class和static變量。
2.方法區中包含的都是在整個程序中永遠惟一的元素,如class,static變量。
爲了更清楚地搞明白髮生在運行時數據區裏的黑幕,咱們來準備2個小道具(2個很是簡單的小程序)。
AppMain.java
OK,讓咱們開始行動吧,出發指令就是:「java AppMain」,包包裏帶好咱們的行動向導圖,Let’s GO!
系統收到了咱們發出的指令,啓動了一個Java虛擬機進程,這個進程首先從classpath中找到AppMain.class文件,讀取這個文件中的二進制數據,而後把Appmain類的類信息存放到運行時數據區的方法區中。這一過程稱爲AppMain類的加載過程。
接着,Java虛擬機定位到方法區中AppMain類的Main()方法的字節碼,開始執行它的指令。這個main()方法的第一條語句就是:
Sample test1=new Sample("測試1");
語句很簡單啦,就是讓java虛擬機建立一個Sample實例,而且呢,使引用變量test1引用這個實例。貌似小case一樁哦,就讓咱們來跟蹤一下Java虛擬機,看看它到底是怎麼來執行這個任務的:
一、 Java虛擬機一看,不就是創建一個Sample實例嗎,簡單,因而就直奔方法區而去,先找到Sample類的類型信息再說。結果呢,嘿嘿,沒找到@@,這會兒的方法區裏尚未Sample類呢。可Java虛擬機也不是一根筋的笨蛋,因而,它發揚「本身動手,豐衣足食」的做風,立馬加載了Sample類,把Sample類的類型信息存放在方法區裏。
二、 好啦,資料找到了,下面就開始幹活啦。Java虛擬機作的第一件事情就是在堆區中爲一個新的Sample實例分配內存, 這個Sample實例持有着指向方法區的Sample類的類型信息的引用。這裏所說的引用,實際上指的是Sample類的類型信息在方法區中的內存地址,其實,就是有點相似於C語言裏的指針啦~~,而這個地址呢,就存放了在Sample實例的數據區裏。
三、 在JAVA虛擬機進程中,每一個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程,棧中的每個元素就被稱爲棧幀,每當線程調用一個方法的時候就會向方法棧壓入一個新幀。這裏的幀用來存儲方法的參數、局部變量和運算過程當中的臨時數據。OK,原理講完了,就讓咱們來繼續咱們的跟蹤行動!位於「=」前的Test1是一個在main()方法中定義的變量,可見,它是一個局部變量,所以,它被會添加到了執行main()方法的主線程的JAVA方法調用棧中。而「=」將把這個test1變量指向堆區中的Sample實例,也就是說,它持有指向Sample實例的引用。
OK,到這裏爲止呢,JAVA虛擬機就完成了這個簡單語句的執行任務。參考咱們的行動向導圖,咱們終於初步摸清了JAVA虛擬機的一點點底細了,COOL!
接下來,JAVA虛擬機將繼續執行後續指令,在堆區裏繼續建立另外一個Sample實例,而後依次執行它們的printName()方法。當JAVA虛擬機執行test1.printName()方法時,JAVA虛擬機根據局部變量test1持有的引用,定位到堆區中的Sample實例,再根據Sample實例持有的引用,定位到方法去中Sample類的類型信息,從而得到printName()方法的字節碼,接着執行printName()方法包含的指令。
在windows中使用taskmanager查看java進程使用的內存時,發現有時候會超過 -Xmx制定的內存大小, -Xmx指定的是java heap,java還要分配內存作其餘的事情,包括爲每一個線程創建棧。 VM的每一個線程都有本身的棧空間,棧空間的大小限制vm的線程數量,太大了,實用的線程數減小,過小容易拋出java.lang.StackOverflowError異常。windows默認爲1M,linux必須運行ulimit -s 2048。 在C語言裏堆(heap)和棧(stack)裏的區別 簡單的能夠理解爲: heap:是由malloc之類函數分配的空間所在地。地址是由低向高增加的。 stack:是自動分配變量,以及函數調用的時候所使用的一些空間。地址是由高向低減小。 一個由c/C++編譯的程序佔用的內存分爲如下幾個部分 一、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。 二、在Java語言裏堆(heap)和棧(stack)裏的區別 1. 棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方。與C++不一樣,Java自動管理棧和堆,程序員不能直接地設置棧或堆。 2. 棧的優點是,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。另外,棧數據能夠共享,詳見第3點。堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,Java的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態分配內存,存取速度較慢。 3. Java中的數據類型有兩種。 一種是基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並無string的基本類型)。這種類型的定義是經過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並無類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,因爲大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的緣由,就存在於棧中。 另外,棧有一個很重要的特殊性,就是存在棧中的數據能夠共享。假設咱們同時定義 int a = 3; int b = 3; 編譯器先處理int a = 3;首先它會在棧中建立一個變量爲a的引用,而後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,而後將a指向3的地址。接着處理int b = 3;在建立完b的引用變量後,因爲在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的狀況。 特別注意的是,這種字面值的引用與類對象的引用不一樣。假定兩個類對象的引用同時指向一個對象,若是一個對象引用變量修改了這個對象的內部狀態,那麼另外一個對象引用變量也即刻反映出這個變化。相反,經過字面值的引用來修改其值,不會致使另外一個指向此字面值的引用的值也跟着改變的狀況。如上例,咱們定義完a與 b的值後,再令a=4;那麼,b不會等於4,仍是等於3。在編譯器內部,遇到a=4;時,它就會從新搜索棧中是否有4的字面值,若是沒有,從新開闢地址存放4的值;若是已經有了,則直接將a指向這個地址。所以a值的改變不會影響到b的值。 另外一種是包裝類數據,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據所有存在於堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據須要動態建立,所以比較靈活,但缺點是要佔用更多的時間。 4.每一個JVM的線程都有本身的私有的棧空間,隨線程建立而建立,java的stack存放的是frames ,java的stack和c的不一樣,只是存放本地變量,返回值和調用方法,不容許直接push和pop frames ,由於frames 多是有heap分配的,因此j爲ava的stack分配的內存不須要是連續的。java的heap是全部線程共享的,堆存放全部 runtime data ,裏面是全部的對象實例和數組,heap是JVM啓動時建立。 5. String是一個特殊的包裝類數據。便可以用String str = new String("abc");的形式來建立,也能夠用String str = "abc";的形式來建立(做爲對比,在JDK 5.0以前,你從未見過Integer i = 3;的表達式,由於類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是能夠的!由於編譯器在後臺進行Integer i = new Integer(3)的轉換)。前者是規範的類的建立過程,即在Java中,一切都是對象,而對象是類的實例,所有經過new()的形式來建立。Java 中的有些類,如DateFormat類,能夠經過該類的getInstance()方法來返回一個新建立的類,彷佛違反了此原則。其實否則。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部經過new()來建立的,而getInstance()向外部隱藏了此細節。那爲何在String str = "abc";中,並無經過new()來建立實例,是否是違反了上述原則?其實沒有。 5. 關於String str = "abc"的內部工做。Java內部將此語句轉化爲如下幾個步驟: (1)先定義一個名爲str的對String類的對象引用變量:String str; (2)在棧中查找有沒有存放值爲"abc"的地址,若是沒有,則開闢一個存放字面值爲"abc"的地址,接着建立一個新的String類的對象o,並將o 的字符串值指向這個地址,並且在棧中這個地址旁邊記下這個引用的對象o。若是已經有了值爲"abc"的地址,則查找對象o,並返回o的地址。 (3)將str指向對象o的地址。 值得注意的是,通常String類中字符串值都是直接存值的。但像String str = "abc";這種場合下,其字符串值倒是保存了一個指向存在棧中數據的引用! 爲了更好地說明這個問題,咱們能夠經過如下的幾個代碼進行驗證。 String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); //true 注意,咱們這裏並不用str1.equals(str2);的方式,由於這將比較兩個字符串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而咱們在這裏要看的是,str1與str2是否都指向了同一個對象。 結果說明,JVM建立了兩個引用str1和str2,但只建立了一個對象,並且兩個引用都指向了這個對象。 咱們再來更進一步,將以上代碼改爲: String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; System.out.println(str1 + "," + str2); //bcd, abc System.out.println(str1==str2); //false 這就是說,賦值的變化致使了類對象引用的變化,str1指向了另一個新對象!而str2仍舊指向原來的對象。上例中,當咱們將str1的值改成"bcd"時,JVM發如今棧中沒有存放該值的地址,便開闢了這個地址,並建立了一個新的對象,其字符串的值指向這個地址。 事實上,String類被設計成爲不可改變(immutable)的類。若是你要改變其值,能夠,但JVM在運行時根據新值悄悄建立了一個新對象,而後將這個對象的地址返回給原來類的引用。這個建立過程雖然說是徹底自動進行的,但它畢竟佔用了更多的時間。在對時間要求比較敏感的環境中,會帶有必定的不良影響。 再修改原來代碼: String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; String str3 = str1; System.out.println(str3); //bcd String str4 = "bcd"; System.out.println(str1 == str4); //true str3 這個對象的引用直接指向str1所指向的對象(注意,str3並無建立新對象)。當str1改完其值後,再建立一個String的引用str4,並指向因str1修改值而建立的新的對象。能夠發現,這回str4也沒有建立新的對象,從而再次實現棧中數據的共享。 咱們再接着看如下的代碼。 String str1 = new String("abc"); String str2 = "abc"; System.out.println(str1==str2); //false 建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。 String str1 = "abc"; String str2 = new String("abc"); System.out.println(str1==str2); //false 建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。 以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中建立,並且其字符串是單獨存值的,即便與棧中的數據相同,也不會與棧中的數據共享。 6. 數據類型包裝類的值不可修改。不只僅是String類的值不可修改,全部的數據類型包裝類都不能更改其內部的值。 7. 結論與建議: (1)咱們在使用諸如String str = "abc";的格式定義類時,老是想固然地認爲,咱們建立了String類的對象str。擔憂陷阱!對象可能並無被建立!惟一能夠確定的是,指向 String類的引用被建立了。至於這個引用究竟是否指向了一個新的對象,必須根據上下文來考慮,除非你經過new()方法來顯要地建立一個新的對象。所以,更爲準確的說法是,咱們建立了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值爲"abc"的String類。清醒地認識到這一點對排除程序中難以發現的bug是頗有幫助的。 (2)使用String str = "abc";的方式,能夠在必定程度上提升程序的運行速度,由於JVM會自動根據棧中數據的實際狀況來決定是否有必要建立新對象。而對於String str = new String("abc");的代碼,則一律在堆中建立新對象,而無論其字符串值是否相等,是否有必要建立新對象,從而加劇了程序的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裏實現是否應用了這個模式,不得而知。 (3)當比較包裝類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。 (4)因爲String類的immutable性質,當String變量須要常常變換其值時,應該考慮使用StringBuffer類,以提升程序效率。 若是java不能成功分配heap的空間,將拋出OutOfMemoryError