目錄介紹
- 6.0.0.1 運作時資料區域有哪些?Java虛拟機棧是做什麼的?本地方法棧又是做什麼的?
- 6.0.0.2 對象的記憶體布局?對象的通路定位方式有哪些?使用指針通路和使用句柄通路各具有何優勢?
- 6.0.0.3 說一下對象的建立過程?變量建立過程種放在虛拟機哪裡?
- 6.0.0.4 OutOfMemoryError異常在哪些資料區域中可能會出現?分别說一下這個資料區域出現OOM的場景和緣由?
- 6.0.0.6 Java中堆和棧的差別?分别寫出堆記憶體溢出與棧記憶體溢出的程式?
- 6.0.0.7 如果對象的引用被置為null,垃圾收集器是否會立即釋放對象占用的記憶體?
- 6.0.0.8 java中垃圾收集的方法有哪些?
- 6.0.1.1 如和判斷一個對象是否存活?引用計數法和可達性算法哪個更加好?如何了解一個對象不一定會被回收?
- 6.0.1.2 Class.forName() 和ClassLoader.loadClass()差別?
好消息
- 部落格筆記大彙總【15年10月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護并且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計500篇[近100萬字],将會陸續發表到網上,轉載請注明出處,謝謝!
- 連結位址: https://github.com/yangchong211/YCBlogs
- 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!所有部落格将陸續開源到GitHub!
- 運作時資料區域有哪些?
- Java虛拟機管理的記憶體包括幾個運作時資料記憶體:方法區、虛拟機棧、本地方法棧、堆、程式計數器,其中方法區和堆是由線程共享的資料區,其他幾個是線程隔離的資料區
- 1.1 程式計數器
- 程式計數器是一塊較小的記憶體,他可以看做是目前線程所執行的行号訓示器。位元組碼解釋器工作的時候就是通過改變這個計數器的值來選取下一條需要執行的位元組碼的指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器則為空。此記憶體區域是唯一一個在Java虛拟機規範中沒有規定任何OutOfMemotyError情況的區域
- 1.2 Java虛拟機棧
- 虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用于儲存局部變量表、操作數棧、動态連結、方法出口等資訊。每個方法從調用直至完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。
- 技術部落格大總結
- 棧記憶體就是虛拟機棧,或者說是虛拟機棧中局部變量表的部分
- 局部變量表存放了編輯期可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用(refrence)類型和returnAddress類型(指向了一條位元組碼指令的位址)
- 其中64位長度的long和double類型的資料會占用兩個局部變量空間,其餘的資料類型隻占用1個。
- Java虛拟機規範對這個區域規定了兩種異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常。如果虛拟機擴充時無法申請到足夠的記憶體,就會跑出OutOfMemoryError異常
- 1.3 本地方法棧
- 本地方法棧和虛拟機棧發揮的作用是非常類似的,他們的差別是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的Native方法服務
- 本地方法棧區域也會抛出StackOverflowError和OutOfMemoryErroy異常
- 1.4 Java堆
- 堆是Java虛拟機所管理的記憶體中最大的一塊。Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動的時候建立,此記憶體區域的唯一目的是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。所有的對象執行個體和數組都在堆上配置設定
- Java堆是垃圾收集器管理的主要區域。Java堆細分為新生代和老年代
- 不管怎樣,劃分的目的都是為了更好的回收記憶體,或者更快地配置設定記憶體
- Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可。如果在堆中沒有完成執行個體配置設定,并且堆也無法在擴充時将會抛出OutOfMemoryError異常
- 1.5 方法區
- 方法區它用于儲存已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料
- 除了Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。這個區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝
- 當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryErroy異常
- 1.6 運作時常量池
- 它是方法區的一部分。Class檔案中除了有關的版本、字段、方法、接口等描述資訊外、還有一項資訊是常量池,用于存放編輯期生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放
- Java語言并不要求常量一定隻有編輯期才能産生,也就是可能将新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法
- 當常量池無法再申請到記憶體時會抛出OutOfMemoryError異常
- 對象的記憶體布局?
- 在HotSpot虛拟機中,對象在記憶體中儲存的布局可以分為3塊區域:對象頭、執行個體資料和對齊填充
- 對象頭包括兩部分:
- a) 儲存對象自身的運作時資料,如哈希碼、GC分帶年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳
- b) 另一部分是指類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是那個類的執行個體
- 對象的通路定位方式有哪些?
- 使用句柄通路
- Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址
- 使用直接指針通路
- Java堆對象的布局就必須考慮如何通路類型資料的相關資訊,而refreence中存儲的直接就是對象的位址
- 使用句柄通路
- 使用指針通路和使用句柄通路各具有何優勢?
- 說一下對象的建立過程?比如:Dog dog= new Dog();
- 當虛拟機執行到new指令時,它先在常量池中查找“Dog”,看能否定位到Dog類的符号引用;如果能,說明這個類已經被加載到方法區了,則繼續執行。如果沒有,就讓Class Loader先執行類的加載。
- 然後,虛拟機開始為該對象配置設定記憶體,對象所需要的記憶體大小在類加載完成後就已經确定了。這時候隻要在堆中按需求配置設定空間即可。具體配置設定記憶體時有兩種方式,第一種,記憶體絕對規整,那麼隻要在被占用記憶體和空閑記憶體間放置指針即可,每次配置設定空間時隻要把指針向空閑記憶體空間移動相應距離即可,當某對象被GC回收後,則需要進行某些對象記憶體的遷移。第二種,空閑記憶體和非空閑記憶體夾雜在一起,那麼就需要用一個清單來記錄堆記憶體的使用情況,然後按需配置設定記憶體。
- 對于多線程的情況,如何確定一個線程配置設定了對象記憶體但尚未修改記憶體管理指針時,其他線程又配置設定該塊記憶體而覆寫的情況?有一種方法,就是讓每一個線程在堆中先預配置設定一小塊記憶體(TLAB本地線程配置設定緩沖),每個線程隻在自己的記憶體中配置設定記憶體。但對象本身按其通路屬性是可以線程共享通路的。
- 記憶體配置設定到後,虛拟機将配置設定的記憶體空間都初始化為零值(不包括對象頭)。執行個體變量按變量類型初始化相應的預設值(數值型為0,boolan為false),是以執行個體變量不賦初值也能使用。接着設定對象頭資訊,比如對象的哈希值,GC分代年齡等。
- 從虛拟機角度,此時一個新的對象已經建立完成了。但從我們程式運作的角度,建立對象才剛剛開始,對象的構造方法還沒有執行。隻有執行完構造方法,按構造方法進行初始化後,對象才是徹底建立完成了。構造函數的執行還涉及到調用父類構造器,如果沒有顯式聲明調用父類構造器,則自動添加預設構造器。
- new運算符可以傳回堆中這個對象的引用
- 變量建立過程種放在虛拟機哪裡?
- 變量是執行個體變量、局部變量或靜态變量的不同将引用放在不同的地方:
- 如果dog局部變量,dog變量在棧幀的局部變量表,這個對象的引用就放在棧幀。
- 如果dog是執行個體變量,dog變量在堆中,對象的引用就放在堆。
- 如果dog是靜态變量,dog變量在方法區,對象的引用就放在方法區。
- 變量是執行個體變量、局部變量或靜态變量的不同将引用放在不同的地方:
- OutOfMemoryError異常在哪些資料區域中可能會出現?
- Java堆溢出
- 虛拟機棧和本地方法棧溢出
- 方法區和運作時常量池溢出
- 分别說一下這個資料區域出現OOM的場景和緣由?
-
- Java堆用于存儲對象執行個體,隻要不斷的建立對象,并且保證GCRoots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在數量到達最大堆的容量限制後就會産生記憶體溢出異常
- 如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到GC Roots的引用鍊。于是就能找到洩露對象是通過怎樣的路徑與GC Roots相關聯并導緻垃圾收集器無法自動回收它們的。掌握了洩漏對象的類型資訊及GC Roots引用鍊的資訊,就可以比較準确地定位出洩漏代碼的位置
- 如果不存在洩露,換句話說,就是記憶體中的對象确實都還必須存活着,那就應當檢查虛拟機的堆參數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期的記憶體消耗
-
- 對于HotSpot來說,雖然-Xoss參數(設定本地方法棧大小)存在,但實際上是無效的,棧容量隻由-Xss參數設定。關于虛拟機棧和本地方法棧,在Java虛拟機規範中描述了兩種異常:
- 如果線程請求的棧深度大于虛拟機所允許的最大深度,将抛出StackOverflowError
- 如果虛拟機在擴充棧時無法申請到足夠的記憶體空間,則抛出OutOfMemoryError異常
- 在單線程下,無論由于棧幀太大還是虛拟機棧容量太小,當記憶體無法配置設定的時候,虛拟機抛出的都是StackOverflowError異常
- 如果是多線程導緻的記憶體溢出,與棧空間是否足夠大并不存在任何聯系,這個時候每個線程的棧配置設定的記憶體越大,反而越容易産生記憶體溢出異常。解決的時候是在不能減少線程數或更換64為的虛拟機的情況下,就隻能通過減少最大堆和減少棧容量來換取更多的線程
-
- String.intern()是一個Native方法,它的作用是:如果字元串常量池中已經包含一個等于此String對象的字元串,則傳回代表池中這個字元串的String對象;否則,将此String對象包含的字元串添加到常量池中,并且傳回此String對象的引用
- 由于常量池配置設定在永久代中,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,進而間接限制其中常量池的容量。
- Intern():JDK1.6 intern方法會把首次遇到的字元串執行個體複制到永久代,傳回的也是永久代中這個字元串執行個體的引用,而由StringBuilder建立的字元串執行個體在Java堆上,是以必然不是一個引用。JDK1.7 intern()方法的實作不會再複制執行個體,隻是在常量池中記錄首次出現的執行個體引用,是以intern()傳回的引用和由StringBuilder建立的那個字元串執行個體是同一個
-
- Java中堆和棧的差別?
- 棧記憶體:主要用來存放基本資料類型和局部變量;當在代碼塊定義一個變量時會在棧中為這個變量配置設定記憶體空間,當超過變量的作用域後這塊空間就會被自動釋放掉。
- 堆記憶體:用來存放運作時建立的對象,比如通過new關鍵字建立出來的對象和數組;需要由Java虛拟機的自動垃圾回收器來管理。
- 分别寫出堆記憶體溢出與棧記憶體溢出的程式?
- 棧記憶體溢出
public void A() { A(); }
- 堆記憶體溢出
public void testd() { List<String> list = new ArrayList<>(); int i = 0; while (true) { list.add(new String(i + "")); i++; } }
- 如果對象的引用被置為null,垃圾收集器是否會立即釋放對象占用的記憶體?
- 不會,在下一個垃圾回收周期中,這個對象将是可被回收的。
- 也就是說當一個對象的引用變為null時,并不會被垃圾收集器立刻回收,而是在下一次垃圾回收時才會釋放其占用的記憶體。
- java中垃圾收集的方法有哪些
- 标記-清除:
- 這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是标記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,标記和清除的效率都很低;2.會産生大量不連續的記憶體碎片,導緻以後程式在配置設定較大的對象時,由于沒有充足的連續記憶體而提前觸發一次GC動作。
- 複制算法:
- 為了解決效率問題,複制算法将可用記憶體按容量劃分為相等的兩部分,然後每次隻使用其中的一塊,當一塊記憶體用完時,就将還存活的對象複制到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再将第二塊上的對象複制到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。
- 于是将該算法進行了改進,記憶體區域不再是按照1:1去劃分,而是将記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就将對象複制到第二塊記憶體區上,然後清除Eden區,如果此時存活的對象太多,以至于Survivor不夠時,會将這些對象通過配置設定擔保機制複制到老年代中。(java堆又分為新生代和老年代)
- 标記-整理
- 該算法主要是為了解決标記-清除,産生大量記憶體碎片的問題;當對象存活率較高時,也解決了複制算法的效率問題。它的不同之處就是在清除對象的時候現将可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會産生記憶體碎片了。
- 分代收集
- 現在的虛拟機垃圾收集大多采用這種方式,它根據對象的生存周期,将堆分為新生代和老年代。在新生代中,由于對象生存期短,每次回收都會有大量對象死去,那麼這時就采用複制算法。老年代裡的對象存活率較高,沒有額外的空間進行配置設定擔保,是以可以使用标記-整理 或者 标記-清除。
- 标記-清除:
-
- 引用計數法
- 所謂引用計數法就是給每一個對象設定一個引用計數器,每當有一個地方引用這個對象時,就将計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器為零時,說明此對象沒有被引用,也就是“死對象”,将會被垃圾回收.
- 引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象A引用對象B,對象B又引用者對象A,那麼此時A,B對象的引用計數器都不為零,也就造成無法完成垃圾回收,是以主流的虛拟機都沒有采用這種算法。
- 2.可達性算法(引用鍊法)
- 該算法的思想是:從一個被稱為GC Roots的對象開始向下搜尋,如果一個對象到GC Roots沒有任何引用鍊相連時,則說明此對象不可用。
- 在java中可以作為GC Roots的對象有以下幾種:
- 虛拟機棧中引用的對象
- 方法區類靜态屬性引用的對象
- 方法區常量池引用的對象
- 本地方法棧JNI引用的對象
- 如何了解一個對象不一定會被回收?
- 雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達GC Root時,這個對象并不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經曆兩次标記
- 如果對象在可達性分析中沒有與GCRoot的引用鍊,那麼此時就會被第一次标記并且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法或者已被虛拟機調用過,那麼就認為是沒必要的。
- 如果該對象有必要執行finalize()方法,那麼這個對象将會放在一個稱為F-Queue的對隊列中,虛拟機會觸發一個Finalize()線程去執行,此線程是低優先級的,并且虛拟機不會承諾一直等待它運作完,這是因為如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue隊列一直等待,造成了記憶體回收系統的崩潰。GC對處于F-Queue中的對象進行第二次被标記,這時,該對象将被移除”即将回收”集合,等待回收。
- Class.forName() 和ClassLoader.loadClass()差別?
- 問到的是反射,但是在底層涉及到了虛拟機的類加載知識。
- Class.forName() 預設執行類加載過程中的連接配接與初始化動作,一旦執行初始化動作,靜态變量就會被初始化為程式員設定的值,如果有靜态代碼塊,靜态代碼塊也會被執行
- ClassLoader.loadClass() 預設隻執行類加載過程中的加載動作,後面的動作都不會執行
其他介紹
01.關于部落格彙總連結
02.關于我的部落格
- 我的個人站點:www.yczbj.org,www.ycbjie.cn
- github: https://github.com/yangchong211
- 知乎: https://www.zhihu.com/people/yang-chong-69-24/pins/posts
- 簡書: http://www.jianshu.com/u/b7b2c6ed9284
- csdn: http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書: http://www.ximalaya.com/zhubo/71989305/
- 開源中國: https://my.oschina.net/zbj1618/blog
- 泡在網上的日子: http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿裡雲部落格: https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條: https://segmentfault.com/u/xiangjianyu/articles
- 掘金: https://juejin.im/user/5939433efe88c2006afa0c6e