1. 程式人生 > >Day7-堆,棧,方法區和GC

Day7-堆,棧,方法區和GC

Tips

  • 只要類持有對外部實力物件的引用, 垃圾回收機制就不會回收該物件

JVM中

堆和棧對比

存什麼

  • 棧記憶體 儲存基本資料型別, 區域性變數方法呼叫和形參,棧分為java方法棧和native方法棧,

    方法棧主要記錄的是方法執行時的棧幀, 每執行一個方法就會新增一個棧幀 ,方法返回後, 棧被清空, 堆等待GC回收
    為單個函式分配的那部分棧空間叫做棧幀(StackFrame)
    正在使用的棧空間叫做呼叫棧(CallStack)
    在記憶體中,棧是從高地址向低地址延伸的,即棧底對應高地址,棧頂對應低地址。

    java執行緒是不是開兩個棧存放不同的棧幀看具體JDK, 比如Oracle JDK和OpenJDK就是一個呼叫棧存放兩種棧幀

  • 堆記憶體 儲存Java中的全部物件,this

int a[] = new int[4];
new int[] 存放在堆, int a[] 存放在棧

Double a[] = new Double[10000000];
   Double qq = 3.1d;
   for (int i = 0; i < a.length; i++) {
       a[i] = qq.doubleValue();
   }

a[i] = qq.doubleValue;
a[i] = Double.valueOf(qq);
a[i] = new Double(qq.doubleValue);
所以此double類的值存在堆

獨有/共享

  • 棧記憶體歸屬於執行緒, 每個執行緒都會有一個棧記憶體, 其儲存的變數只能在其所屬的執行緒中可見, 棧記憶體可以理解成執行緒的私有記憶體, 所以叫執行緒棧

  • 堆記憶體的物件, 對所有執行緒可見, 可以被所有執行緒訪問

異常

  • 棧沒有空間儲存方法呼叫和區域性變數, JVM會丟擲Java.lang.StackOverFlowError, 純java程式碼無法洩漏棧空間, 它完全被JVM掌控
  • 堆沒有空間儲存物件, JVM會丟擲java.lang.OutOfMemoryError

空間大小

  • 棧記憶體遠小於堆記憶體, 棧可通過jvm引數 -XSS設定, 預設隨著虛擬機器和作業系統改變

執行效率

  • 棧是存取效率靈活, 僅次於暫存器, 棧資料可以共享, 但棧中的資料大小和生命週期固定, 缺乏靈活性
  • 堆是自動分配記憶體大小, 生存期不用告訴編譯器, 等gc回收, 但是因為動態分配記憶體, 儲存效率會比較慢

方法區

  • 方法區存類資訊, 靜態方法, 常量, 即時編譯器編譯後的程式碼

GC(Garabage Collection)

指的是堆中資料的回收, 首先堆可以劃分為新生代和老年代


新生代繼續劃分為 Eden 和 Survivor Space(倖存區), Survivor Space 再被劃分成 From 和 To


新物件首先被建立在 Eden, (如果物件過大,如陣列,則直接放入老年代). 在 GC 中, Eden 會被移入Survivor Space. 直到物件熬過一定的Minor GC的次數, 會被移到老年代, 老年代用Major GC來清理

空間佔比:

  • 新生代 : 老年代 = 1:2
  • Eden : From : To = 8:1:1

分代收集

新生代使用Minor GC, 老年代使用Major GC
Minor GC 和 Major GC 統稱為 Full GC
所有的Minor GC 會觸發全世界暫停 STW(stop-the-world), 停止應用程式的執行緒, 當然對於大多數應用,停頓的延遲可以忽略不計, 真相是大部分Eden區中的物件都能被認為是垃圾,所以不會存放到Survivor Space.
現在很多的GC機制都會清理永久代(靜態方法區)

  • JVM並不強制要求GC實現哪種GC演算法

純java程式碼無法洩漏棧空間, 它完全被JVM掌控, 但如果有其他資源依附在java物件上, 如native memory(DirectByteBuffer), file(fileInputStream), 那麼當然自己關閉最合適

  • 雖然有finalizer, PhantomReference之類的讓程式設計師向GC註冊, 請求釋放資源,但是GC執行時間不確定(因為是一條單獨的執行緒), 還是自己釋放的好

可達性檢測

  • 引用計數: 一種在jdk1.2之前被使用的垃圾收集演算法,我們需要了解其思想。其主要思想就是維護一個counter,當counter為0的時候認為物件沒有引用,可以被回收。缺點是無法處理迴圈引用。目前iOS開發中的一個常見技術ARC(Automatic Reference Counting)也是採用類似的思路。在當前的JVM中應該是沒有被使用的。

  • 根搜演算法: gc root 根據引用關係來便利整個堆, 並標記, 這稱之為Mark, 之後回收掉違背Mark的物件, 解決了「孤島效應」, 這裡的gc root 指的是:

    • 虛擬機器棧中引用的物件(棧幀中的本地變量表)
    • 方法區中的類靜態屬性引用的物件
    • 方法區中的常用變數的物件
    • 本地方法棧中JNI 引用的物件

