一個進程對應一個jvm實例,一個運行時數據區,又包含多個線程,這些線程共享了方法區和堆,每一個線程包含了程序計數器、本地方法棧和虛擬機棧。java
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
複製代碼
public class SimpleHeap {
private int id;//屬性、成員變量
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimpleHeap sl = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
int[] arr = new int[10];
Object[] arr1 = new Object[10];
}
}
複製代碼
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虛擬機中的堆內存總量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虛擬機試圖使用的最大堆內存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
System.out.println("系統內存大小爲:" + initialMemory * 64.0 / 1024 + "G");//系統內存大小爲:15.3125G
System.out.println("系統內存大小爲:" + maxMemory * 4.0 / 1024 + "G");//系統內存大小爲:14.22265625G
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
設置堆大小爲600m,打印出的結果爲575m,這是由於倖存者區S0和S1各佔據了25m,可是他們始終有一個是空的,存放對象的是伊甸園區和一個倖存者區
git
java.lang.OutOfMemoryError: Java heap space
代碼示例:github
/**
* -Xms600m -Xmx600m
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
複製代碼
存儲在JVM中的java對象能夠被劃分爲兩類:面試
Java堆區進一步細分能夠分爲年輕代(YoungGen)和老年代(OldGen)算法
其中年輕代能夠分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫frmo區,to區)
數組
配置新生代與老年代在堆結構的佔比緩存
在hotSpot中,Eden空間和另外兩個Survivor空間缺省所佔的比例是8:1:1(測試的時候是6:1:1),開發人員能夠經過選項 -XX:SurvivorRatio 調整空間比例,如-XX:SurvivorRatio=8安全
幾乎全部的Java對象都是在Eden區被new出來的bash
絕大部分的Java對象都銷燬在新生代了(IBM公司的專門研究代表,新生代80%的對象都是「朝生夕死」的)多線程
可使用選項-Xmn設置新生代最大內存大小(這個參數通常使用默認值就行了)
測試代碼
/**
* -Xms600m -Xmx600m
*
* -XX:NewRatio : 設置新生代與老年代的比例。默認值是2.
* -XX:SurvivorRatio :設置新生代中Eden區與Survivor區的比例。默認值是8
* -XX:-UseAdaptiveSizePolicy :關閉自適應的內存分配策略 '-'關閉,'+'打開 (暫時用不到)
* -Xmn:設置新生代的空間的大小。 (通常不設置)
*
*/
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("我只是來打個醬油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
爲新對象分配內存是件很是嚴謹和複雜的任務,JVM的設計者們不只須要考慮內存如何分配、在哪裏分配的問題,而且因爲內存分配算法與內存回收算法密切相關,因此還須要考慮GC執行完內存回收後是否會在內存空間中產生內存碎片。
總結
==針對倖存者s0,s1區:複製以後有交換,誰空誰是to==
==關於垃圾回收:頻繁在新生區收集,不多在養老區收集,幾乎再也不永久區/元空間收集。==
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
複製代碼
對應堆空間分配過程
JVM在進行GC時,並不是每次都針對上面三個內存區域(新生代、老年代、方法區)一塊兒回收的,大部分時候回收都是指新生代。
針對hotSpot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
Young GC ->Full GC -> OOM
/** 測試GC分代回收
* 測試MinorGC 、 MajorGC、FullGC
* -Xms9m -Xmx9m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "testGC";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍歷次數爲:" + i);
}
}
}
複製代碼
日誌輸出
爲何要把Java堆分代?不分代就不能正常工做了麼
分配60m堆空間,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer對象20m,Eden 區沒法存放buffer, 直接晉升老年代
/** 測試:大對象直接進入老年代
* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
public class YoungOldAreaTest {
// 新生代 20m ,Eden 16m, s0 2m, s1 2m
// 老年代 40m
public static void main(String[] args) {
//Eden 區沒法存放buffer 晉升老年代
byte[] buffer = new byte[1024 * 1024 * 20];//20m
}
}
複製代碼
日誌輸出
爲何有TLAB(Thread Local Allocation Buffer)
什麼是TLAB
說明
/**
* 測試-XX:UseTLAB參數是否開啓的狀況:默認狀況是開啓的
*/
public class TLABArgsTest {
public static void main(String[] args) {
System.out.println("我只是來打個醬油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
在發生Minor Gc以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間。
在JDK6 Update24以後(JDK7),HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK6 Update24以後的規則變爲==只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。==
在《深刻理解Java虛擬機》中關於Java堆內存有這樣一段描述:隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,==棧上分配、標量替換優化技術==將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼「絕對」了。
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個廣泛的常識。可是,有一種特殊狀況,那就是若是==通過逃逸分析(Escape Analysis)後發現,一個對象並無逃逸出方法的話,那麼就可能被優化成棧上分配==。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最多見的堆外存儲技術。
此外,前面提到的基於OpenJDK深度定製的TaoBaoVM,其中創新的GCIH(GCinvisible heap)技術實現off-heap,將生命週期較長的Java對象從heap中移至heap外,而且GC不能管理GCIH內部的Java對象,以此達到下降GC的回收頻率和提高GC的回收效率的目的。
public void method(){
V v = new V();
//use V
//......
v = null;
}
複製代碼
沒有發生逃逸的對象,則能夠分配到棧上,隨着方法執行的結束,棧空間就被移除。
public static StringBuffer createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
複製代碼
因爲上述方法返回的sb在方法外被使用,發生了逃逸,上述代碼若是想要StringBuffer sb不逃出方法,能夠這樣寫:
public static String createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
複製代碼
/**
* 逃逸分析
*
* 如何快速的判斷是否發生了逃逸分析,就看new的對象實體是否有可能在方法外被調用。
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis對象,發生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
爲成員屬性賦值,發生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:若是當前的obj引用聲明爲static的?仍然會發生逃逸。
/*
對象的做用域僅在當前方法中有效,沒有發生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成員變量的值,發生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()一樣會發生逃逸
}
}
複製代碼
結論
開發中能使用局部變量的,就不要使用在方法外定義
使用逃逸分析,編譯器能夠對代碼作以下優化:
代碼分析
如下代碼,關閉逃逸分析(-XX:-DoEscapeAnalysi),維護10000000個對象,若是開啓逃逸分析,只維護少許對象(JDK7 逃逸分析默認開啓)
/**
* 棧上分配測試
* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看執行時間
long end = System.currentTimeMillis();
System.out.println("花費的時間爲: " + (end - start) + " ms");
// 爲了方便查看堆內存中對象個數,線程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未發生逃逸
}
static class User {
}
}
複製代碼
/**
* 同步省略說明
*/
public class SynchronizedTest {
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
//代碼中對hollis這個對象進行加鎖,可是hollis對象的生命週期只在f()方法中
//並不會被其餘線程所訪問控制,因此在JIT編譯階段就會被優化掉。
//優化爲 ↓
public void f2() {
Object hollis = new Object();
System.out.println(hollis);
}
}
複製代碼
public class ScalarTest {
public static void main(String[] args) {
alloc();
}
public static void alloc(){
Point point = new Point(1,2);
}
}
class Point{
private int x;
private int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
}
複製代碼
以上代碼,通過標量替換後,就會變成
public static void alloc(){
int x = 1;
int y = 2;
}
複製代碼
能夠看到,Point這個聚合量通過逃逸分析後,發現他並無逃逸,就被替換成兩個標量了。那麼標量替換有什麼好處呢?就是能夠大大減小堆內存的佔用。由於一旦不須要建立對象了,那麼就再也不須要分配堆內存了。
標量替換爲棧上分配提供了很好的基礎。
測試代碼
/**
* 標量替換測試
* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class ScalarReplace {
public static class User {
public int id;//標量(沒法再分解成更小的數據)
public String name;//聚合量(String還能夠分解爲char數組)
}
public static void alloc() {
User u = new User();//未發生逃逸
u.id = 5;
u.name = "www.atguigu.com";
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花費的時間爲: " + (end - start) + " ms");
}
}
複製代碼
【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器