初學JVM

  • JAVA如何實現跨平臺?

Java實現跨平臺主要是通過JVM
JVM在不同的平臺有不同的版本,在不同的平臺要安裝不同的JVM版本,我們編寫JAVA源碼後,經過編譯會生成字節碼文件.class文件。
Java虛擬機JVM將.class文件翻譯成不同平臺對應的機器碼,從而在不同的平臺下運行
注意:編譯的結果不是生成機器碼,而是生成字節碼,字節碼不能直接運行,必須通過JVM翻譯成機器碼才能運行。不同平臺下編譯生成的字節碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。
所以,運行Java程序必須有JVM的支持,因爲編譯的結果不是機器碼,必須要經過JVM的再次翻譯才能執行。即使你將Java程序打包成可執行文件(例如
.exe),仍然需要JVM的支持。並且值得注意的是跨平臺的不是JVM,是JAVA程序,因爲JVM也是需要在不同的平臺下安裝不同版本的JVM

在這裏插入圖片描述

  • JVM組成:運行時數據區、類裝載子系統和字節碼執行引擎
    在這裏插入圖片描述
  • 下面我們通過一個程序實例來了解JVM:

在這裏插入圖片描述
在這裏插入圖片描述
棧(線程):每個線程都有自己的棧內存空間,放自己線程在運行過程中它的局部變量、操作數棧、動態鏈接和方法出口。
棧幀:一個方法對應一塊棧幀內存區域。

  • 下面我們來看一下compute()方法的字節碼:
    在這裏插入圖片描述
    0:iconst_1->將int類型常量1壓入操作數棧
    在這裏插入圖片描述
    1:istore_1->將int類型值存入局部變量1(也就是a)
    在這裏插入圖片描述
    在這裏插入圖片描述
    2:iconst_2->將int類型常量2壓入操作數棧
    3:istore_2->將int類型值存入局部變量2(也就是b)
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 每個線程運行時都會從一大塊程序計數器內存空間中分配一塊線程專屬的程序計數器。
    程序計數器:用來存放字節碼中當前程序運行代碼的位置(行號)。->在上述字節碼中,當程序執行行號爲4的行時,另一個線程把CPU時間片搶走,當前線程被掛起,另一個線程執行完畢,CPU繼續執行當前線程,下次再運行compute()方法時,從行號爲4的行開始執行,程序計數器就用來標識當前程序執行到的位置。
    在這裏插入圖片描述
    使用字節碼執行引擎來動態修改程序計數器標識的位置(行號)。
    在這裏插入圖片描述

4:iload_1->從局部變量1(a)中裝載int類型值
5:iload_2->從局部變量2(b)中裝載int類型值
在這裏插入圖片描述
在這裏插入圖片描述
6:iadd->執行int類型的加法(把2和1從操作數棧中彈出,執行加法再壓回操作數棧)
在這裏插入圖片描述
7:bipush 10->將一個8位帶符號整數壓入棧(把10壓入操作數棧)
在這裏插入圖片描述
9:imul->執行int類型的乘法
在這裏插入圖片描述
把10和3從操作數棧中彈出,執行乘法再壓回操作數棧在這裏插入圖片描述
10:istore_3->將int類型值存入局部變量3(也就是c)
在這裏插入圖片描述
11:iload_3->從局部變量3(c)中裝載int類型值

  • 操作數棧:在程序運行做運算的過程中,需要暫存(數據)的一個臨時內存空間,運算結束后里面是空的。
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 方法出口:調用compute()方法時,記錄其在main()方法中執行到哪一行,當調用完compute()方法後還能回到main()方法的對應位置上。
    在這裏插入圖片描述
    在這裏插入圖片描述
    這裏的math,是爲math分配的一塊內存空間,存儲的不是對象(對象存儲在堆裏),存放的是math對象在堆中的內存地址(字符串),內存地址就是一個指針,一個對象的引用。
    在這裏插入圖片描述
    堆和棧之間的關係:棧上有很多指針,指向堆的對象,是對堆的對象的引用。
    在這裏插入圖片描述

  • 方法區:方法區在JDK1.8之後用的是直接內存,嚴格意義上說,方法區是放在JAVA內存區域裏的,使用的內存空間是物理內存,不是用的JAVA虛擬機內部的內存。

