一. 堆和GC介紹
1.java堆的特點
《深入理解java虛擬機》是怎么描述java堆的
- Java堆(Java Heap)是java虛擬機所管理的內存中最大的一塊
- java堆被所有線程共享的一塊內存區域
- 虛擬機啟動時創建java堆
- java堆的唯一目的就是存放對象實例。
- java堆是垃圾收集器管理的主要區域。
- 從內存回收的角度來看, 由於現在收集器基本都采用分代收集算法, 所以Java堆可以細分為:新生代(Young)和老年代(Old)。 新生代又被划分為三個區域Eden、From Survivor, To Survivor等。無論怎么划分,最終存儲的都是實例對象, 進一步划分的目的是為了更好的回收內存, 或者更快的分配內存。
- java堆的大小是可擴展的, 通過-Xmx和-Xms控制。
- 如果堆內存不夠分配實例對象, 並且堆也無法在擴展時, 將會拋出outOfMemoryError異常。

2.堆內存划分:
- 堆大小 = 新生代 + 老年代。堆的大小可通過參數–Xms(堆的初始容量)、-Xmx(堆的最大容量) 來指定。
- 其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to,以示區分。默認的,Edem : from : to = 8 : 1 : 1 。(可以通過參數 –XX:SurvivorRatio 來設定 。
- 即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
- JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為存儲對象,所以無論什么時候,總是有一塊 Survivor 區域是空閑着的。
- 新生代實際可用的內存空間為 9/10 ( 即90% )的新生代空間。
3.堆的垃圾回收方式
java堆是GC垃圾回收的主要區域。 GC分為兩種: Minor GC、Full GC(也叫做Major GC).
1. Minor GC(簡稱GC)
Minor GC是發生在新生代中的垃圾收集動作, 所采用的是復制算法。
GC一般為堆空間某個區發生了垃圾回收,
新生代(Young)幾乎是所有java對象出生的地方。即java對象申請的內存以及存放都是在這個地方。java中的大部分對象通常不會長久的存活, 具有朝生夕死的特點。
當一個對象被判定為“死亡”的時候, GC就有責任來回收掉這部分對象的內存空間。
新生代是收集垃圾的頻繁區域。
回收過程如下:
當對象在 Eden ( 包括一個 Survivor 區域,這里假設是 from 區域 ) 出生后,在經過一次 Minor GC 后,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納(上面已經假設為 from 區域,這里應為 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用復制算法將這些仍然還存活的對象復制到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置為1,以后對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。
但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。
2.Full GC
Full GC 基本都是整個堆空間及持久代發生了垃圾回收,所采用的是標記-清除算法。
現實的生活中,老年代的人通常會比新生代的人 “早死”。堆內存中的老年代(Old)不同於這個,老年代里面的對象幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那么容易就 “死掉” 了的。因此,Full GC 發生的次數不會有 Minor GC 那么頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長,一般是Minor GC的 10倍以上。
另外,標記-清除算法收集垃圾的時候會產生許多的內存碎片 ( 即不連續的內存空間 ),此后需要為較大的對象分配內存空間時,若無法找到足夠的連續的內存空間,就會提前觸發一次 GC 的收集動作
擴展: Minor GC是如何觸發的, 又是如何工作的? 如下圖:
Minor GC是由字節碼執行引擎觸發的. 當我們的程序中需要new一個對象的時候, 就會將這個對象放入到Eden區域, 當Eden區域中的對象越來越多, 直到滿了, 這時放不下了, 就會觸發字節碼執行引擎發起GC操作. 第一次發起的GC, 將會看看哪些對象還活着, 哪些對象已經不用了, 活着的對象放入survivor中的一個區, 不再被引用的對象, 直接被回收了
如何判斷對象是否還活着呢?
字節碼執行引擎會去找很多gc root.
什么是gc root呢?
GC Root是一個對象, 以這個對象作為啟動點,從這些節點開始向下搜索引用的對象, 找到的對象都標記為非垃圾對象, 其余未標記的對象都是垃圾對象.
GC Root根節點有哪些?
線程棧的局部變量, 方法區中的靜態變量, 本地方法棧的變量等等。
垃圾收集的原理
在Math中, 我們看棧中main方法的局部變量表中的math變量. 方法區中的user變量. 他們都是GC Root根對象. 他們指向的是一塊堆內存空間.
實質是, GC垃圾回收的過程, 就是尋找GC Root的過程. 從棧中找局部變量, 從方法區中找靜態變量. 從GC Root出發, 找到所有的引用變量. 這些變量可能會引用其他的變量, 變量還會再引用其他變量. 直到不再引用其他變量為止, 以上這些都是非垃圾對象. 如果一個對象沒有被任何對象引用, 那它就是垃圾對象。
垃圾對象最后就被回收, 非垃圾對象進入到Survivor的一個區域里面. 每次進入sruvivor區域,對象的分代年齡都會+1, 分代年齡保存在哪里呢?保存在對象頭里面.
程序還在繼續運行, 又會產生新的對象放入到Eden區, 當Eden區又被放滿了, 就會再次出發GC, 此時會尋找Eden+sruvivor(一個區域)中的GC Root, 將其標記,
沒有被引用的對象被回收, 其他被引用的對象會保存到另一個survivor區域. 分代年齡+1
這樣運行, 直到分代年齡為15(默認15,可設置)時, 也就是GC發生了15次還活着的對象, 就會被放到老年代.
通常什么樣的對象會被放到老年代呢?
靜態變量引用的對象, 靜態常量. 比如說: 對象池, 緩存對象, spring容器里面的對象,
二. 使用工具查看GC流轉的過程
我們使用的工具是jvisualvm工具, 這是jdk自帶的一個工具。這個工具通常是在開發環境使用,因為其本身比較耗性能,所以線上一般不用。本地調試可以使用。
先來准備一段代碼, 一段很簡單的代碼, 不停的去產生新的對象
package com.lxl.jvm;
import java.util.ArrayList;
import java.util.List;
public class HeapTest {
public static void main(String[] args) throws InterruptedException {
List<User> userList = new ArrayList<>();
while (true) {
userList.add(new User());
Thread.sleep(10);
}
}
}
我們來按照上面的邏輯分析代碼
-
userList: 是放在棧中的局部變量表中的一個變量
new ArrayList<>(): 是放在堆中的一個對象 -
new User(): 在堆中構建一個新的User對象, 並分配了一個地址,並將這個地址添加到new ArrayList()中.
這里面 userList是根對象, new User()最終會被newArrayList()引用, 而userList又引用new ArrayList(); 所以, 他們都不會是垃圾, 因此都不會被回收.

那么死循環不停的構造對象, 添加引用. Eden區遲早會放滿, 放滿了就會觸發GC, 那么GC能把他們回收呢? 回收不了, 因為都在被GC Root直接或間接引用. 最終都會被放入老年代. 然后還在持續構造新的對象,最終會怎么樣?最終會內存溢出. 我們來看看可視化效果。
首先, 我們啟動程序, 然后在控制台啟動jvisualvm

我們來看的是HeapTest, 這里面有很多性能指標可以查看. 我們重點看visual GC. 如果沒有visual GC 可以參考這篇文章: https://xiaojin21cen.blog.csdn.net/article/details/106612383
從這個圖上,我們可以看到每過一段您時間, 觸發一次GC, 因為不能被回收, 因此會轉移到另一個survivor區域. 經過15次回收, 還沒有收走, 那么就進入到old老年區.
老年區的對象越來越多, 當老年代對象滿了以后, 會觸發full GC, full GC回收的是整個堆以及方法區的內容. 實際上老年代沒有能夠回收的對象, 這時候在往老年代放, 就會發生OOM
使用這個工具還可以分析我們自己的程序代碼的垃圾回收清空
三. Stop The World
在發生GC的時候, 會發生STW, Stop the world.
1. 什么是Stop The World呢 ?
舉個例子:在一個電商網站,用戶正在下單,這是由於內存滿了,觸發GC,這時候整個線程就會處於停滯狀態。用戶的感受就是一直在loading。。。。直到GC完畢,應用線程恢復工作。所以,Stop The World對我們的用戶是有一定影響的。JVM調優主要的目的就是減少Full GC的次數和時間。minor GC也會stop the world,但是他的時間很短,所以我們重點調優還是在full gc
2. 那么為什么一定要stop the world呢? 不STW不可以呢?
回答這個問題, 我們可以使用假設法, 假設沒有stop the world 會怎么樣?
我們知道, 在垃圾回收之前, 要先找到GC Root, 然后標記是否被引用, 最終沒有被引用的對象就是我們要回收的垃圾. 那就是沒有對象引用他了.通常會回收這塊內存空間地址 這個時候, 如果主線程也在運行, 剛好有一個變量存放在這個內存地址了, 而你並行的觸發了GC, 這時候程序就發生混亂了.
這是一種情況,另一種是在觸發GC的過程中,一部分變量正在被標記,而GC已經開始了,標記完以后,發現了垃圾,結果由於GC已經掃描完這里了,到這這一塊垃圾沒有被清理掉,要等待下一次垃圾回收來清理。