另外,小編也有根據以下總結內容,錄制了對JVM講解視頻。里面有個用visio畫的JVM內部結構圖,每部分的作用都有詳細講解,希望能有幫助。網址:https://edu.csdn.net/lecturer/board/10494
轉自:https://blog.csdn.net/csdnliuxin123524/article/details/81303711/?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-3&spm=1001.2101.3001.4242
1.java自動管理堆(heap)和(棧),程序員不能直接的設置堆和棧。
3.操作系統的堆和棧:
堆(操作系統):一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收,分配方式類似於鏈表。
棧(操作系統):由操作系統自動分配釋放,存放函數的參數值,局部變量值等。操作方式與數據結構中的棧相類似。
2.為什么jvm的內存是分布在操作系統的堆中呢??因為操作系統的棧是操作系統管理的,它隨時會被回收,所以如果jvm放在棧中,那java的一個null對象就很難確定會被誰回收了,那gc的存在就一點意義都莫有了,而要對棧做到自動釋放也是jvm需要考慮的,所以放在堆中就最合適不過了。
4,
上圖表明:jvm虛擬機位於操作系統的堆中,並且,程序員寫好的類加載到虛擬機執行的過程是:當一個classLoder啟動的時候,classLoader的生存地點在jvm中的堆,然后它會去主機硬盤上將A.class裝載到jvm的方法區,方法區中的這個字節文件會被虛擬機拿來new A字節碼(),然后在堆內存生成了一個A字節碼的對象,然后A字節碼這個內存文件有兩個引用一個指向A的class對象,一個指向加載自己的classLoader,
5,java虛擬機的生命周期:聲明周期起點是當一個java應用main函數啟動時虛擬機也同時被啟動,而只有當在虛擬機實例中的所有非守護進程都結束時,java虛擬機實例才結束生命。
6,java虛擬機與main方法的關系:main函數就是一個java應用的入口,main函數被執行時,java虛擬機就啟動了。啟動了幾個main函數就啟動了幾個java應用,同時也啟動了幾個java的虛擬機。
7,java的虛擬機種有兩種線程,一種叫叫守護線程,一種叫非守護線程(也叫普通線程),main函數就是個非守護線程,虛擬機的gc就是一個守護線程。java的虛擬機中,只要有任何非守護線程還沒有結束,java虛擬機的實例都不會退出,所以即使main函數這個非守護線程退出,但是由於在main函數中啟動的匿名線程也是非守護線程,它還沒有結束,所以jvm沒辦法退出
8,虛擬機的gc(垃圾回收機制)就是一個典型的守護線程。
9,實例理解“當所有的非守護線程全部解說,jvm聲明周期才結束”:
-
public class MianAndThread{
-
public static void main( String args[]){
-
new Thread(new Runnable(){
-
@override
-
public void run(){
-
Thread.currendThread.sleep( 5000s);
-
System. out.println("睡了5s后打印,這是出main之外的非守護線程,這個推出后這個引用結束,jvm聲明周期結束。任務管理的java/javaw.exe進程結束"
-
}
-
}
-
System. out.println("mian線程直接打印,mian線程結束,電腦任務管理器的java/javaw.exe進程並沒有結束。")
-
}
-
}
10,GC垃圾回收機制不是創建的變量為空是就被立刻回收,而是超出變量的作用域后就被自動回收。
11,程序在jvm原先的流程:
首先,當一個程序啟動之前,它的class會被類裝載器裝入方法區(不好聽,其實這個區我喜歡叫做Permanent區),執行引擎讀取方法區的字節碼自適應解析,邊解析就邊運行(其中一種方式),然后pc寄存器指向了main函數所在位置,虛擬機開始為main函數在java棧中預留一個棧幀(每個方法都對應一個棧幀),然后開始跑main函數,main函數里的代碼被執行引擎映射成本地操作系統里相應的實現,然后調用本地方法接口,本地方法運行的時候,操縱系統會為本地方法分配本地方法棧,用來儲存一些臨時變量,然后運行本地方法,調用操作系統APIi等等。
12,根據Java虛擬機規范的規定,如果方法區的內存空間不能滿足內存分配需要時,將拋出OutOfMemoryError異常。
13,jvm的結構圖:
方便理解可把上圖分為“功能區”和"數據區”(好好理解功能和數據的含義(一動一靜)):參考下面13.1,功能區:垃圾回收系統、類加載器、執行引擎;數據區:也就是整個運行時數據區;
13.1 jvm內部執行運行流程圖:
14,jvm結構圖各模塊的生命周期總結:
對13中的結構圖,做一下統計,啟動一個jvm虛擬機程序就是啟動了一個進程。啟動的同時就在操作系統的堆內存中開辟一塊jvm內存區,對於13圖中各個小模塊的聲明周期:
虛擬機棧、本地方法棧、程序計數器這三個模塊是線程私有的,有多少線程就有多少個這三個模塊,聲明周期跟所屬線程的聲明周期一致。以程序計數器為例,因為多線程是通過線程輪流切換和分配執行時間來實現,所以當線程切回到正確執行位置,每個線程都有獨立的程序技術器,各個線程之間的計數器互不影響,獨立存儲。
其余是跟JVM虛擬機的生命周期一致。
15,13圖中,程序計數器模塊是JVM內存區域唯一不會報outofMemoryError情況的區域。
16,結合13圖,我們總結出JVM內存包含兩個子系統和兩個組件,兩個子系統是:Classloader子系統和Executionengine(執行引擎)子系統;兩個組件分別是:Runtimedataarea(運行時數據區域)組件和Nativeinterface(本地庫接口)組件。
從圖中可以看出運行時數據區域包含5部分:方法區,堆,虛擬機棧,本地方法棧,程序計數器
17,什么是本地庫接口和本地方法庫:(1)本地方法庫接口:即操作系統所使用的編程語言的方法集,是歸屬於操作系統的。(2)本地方法庫保存在動態鏈接庫中,即.dll(windows系統)文件中,格式是各個平台專有的。(3)個人感覺上圖的本地庫接口有點多余,以下面代碼為例:
計算兩個int的和(傳入int參數並返回int類型)
-
class Calc
-
{
-
static{
-
System.loadLibrary( "Calc");
-
}
-
-
public static native int add(int a, int b);
-
-
public static void main(String[] args)
-
{
-
System. out.println(add(11,23));
-
}
-
}
對應的C代碼:
-
-
-
-
/* jint 對應着java 的int類型 */
-
JNIEXPORT jint JNICALL Java_Calc_add(JNIEnv *env, jclass jc, jint a, jint b)
-
{
-
jint ret = a + b;
-
return ret;
-
}
在java代碼中會通過System.loadLibrary("")加載c語言庫(本地方法庫)直接與操作系統平台交互。
18,雙親委派機制:JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
見下圖:
例如:當jvm要加載Test.class的時候,
(1)首先會到自定義加載器中查找(其實是看運行時數據區的方法區有沒有加載),看是否已經加載過,如果已經加載過,則返回字節碼。
(2)如果自定義加載器沒有加載過,則詢問上一層加載器(即AppClassLoader)是否已經加載過Test.class。
(3)如果沒有加載過,則詢問上一層加載器(ExtClassLoader)是否已經加載過。
(4)如果沒有加載過,則繼續詢問上一層加載(BoopStrap ClassLoader)是否已經加載過。
(5)如果BoopStrap ClassLoader依然沒有加載過,則到自己指定類加載路徑下("sun.boot.class.path")查看是否有Test.class字節碼,有則返回,沒有通
知下一層加載器ExtClassLoader到自己指定的類加載路徑下(java.ext.dirs)查看。
(6)依次類推,最后到自定義類加載器指定的路徑還沒有找到Test.class字節碼,則拋出異常ClassNotFoundException。
代碼如下:
-
protected Class<?> loadClass(String name, boolean resolve)
-
throws ClassNotFoundException
-
{
-
synchronized (getClassLoadingLock(name)) {
-
// 首先,檢查是否已經加載過
-
Class<?> c = findLoadedClass(name);
-
if (c == null) {
-
long t0 = System.nanoTime();
-
try {
-
if (parent != null) {
-
//父加載器不為空,調用父加載器的loadClass
-
c = parent.loadClass(name, false);
-
} else {
-
//父加載器為空則,調用Bootstrap Classloader
-
c = findBootstrapClassOrNull(name);
-
}
-
} catch (ClassNotFoundException e) {
-
// ClassNotFoundException thrown if class not found
-
// from the non-null parent class loader
-
}
-
-
if (c == null) {
-
// If still not found, then invoke findClass in order
-
// to find the class.
-
long t1 = System.nanoTime();
-
//父加載器沒有找到,則調用findclass
-
c = findClass(name);
-
-
// this is the defining class loader; record the stats
-
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
-
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
-
sun.misc.PerfCounter.getFindClasses().increment();
-
}
-
}
-
if (resolve) {
-
//調用resolveClass()
-
resolveClass(c);
-
}
-
return c;
-
}
-
}
為什么要使用這種加載方式呢?這里要注意幾點,1,類加載器代碼本身也是java類,因此類加載器本身也是要被加載的,因此顯然必須有第一個類加載器不是Java類,這就是bootStrap,是使用c++寫的其他這是java了。2,雖說bootStrap、extclassLoader、appclassloader三個是父子類加載器關系,但是並沒有使用繼承,而是使用了組合關系。3,優點,具備了一種帶優先級的層次關系,越是基礎的類,越是被上層的類加載器進行加載,可以比較籠統的說像jdk自帶的幾個jar包肯定是位於最頂級的,再就是我們引用的包,最后是我們自己寫的,保證了java程序的穩定性。
19,jdk,jre,JVM的關系:JDK(Java Development Kit) 是 Java 語言的軟件開發工具包(SDK)。在JDK的安裝目錄下有一個jre目錄,里面有兩個文件夾bin和lib,在這里可以認為bin里的就是jvm,lib中則是jvm工作所需要的類庫,而jvm和 lib合起來就稱為jre。
jdk,jre,JVM的關系圖:
20,JVM運行簡易過程:
上圖左半部分其實不是在JVM中,程序員在eclipse上寫的是.java文件,經過編譯成.class文件(比如maven工程需要maven install,打成jar報,jar包里面都是.calss文件);這些步驟都是在eclipse上進行的。然后類加載器(classloader)一直到解釋器是屬於JVM的
21,解釋13中JVM結構圖各模塊的內容:
程序計數器(Program Counter Register):也叫PC寄存器,是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令、分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。,(1),區別於計算機硬件的pc寄存器,兩者不略有不同。計算機用pc寄存器來存放“偽指令”或地址,而相對於虛擬機,pc寄存器它表現為一塊內存(一個字長,虛擬機要求字長最小為32位),虛擬機的pc寄存器的功能也是存放偽指令,更確切的說存放的是將要執行指令的地址。(2)當虛擬機正在執行的方法是一個本地(native)方法的時候,jvm的pc寄存器存儲的值是undefined。(3)程序計數器是線程私有的,它的生命周期與線程相同,每個線程都有一個。(4)此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
Java虛擬機棧(Java Virtual Machine Stack):(1)線程私有的,它的生命周期與線程相同,每個線程都有一個。(2)每個線程創建的同時會創建一個JVM棧,JVM棧中每個棧幀存放的為當前線程中局部基本類型的變量(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double;和reference (32 位以內的數據類型,具體根據JVM位數(64為還是32位)有關,因為一個solt(槽)占用32位的內存空間 )、部分的返回結果,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址;(3)每一個方法從被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。(5)棧運行原理:棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,A方法又調用了B方法,於是產生棧幀F2也被壓入棧,B方法又調用了C方法,於是產生棧幀F3也被壓入棧…… 依次執行完畢后,先彈出后進......F3棧幀,再彈出F2棧幀,再彈出F1棧幀。(6)JAVA虛擬機棧的最小單位可以理解為一個個棧幀,一個方法對應一個棧幀,一個棧幀可以執行很多指令,如下圖:
(7)對上圖中的動態鏈接解釋下,比如當出現main方法需要調用method1()方法的時候,操作指令就會觸動這個動態鏈接就會找打方法區中對於的method1(),然后把method1()方法壓入虛擬機棧中,執行method1棧幀的指令;此外如果指令表示的代碼是個常量,這也是個動態鏈接,也會到方法區中的運行時常量池找到類加載時就專門存放變量的運行時常量池的數據。
本地方法棧(Native Method Stack):(1)先解釋什么是本地方法:jvm中的本地方法是指方法的修飾符是帶有native的但是方法體不是用java代碼寫的一類方法,這類方法存在的意義當然是填補java代碼不方便實現的缺陷而提出的。案例介紹將在 下面22知識點仔細介紹。(2)作用同java虛擬機棧類似,區別是:虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則是為虛擬機使用到的Native方法服務。(3)是線程私有的,它的生命周期與線程相同,每個線程都有一個。
Java 堆(Java Heap):(1)是Java虛擬機所管理的內存中最大的一塊。(2)不同於上面3個,堆是jvm所有線程共享的。(3)在虛擬機啟動的時候創建。(4)唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都要在這里分配內存。(5)
Java堆是垃圾收集器管理的主要區域。(6)因此很多時候java堆也被稱為“GC堆”(Garbage Collected Heap)。從內存回收的角度來看,由於現在收集器基本都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代;新生代又可以分為:Eden 空間、From Survivor空間、To Survivor空間。(23知識點詳細介紹)(7)java堆是計算機物理存儲上不連續的、邏輯上是連續的,也是大小可調節的(通過-Xms和-Xmx控制)。(8)如果在堆中沒有內存完成實例的分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
方法區(Method Area):(1)在虛擬機啟動的時候創建。(2)所有jvm線程共享。(3)除了和堆一樣不需要不連續的內存空間和可以固定大小或者可擴展外,還可以選擇不實現垃圾收集。(5)用於存放已被虛擬機加載的類信息、常量、靜態變量、以及編譯后的方法實現的二進制形式的機器指令集等數據。(4)被裝載的class的信息存儲在Methodarea的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,然后讀入這個class文件內容並把它傳輸到虛擬機中。(6)運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
方法區補充:指令集是個非常重要概念,因為程序員寫的代碼其實在jvm虛擬機中是被轉成了一條條指令集執行的,看下圖
首先看看上面各部位位於13圖中的那些位置:左側的foo代碼是指令集,可見就是在方法區,程序計數器就不用說了,局部變量區位於虛擬機棧中,右側最下方的求值棧(也就是操作數棧)我們從動圖中明顯可以看出存在棧頂這個關鍵詞因此也是位於java虛擬機棧的。
另外,圖中,指令是Java代碼經過javac編譯后得到的JVM指令,PC寄存器指向下一條該執行的指令地址,局部變量區存儲函數運行中產生的局部變量,棧存儲計算的中間結果和最后結果。
上圖的執行的源代碼是:
-
public class Demo {
-
-
public static void foo() {
-
-
int a = 1;
-
-
int b = 2;
-
-
int c = (a + b) * 5;
-
-
}
-
-
}
下面簡單解釋下執行過程,注意:偏移量的數字只是簡單代表第幾個指令哦,首先常數1入棧,棧頂元素就是1,然后棧頂元素移入局部變量區存儲,常數2入棧,棧頂元素變為2,然后棧頂元素移入局部變量區存儲;接着1,2依次再次入棧,彈出棧頂兩個元素相加后結果入棧,將5入棧,棧頂兩個元素彈出並相乘后結果入棧,然后棧頂變為15,最后移入局部變量。執行return命令如果當前線程對應的棧中沒有了棧幀,這個Java棧也將會被JVM撤銷。
類加載器子系統(class loader subsystem):(1)根據給定的全限定名類名(如java.lang.Object)來裝載class文件的內容到Runtimedataarea中的methodarea(方法區域)。Java程序員可以extends java.lang.ClassLoader類來寫自己的Classloader。(2) 對(1)中的加載過程是:當一個classloader啟動時,classloader的生存地點在jvm中的堆,然后它去主機硬盤上去裝載A.class到jvm的methodarea(方法區),方法區中的這個字節文件會被虛擬機拿來new A字節碼,然后在堆內存生成了一個A字節碼的對象,然后A自己碼這個內存文件有兩個引用,一個指向A的class對象,一個指向加載自己的classloader。見下圖:
執行引擎(Executionengine子系統):(1)負責執行來自類加載器子系統(class loader subsystem)中被加載類中在方法區包含的指令集,通俗講就是類加載器子系統把代碼邏輯(什么時候該if,什么時候該相加,相減)都以指令的形式加載到了方法區,執行引擎就負責執行這些指令就行了。
用網上最流行的一張圖表示就是:
(1)程序在JVM主要執行的過程是執行引擎與運行時數據區不斷交互的過程,可理解為上面“方法區中的動圖” (2)但是執行引擎拿到的方法區中的指令還是人能夠看懂的,這里執行引擎的工作就是要把指令轉成JVM執行的語言(也可以理解成操作系統的語言),最后操作系統語言再轉成計算機機器碼。
(3)
- 解釋器:一條一條地讀取,解釋並且執行字節碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋字節碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。字節碼這種“語言”基本來說是解釋執行的。
- 即時(Just-In-Time)編譯器:即時編譯器被引入用來彌補解釋器的缺點。執行引擎首先按照解釋執行的方式來執行,然后在合適的時候,即時編譯器把整段字節碼編譯成本地代碼。然后,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過本地代碼去執行它。執行本地代碼比一條一條進行解釋執行的速度快很多。編譯后的代碼可以執行的很快,因為本地代碼是保存在緩存里的。
上面也是才能夠別處拷來的,是對上圖的解釋,這里的字節碼解釋器也就對應20中的解釋器。簡單理解jit就是當代碼中某些方法復用次數比較高的,並超過一個特定的值就成為了“熱點代碼”。那么這個這些熱點代碼就會被編譯成本地代碼(其實可以理解成緩存)加快訪問速度。
22,本地(native)方法講解:
(1)本地方法就是帶有native標識符修飾的方法;(2)native修飾符修飾的方法並不提供方法體,但因為其實現體是由非java代碼在在外部實現的,因此不能與abstract連用;(3)存在的意義:不方便用java語言寫的代碼,使用更為專業的語言寫更合適;甚至有些JVM的實現就是用c編寫的,所以只能使用c來寫,
(4)更多的本地方法最好是與jdk的執行引擎的解釋器語言一致(執行引擎、解釋器:參考21的執行引擎);
(5)Windows、Linux、UNIX、Dos操作系統的核心代碼大部分是使用C和C++編寫,底層接口用匯編編寫.
(6)為什么native方法修飾的修飾的方法PC程序計數器為undefined。讀懂上面的所有知識點可以就很容易自己理解了。在一開始類加載時,native修飾的方法就被保存在了本地方法棧中,當需要調用native方法時,調用的是一個指向本地方法棧中某方法的地址,然后執行方法直接與操作系統交互,返回運行結果。整個過程並沒有經過執行引擎的解釋器把字節碼解釋成操作系統語言,PC計數器也就沒有起作用。
23,GC垃圾回收機制:
了解堆內存:
類加載器讀取了類文件后,需要把類、方法、常變量放到堆內存中,以方便執行器執行,堆內存分為三部分:
① 新生區
新生區是類的誕生、成長、消亡的區域,一個類在這里產生,應用,最后被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和幸存者區(Survivor pace),所有的類都是在伊甸區被new出來的。幸存區有兩個:0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園進行垃圾回收(Minor GC),將伊甸園中的剩余對象移動到幸存0區。若幸存0區也滿了,再對該區進行垃圾回收,然后移動到1區。那如果1去也滿了呢?再移動到養老區。若養老區也滿了,那么這個時候將產生Major GC(FullGCC),進行養老區的內存清理。若養老區執行Full GC 之后發現依然無法進行對象的保存,就會產生OOM異常“OutOfMemoryError”。
如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。原因有二:
a.Java虛擬機的堆內存設置不夠,可以通過參數-Xms、-Xmx來調整。
b.代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)。
② 養老區
養老區用於保存從新生區篩選出來的 JAVA 對象,一般池對象都在這個區域活躍。
③ 永久區
永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所占用的內存。
如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內存設置不夠。原因有二:
a. 程序啟動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。
b. 大量動態反射生成的類不斷被加載,最終導致Perm區被占滿。
說明:
Jdk1.6及之前:常量池分配在永久代 。
Jdk1.7:有,但已經逐步“去永久代” 。
Jdk1.8及之后:無(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現在JDK1.8中)。
24,21運行時數據區各個模塊協作工作的總結較好的圖(來自:https://blog.csdn.net/wangtaomtk/article/details/52267634)
首先要執行的代碼是:
先執行main方法:
當要調用其他方法時:
25,java代碼編譯(Java Compiler)過程,也就是由.java文件到.class文件的過程(附上網上很流行的圖,個人感覺沒必要記,反正我是記不住)
注:源代碼就是.java文件,JVM字節碼就是.class文件
27,是先加載字節碼文件還是先執行main方法:先加載字節碼文件到方法區,然后在找到main執行程序。
28,java被編譯成了class文件,JVM怎么從硬盤上找到這個文件並裝載到JVM里呢?
是通過java本地接口(JNI),找到class文件后並裝載進JVM,然后找到main方法,最后執行。
29,我們平時所說的八大基本類型的在棧中的存放位置是:運行時數據區--》虛擬機棧--》虛擬機棧的一個棧幀--》棧幀中的局部變量表;局部變量表存放的數據除了八大基本類型外,還可以存放一個局部變量表的容量的最小單位變量槽(slot)的大小,通常表示為reference;所以是可以放字符串類型的,但是要以 String a="aa";的形式出現,如果是new Object()那就只能實在哎堆中了,棧里面存的是棧執行堆的地址。
30,堆內存大小-Xms -Xmx設置相同,因為-Xmx越大tomcat就有更多的內存可以使用,這就意味着JVM調用垃圾回收機制的頻率就會減少(垃圾回收機制被調用是jvm內存不夠時自動調用的)可以避免每次垃圾回收完成后JVM重新分配內存。
31,GC具體什么時候執行,這個是由系統來進行決定的,是無法預測的。
32,方法區也被成為堆內存中的永久代,看下面例子:
-
Student s = new Student("小明",18);
-
-
s 是指針,存放在棧中。
-
-
new Student("小明",18) 是對象 ,存放在堆中。
-
-
Student 類的信息存放在方法區。
-
-
總結 :
-
-
對象的實例保存在堆上,對象的元數據(instantKlass)保存在方法區,對象的引用保存在棧上。
類加載是會先看方法區有沒有已經加載過這個類,因此方法區中的類是唯一的。方法區中的類都是運行時的,都是正在使用的,是不能被GC的,所以可以理解成永久代。
33,java 內存模型
讀完上面那么多最后一個知識點,理解下多線程的一點知識。我們應該知道了在運行時數據內存區中虛擬機棧、pc寄存器、本地方法棧是每個線程都有的,很明顯這些都是獨立的不會發生線程不安全的問題,但是我們平時討論的線程不安全、要加鎖等等情況是怎么回事呢?
其實,發生線程不安全問題的原因在於cpu,看下圖,簡單理解cpu
在CPU內部有一組CPU寄存器,也就是CPU的儲存器。CPU操作寄存器的速度要比操作計算機主存快的多。在主存和CPU寄存器之間還存在一個CPU緩存,CPU操作CPU緩存的速度快於主存但慢於CPU寄存器。某些CPU可能有多個緩存層(一級緩存和二級緩存)。計算機的主存也稱作RAM,所有的CPU都能夠訪問主存,而且主存比上面提到的緩存和寄存器大很多。
當一個CPU需要訪問主存時,會先讀取一部分主存數據到CPU緩存,進而在讀取CPU緩存到寄存器。當CPU需要寫數據到主存時,同樣會先flush寄存器到CPU緩存,然后再在某些節點把緩存數據flush到主存。
Java內存模型和硬件架構之間的橋接
正如上面講到的,Java內存模型和硬件內存架構並不一致。硬件內存架構中並沒有區分棧和堆,從硬件上看,不管是棧還是堆,大部分數據都會存到主存中,當然一部分棧和堆的數據也有可能會存到CPU寄存器中,如下圖所示,Java內存模型和計算機硬件內存架構是一個交叉關系:
當對象和變量存儲到計算機的各個內存區域時,必然會面臨一些問題,其中最主要的兩個問題是:
- 共享對象對各個線程的可見性2. 共享對象的競爭現象
問題1:
共享對象的可見性
當多個線程同時操作同一個共享對象時,如果沒有合理的使用volatile和synchronization關鍵字,一個線程對共享對象的更新有可能導致其它線程不可見。
想象一下我們的共享對象存儲在主存,一個CPU中的線程讀取主存數據到CPU緩存,然后對共享對象做了更改,但CPU緩存中的更改后的對象還沒有flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的。最終就是每個線程最終都會拷貝共享對象,而且拷貝的對象位於不同的CPU緩存中。
下圖展示了上面描述的過程。左邊CPU中運行的線程從主存中拷貝共享對象obj到它的CPU緩存,把對象obj的count變量改為2。但這個變更對運行在右邊CPU中的線程不可見,因為這個更改還沒有flush到主存中:
要解決共享對象可見性這個問題,我們可以使用java volatile關鍵字。 Java’s volatile keyword. volatile 關鍵字可以保證變量會直接從主存讀取,而對變量的更新也會直接寫到主存。volatile原理是基於CPU內存屏障指令實現的。
問題2:
競爭現象
如果多個線程共享一個對象,如果它們同時修改這個共享對象,這就產生了競爭現象。
如下圖所示,線程A和線程B共享一個對象obj。假設線程A從主存讀取Obj.count變量到自己的CPU緩存,同時,線程B也讀取了Obj.count變量到它的CPU緩存,並且這兩個線程都對Obj.count做了加1操作。此時,Obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。
如果這兩個加1操作是串行執行的,那么Obj.count變量便會在原始值上加2,最終主存中的Obj.count的值會是3。然而下圖中兩個加1操作是並行的,不管是線程A還是線程B先flush計算結果到主存,最終主存中的Obj.count只會增加1次變成2,盡管一共有兩次加1操作。
要解決上面的問題我們可以使用java synchronized代碼塊。synchronized代碼塊可以保證同一個時刻只能有一個線程進入代碼競爭區,synchronized代碼塊也能保證代碼塊中所有變量都將會從主存中讀,當線程退出代碼塊時,對所有變量的更新將會flush到主存,不管這些變量是不是volatile類型的。
volatile和 synchronized區別
volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化
支撐Java內存模型的基礎原理
34,方法區與堆的區別
方法區存放了類的信息,有類的靜態變量、final類型變量、field自動信息、方法信息,處理邏輯的指令集,我們仔細想想一個類里面也就這些東西,而堆中存放是對象和數組,咋一看好像方法區跟堆的作用是一樣的。其實呢,1,這里就關系到我們平時說的對象是類的實例,是不是有點恍然大悟了?這里的對應關系就是 “方法區--類” “堆--對象”,以“人”為例就是,堆里面放的是你這個“實實在在的人,有血有肉的”,而方法區中存放的是描述你的文字信息,如“你的名字,身高,體重,還有你的行為,如吃飯,走路等”。2,再者我們從另一個角度理解,就是從前我們得知方法區中的類是唯一的,同步的。但是我們在代碼中往往同一個類會new幾次,也就是有多個實例,既然有多個實例,那么在堆中就會分配多個實例空間內存。
35,方法區的內容是一次把一個工程的所有類信息都加載進去再去執行還是邊加載邊執行呢?
其實單從性能方面也能猜測到是只加載當前使用的類,也就是邊加載邊執行。例如我們使用tomcat啟動一個spring工程,通常啟動過程中會加載數據庫信息,配置文件中的攔截器信息,service的注解信息,一些驗證信息等,其中的類信息就會率先加載到方法區。但如果我們想讓程序啟動的快一點就會設置懶加載,把一些驗證去掉,如一些類信息的加載等真正使用的時候再去加載,這樣說明了方法區的內容可以先加載進去,也可以在使用到的時候加載。
36,方法區,棧、堆之間的過程
類加載器加載的類信息放到方法區,--》執行程序后,方法區的方法壓如棧的棧頂--》棧執行壓入棧頂的方法--》遇到new對象的情況就在堆中開辟這個類的實例空間。(這里棧是有此對象在堆中的地址的)