06.Java虛擬機問題

目錄介紹

  • 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!

6.0.0.1 運行時數據區域有哪些?Java虛擬機棧是做什麼的?本地方法棧又是做什麼的?

  • 運行時數據區域有哪些?
    • 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異常

6.0.0.2 對象的內存佈局?對象的訪問定位方式有哪些?使用指針訪問和使用句柄訪問各具有何優勢?

  • 對象的內存佈局?
    • 在HotSpot虛擬機中,對象在內存中儲存的佈局可以分爲3塊區域:對象頭、實例數據和對齊填充
    • 對象頭包括兩部分:
      • a) 儲存對象自身的運行時數據,如哈希碼、GC分帶年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳
      • b) 另一部分是指類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例
  • 對象的訪問定位方式有哪些?
    • 使用句柄訪問
      • Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址
    • 使用直接指針訪問
      • Java堆對象的佈局就必須考慮如何訪問類型數據的相關信息,而refreence中存儲的直接就是對象的地址
  • 使用指針訪問和使用句柄訪問各具有何優勢?
    • 使用句柄訪問優勢:reference中存儲的是穩點的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改
    • 使用直接指針訪問優勢:速度更快,節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本
    • 技術博客大總結

6.0.0.3 說一下對象的創建過程?變量創建過程种放在虛擬機哪裏?

  • 說一下對象的創建過程?比如:Dog dog= new Dog();
    • 當虛擬機執行到new指令時,它先在常量池中查找“Dog”,看能否定位到Dog類的符號引用;如果能,說明這個類已經被加載到方法區了,則繼續執行。如果沒有,就讓Class Loader先執行類的加載。
    • 然後,虛擬機開始爲該對象分配內存,對象所需要的內存大小在類加載完成後就已經確定了。這時候只要在堆中按需求分配空間即可。具體分配內存時有兩種方式,第一種,內存絕對規整,那麼只要在被佔用內存和空閒內存間放置指針即可,每次分配空間時只要把指針向空閒內存空間移動相應距離即可,當某對象被GC回收後,則需要進行某些對象內存的遷移。第二種,空閒內存和非空閒內存夾雜在一起,那麼就需要用一個列表來記錄堆內存的使用情況,然後按需分配內存。
    • 對於多線程的情況,如何確保一個線程分配了對象內存但尚未修改內存管理指針時,其他線程又分配該塊內存而覆蓋的情況?有一種方法,就是讓每一個線程在堆中先預分配一小塊內存(TLAB本地線程分配緩衝),每個線程只在自己的內存中分配內存。但對象本身按其訪問屬性是可以線程共享訪問的。
    • 內存分配到後,虛擬機將分配的內存空間都初始化爲零值(不包括對象頭)。實例變量按變量類型初始化相應的默認值(數值型爲0,boolan爲false),所以實例變量不賦初值也能使用。接着設置對象頭信息,比如對象的哈希值,GC分代年齡等。技術博客大總結
    • 從虛擬機角度,此時一個新的對象已經創建完成了。但從我們程序運行的角度,新建對象纔剛剛開始,對象的構造方法還沒有執行。只有執行完構造方法,按構造方法進行初始化後,對象纔是徹底創建完成了。構造函數的執行還涉及到調用父類構造器,如果沒有顯式聲明調用父類構造器,則自動添加默認構造器。
    • new運算符可以返回堆中這個對象的引用
  • 變量創建過程种放在虛擬機哪裏?
    • 變量是實例變量、局部變量或靜態變量的不同將引用放在不同的地方:
      • 如果dog局部變量,dog變量在棧幀的局部變量表,這個對象的引用就放在棧幀。
      • 如果dog是實例變量,dog變量在堆中,對象的引用就放在堆。
      • 如果dog是靜態變量,dog變量在方法區,對象的引用就放在方法區。

6.0.0.4 OutOfMemoryError異常在哪些數據區域中可能會出現?分別說一下這個數據區域出現OOM的場景和緣由?

  • OutOfMemoryError異常在哪些數據區域中可能會出現?
    • Java堆溢出
    • 虛擬機棧和本地方法棧溢出
    • 方法區和運行時常量池溢出
  • 分別說一下這個數據區域出現OOM的場景和緣由?
    • Java堆溢出
      • 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創建的那個字符串實例是同一個

6.0.0.6 Java中堆和棧的區別?分別寫出堆內存溢出與棧內存溢出的程序?

  • 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++;
      }
      }

6.0.0.7 如果對象的引用被置爲null,垃圾收集器是否會立即釋放對象佔用的內存?

  • 如果對象的引用被置爲null,垃圾收集器是否會立即釋放對象佔用的內存?
    • 不會,在下一個垃圾回收週期中,這個對象將是可被回收的。
    • 也就是說當一個對象的引用變爲null時,並不會被垃圾收集器立刻回收,而是在下一次垃圾回收時纔會釋放其佔用的內存。

6.0.0.8 java中垃圾收集的方法有哪些?

  • java中垃圾收集的方法有哪些
    • 標記-清除:
      • 這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。
    • 複製算法:
      • 爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。
      • 於是將該算法進行了改進,內存區域不再是按照1:1去劃分,而是將內存劃分爲8:1:1三部分,較大那份內存交Eden區,其餘是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊內存區上,然後清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java堆又分爲新生代和老年代)
    • 標記-整理技術博客大總結
      • 該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
    • 分代收集
      • 現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。

6.0.1.1 如和判斷一個對象是否存活?引用計數法和可達性算法哪個更加好?如何理解一個對象不一定會被回收?

    1. 引用計數法
      • 所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.
      • 引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象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中的對象進行第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。

6.0.1.2 Class.forName() 和ClassLoader.loadClass()區別?

  • Class.forName() 和ClassLoader.loadClass()區別?
    • 問到的是反射,但是在底層涉及到了虛擬機的類加載知識。
    • Class.forName() 默認執行類加載過程中的連接與初始化動作,一旦執行初始化動作,靜態變量就會被初始化爲程序員設置的值,如果有靜態代碼塊,靜態代碼塊也會被執行
    • ClassLoader.loadClass() 默認只執行類加載過程中的加載動作,後面的動作都不會執行

其他介紹

01.關於博客彙總鏈接

02.關於我的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章