java減小GC開銷 from

  • 不要顯示呼叫System.gc()
    此函式只是建議JVM進行GC, 無法保證立馬執行
  • 減小臨時物件的使用
  • 物件不用時顯示置為null
  • 使用StringBuilder拼接字串
    String的擴增是新建物件, 多次 + 會多次建立新物件
  • 能用基本型別就不用物件
  • 少用靜態
  • 分散物件建立和刪除的時間

整理策略

  • 複製
    主要在新生代的回收上, 通過from 和 to 區的來回拷貝.對於新生成的物件, 頻繁的複製可以很快找到 那些不用的物件.
  • 標記清除和標記整理
    主要在老生代的回收上, 通過根搜的標記清除或者處理掉不用的物件.
    整理的過程


    清除的過程

清除會產生碎片,對記憶體的利用不是很好, 但是不代表整理比清除好, 畢竟整理慢, 比如CMSGC就是使用清除而不是整理的

  • 具體的垃圾收集器
    • 新生代收集器:有Serial收集器、ParNew收集器、Parallel Scavenge收集器
    • 老生代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器


思考一下複製和標記清除/整理的區別,為什麼新生代要用複製?因為對新生代來講,一次垃圾收集要回收掉絕大部分物件,我們通過冗餘空間的辦法來加速整理過程(不冗餘空間的整理操作要做swap,而冗餘只需要做move)。同時可以記錄下每個物件的『年齡』從而優化『晉升』操作使得中年物件不被錯誤放到老年代。而反過來老年代偏穩定,我們哪怕是用清除,也不會產生太多的碎片,並且整理的代價也並不會太大。

暫存器

在計算機領域,暫存器是CPU內部的元件,它是有限存貯容量的高速存貯部件,可用來暫存指令、資料和地址。
暫存器分為通用暫存器和特殊暫存器。通用暫存器有 ax/bx/cx/dx/di/si,在大多數指令中可以任意選用,但也有一些規定某些指令只能用某個特定的「通用」暫存器;特殊暫存器有 bp/sp/ip 等,特殊暫存器均有特定用途。

在 Stack Frame 中,涉及到三種重要的特殊暫存器:

  • bp ( base pointer ) 暫存器
  • sp ( stack poinger ) 暫存器
  • ip ( instruction pointer ) 暫存器

需要注意的是,不同架構的CPU,暫存器名稱會新增不同的字首來表示暫存器的大小。例如對於x86架構,字母「e」用作名稱字首,表示暫存器大小為32位;對於x86_64架構,字母「r」用作名稱字首,表示暫存器大小為64位。

舉例

  • 下圖是linux 中一個程序的虛擬記憶體分佈:
  • 圖中0號地址在最下邊,越往上記憶體地址越大。
    以32位地址作業系統為例,一個程序可擁有的虛擬記憶體地址範圍為0-2^32。分為兩部分,一部分留給kernel使用(kernel virtual memory),剩下的是程序本身使用, 即圖中的process virtual memory。
    普通Java 程式使用的就是process virtual memory.
    上圖中最頂端的一部分記憶體叫做user stack. 這就是題目問的 stack. 中間有個 runtime heap。就是題目中的heap. 他們的名字和資料結構裡的stack 和 heap 幾乎每啥關係。
    注意在上圖中,stack 是向下生長的; heap是向上生長的。
    當程式進行函式呼叫時,每個函式都在stack上有一個 call frame。
    比如對於以下程式,
public void foo(){
  //do something...
  println("haha"); // <<<=== 在這兒設定breakpoint 1
}

public void bar(){
  foo();
}

main(){
  bar();
  println("hahaha"); // <<<=== 在這兒設定 breakpoint 2
}

當程式執行到breakponit1時,user stack 裡會有三個frame
|
| main 函式的 frame-------------------
|
| bar 函式的 frame-------------------<<<=== %ebp
|
| foo 函式的 frame------------------- <<<===%esp
其中 esp 和 ebp 都是暫存器。 esp 指向stack 的頂(因為stack 向下生長,esp會向下走); ebp 指向當前frame的邊界。
當程式繼續執行到brekapoing 2的時候stack 大概是這樣的:
|
-------------------<<<=== %ebp
|
| main 函式的 frame------------------- <<<===%esp
也就是說當一個函式執行結束後,它對應的call frame就被銷燬了。(其實就是esp 和 ebp分別以東,但是記憶體地址中的資料只有在下一次寫的時候才被覆蓋。)
說了這麼多,終於該說什麼東西放在stack 上什麼東西放在heap 上了。
最直白的解釋:

public void foo(){
  int i = 0; // <= i 的值存在stack上,foo()的call frame 裡。
  Object obj = new Object(); // object 物件本身存在heap 裡, foo()的call frame 裡存該物件的地址。
}

資料結構中

棧是先進後出的結構