在這裏插入圖片描述
方法區中主要存放常量(final修飾)、靜態變量和類信息(比如我們上面代碼的例子:Math.class(字節碼文件))。
通過Math.class(字節碼文件)通過類裝載子系統加載到方法區。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
方法區的內存空間存放的user是user對象在堆中內存空間的入口地址。
在這裏插入圖片描述

  • 本地方法: 一個Native Method就是一個Java調用非Java代碼的接口。方法的實現由非Java語言實現,比如C或C++。

在這裏插入圖片描述

  • 本地方法棧: 如果在程序運行中調用了本地方法,就會在本地方法棧內存空間裏給本地方法運行過程中分配一塊自己專屬的內存空間。
  • 堆:
    在這裏插入圖片描述
    新new出來的對象是存放在Eden區。

在這裏插入圖片描述
第一次Eden區滿了,再往裏面放對象,會觸發字節碼執行引擎開啓minor gc(垃圾回收)。這時會JVM會從棧和方法區內找到所有的GC Roots,從GC Roots出發找到所有它引用的對象。找到的對象都標記爲非垃圾對象,將這些對象複製到Survivor區s0中;其餘未標記的都是垃圾對象,直接回收。
GC Roots根節點:線程棧的本地變量、靜態變量、本地方法棧的變量等。
可達性分析算法:將GC Roots對象作爲起點,從這些結點開始向下搜索引用的對象,找到的對象都標記爲非垃圾對象,其餘未標記的都是垃圾對象。
在這裏插入圖片描述
第二次 Eden區滿了,JVM依然會從棧和方法區內找到所有的GC Roots,從GC Roots出發找到所有它引用的對象。找到的對象都標記爲非垃圾對象,將這些對象(這些對象有Eden區的和Survivor區s0的)複製到Survivor區s1中。
**gc分代年齡(**存放在對象頭(MarkWord)中,鎖的信息也是存放咋對象頭中):經歷的gc次數。
在這裏插入圖片描述
第三次 Eden區滿了,JVM依然會從棧和方法區內找到所有的GC Roots,從GC Roots出發找到所有它引用的對象。找到的對象都標記爲非垃圾對象,將這些對象(這些對象有Eden區的和Survivor區s1的)複製到Survivor區s0中。
在這裏插入圖片描述
對象會從Eden區到Survivor區,然後在Survivor區的s0和s1區中來回流轉,當分代年齡超過15時,會被JVM放到老年代。
**哪些對象可能被放入老年代?**靜態變量,Spring容器的bean(controller、service等),線程池的對象、緩存對象。
Java中什麼樣的對象能夠進入老年代?

1.大對象:所謂的大對象是指需要大量連續內存空間的java對象,最典型的大對象就是那種很長的字符串以及數組,大對象對虛擬機的內存分配就是壞消息,尤其是一些朝生夕滅的短命大對象,寫程序時應避免。
2.長期存活的對象:虛擬機給每個對象定義了一個對象年齡(Age)計數器,如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1,。對象在Survivor區中每熬過一次Minor GC,年齡就增加1,當他的年齡增加到一定程度(默認是15歲), 就將會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。
3.動態對象年齡判定:爲了能更好地適應不同程度的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

當老年代放滿,會觸發字節碼執行引擎開啓full gc,full gc會對整個堆的所有內存空間做整體回收。Minor gc只會回收年輕代。
在這裏插入圖片描述

但是full gc也只能去回收沒有被引用的對象,當老年代持續增多,就會發生內存溢出(OOM,Out Of Memery)。

JAVA虛擬機調優的目的: 減少full gc->減少stw時間。
stw(stop the world): 停止所有用戶線程(在full gc時會產生stw,minor gc的stw時間很短,可以忽略不計)。
在這裏插入圖片描述

  • 爲什麼full gc時要進行stw?

如果不進行stw,從GC Roots找對象,找完一條線之後,找另一個時,由於沒有stw,用戶線程繼續執行,可能gc還沒有結束,但是用戶線程已經結束了,用戶線程結束會釋放局部變量的內存空間,那指向一開始gc收集的指向堆對象的指針就沒有了,剛開始垃圾回收器收集標記爲非垃圾的對象,現在又變成垃圾了,但是gc還沒有結束,那gc又要檢查之前的變量,過程會變得非常複雜。Stw(停止用戶線程)讓gc收集過的對象狀態不再改變。