3萬字的Java虛擬機學習筆記,首次公開分享,還貼心準備了PDF!

java虛擬機一直屬於比較難的一個知識點,很多初學者會不知如何下手,很多文章寫的晦澀難懂,只因JVM本身比較難理解,想學習的可以先看看我在學習JVM的時候記的筆記,大概三萬多字,絕對讓你有收穫,底部有獲取PDF的方法哦!

重點先理解這些

隨着自己的不斷學習,對之前所做的筆記會有不同的認識和理解,所以在不斷學習的過程中,也要經常回顧自己的筆記

當我們完成一個java文件的編寫,然後經過javac命令的編譯成了class文件,這個class文件除了有類的版本,方法,字段和接口等信息以外,還有一項重要的信息就是常量池,這個叫做class文件常量池,主要就是用來存放

1.編譯期生成的各種字面值

  1. i.文本字符串
  2. ii.八種基本數據類型的值
  3. iii.被聲明爲final的常量等

2.符號引用

i.類和方法的全限定名
ii.字段的名稱和描述符
iii.方法的名稱和描述符

當類從java文件編譯成class文件,這個時候就有了class文件常量池,當被加載到內存中的時候class文件常量池也被加載進去了,這個時候class文件常量池就變成了運行時常量池,此時可以動態的添加字面量,符號引用也可以被解析爲直接引用。

當一個線程開始的時候就產生了一個java虛擬機棧,當線程中的一個方法被調用的時候就會產生一個棧幀,這個棧幀就開始入棧(java虛擬機棧),這個棧幀中有一個局部變量表,用來存放基本數據類型和對象引用,基本數據類型的值存放在操作數棧中,而實例對象存放在堆中,但是對象引用在局部變量表中,此對象引用指向堆中的具體對象,基本數據類型指向操作數棧中的具體的值。

字符串常量池在jdk1.7之前字符常量池是存放在方法區中的,但是在jdk1.7及之後就從方法區中移除了字符串常量池,放在了堆中。

符號引用:強調的是編譯成class文件之後,這個時候並不能確定一個類的引用到底指向誰,因此只能使用特定的符號代替,這就叫做符號引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。

直接引用:在類加載階段,經過解析將符號引用解析成直接引用,也就成了指向一個具體目標的內存地址。

對象引用:我們能看到的,在java文件中的實例對象的引用

1、什麼是JVM(Java虛擬機)

維基百科的解釋:

Java虛擬機(英語:Java Virtual Machine,縮寫爲JVM),一種能夠運行Java
bytecode的虛擬機,以堆棧結構機器來進行實做。最早由太陽微系統所研發並實現第一個實現版本,是Java平臺的一部分,能夠運行以Java語言寫作的軟件程序。

Java虛擬機有自己完善的硬體架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。JVM屏蔽了與具體操作系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。通過對中央處理器(CPU)所執行的軟件實現,實現能執行編譯過的Java程序碼(Applet與應用程序)。

作爲一種編程語言的虛擬機,實際上不只是專用於Java語言,只要生成的編譯文件匹配JVM對加載編譯文件格式要求,任何語言都可以由JVM編譯運行。此外,除了甲骨文,也有其他開源或閉源的實現。

這裏要注意的是Java虛擬機並不是專門針對Java語言的,只要可以編譯生成字節碼文件就可以被jvm執行,也就是說Java虛擬機所針對的對象是字節碼文件,而不是一個特定的編程語言。

從操作系統層面理解虛擬機

Java虛擬機是運行在操作系統中的,也就是說Java虛擬機是以一個進程的方式運行在操作系統中的,因爲進程是操作系統的執行單位。

當Java虛擬機在運行的時候就是操作系統中的一個進程實例,沒有運行的話就是一個程序。

對於我們熟知的命令行而言,一個命令就對應一個進程,也就是說當你輸入一個命令並且回車的時候就創建了一個進程實例。創建完一個進程之後,就會加載相對應的可執行文件到進程的地址空間中,然後執行其中的命令。

比如我們熟知的,使用javac可以將一個Java源文件翻譯成Java字節碼指令,然後使用java命令去執行這個字節碼文件,當我們輸入java然後回車的時候其實就是創建了一個Java虛擬機的進程實例,接着就會將這個字節碼文件加載進Java虛擬機中(此進程),在Java虛擬機中是有相應的空間的。

拿javac和java這兩個命令來詳細的說一下:

在這個過程中javac就是將我們寫的Java源文件翻譯成Java虛擬機可以執行的字節碼文件,也就是class文件(也叫做本地文件),重點在這個java指令上,當我們輸入java這個指令,就會啓動一個Java程序,這個Java程序運行起來就是一個Java虛擬機進程,這個進程啓動之後就會把相應的類加載進內存中,加載進內存之後就會對這個類進行初始化和動態鏈接,接下來就是從這個類的main方法開始執行了。

字節碼文件被加載進內存之後不是直接在cpu上執行,而是被Java虛擬機進程託管着,需要由Java虛擬機對這個字節碼文件進行一系列的操作。字節碼文件是被Java虛擬機中的類加載器加載的,然後由這個虛擬機進程去解釋這個class文件中的字節碼指令。然後再把這個字節碼指令翻譯成本機cpu能夠識別的指令,然後再在cpu上運行。

也就是說我們在執行一個Java程序的時候,實際上是執行一個叫做Java虛擬機的進程,這個Java虛擬機進程會執行一些底層的操作,比如內存的分配和釋放,我們寫的Java源文件被翻譯後的字節碼文件只不過是虛擬機進程的一個“原料”而已。

之前說過字節碼文件也就是class文件,是由Java虛擬機中的類加載器來加載的,但是這個加載是按照需要來加載的,也就是隻有當一個類需要的時候纔去加載這個類,並不是一開始就會加載所有的類。

當一個類被加載進Java虛擬機內部之後,Java虛擬機會去讀取這個字節碼文件中存在的字節碼指令,在Java虛擬機中去讀取這個字節碼指令的部分叫做執行引擎,執行引擎負責字節碼指令的執行。
Java一個非常大的特點就是內存的自動管理,不需要我們去寫代碼來進行內存的釋放,它會自動的去進行內存的分配和釋放,而這部分就是由垃圾收集這個子系統來執行的。

所以有三個子系統在Java虛擬機中是非常重要的。

1、類加載器子系統
2、執行引擎子系統
3、垃圾收集子系統

這裏總結一句話,就是虛擬機的執行,必須加載字節碼文件,然後執行字節碼文件中的字節碼指令。

那麼Java虛擬機就需要有自己的空間來存放相應的數據了,比如加載的字節碼需要一個單獨的空間來存放,一個線程的執行也需要內存空間來維護方法的調用關係,存放方法中的數據和中間計算結果,還有創建的對象也需要一定的空間,這就牽涉到Java虛擬機的內存空間的知識了,也就是Java虛擬機的內存結構。

2、Java的運行條件

這個我們就要說說想要運行一個Java源程序,我們該有哪些東西或者做些什麼,這個我們剛開始學Java或許都會傻傻分不清楚以下幾個名詞

  1. jdk
  2. jre
  3. Jvm

想必剛開始大家學習Java一定深受環境配置之苦吧,爲啥我照着書上講的視頻裏面說的配置的,可是爲啥就是人家的可以我的就不行呢?

這是個神奇的問題,試問學編程的誰還沒有遇到點詭異的事情,不過實際情況可能是你其中的某個步驟是真的錯了,爲啥,因爲這些jdk啊,jre啊那麼高級的詞彙怎麼可能讓你那麼輕鬆就能弄明白的

那這些高級詞彙都是啥勒?

首先是這個jdk,對對對,我得先告訴你它的全名叫做

Java Development Kit

翻譯過來也就是Java開發工具包,從這名字我們大概知道,有了它我們就能進行Java開發了,不過要怎麼才能使用這個Java開發工具包呢?那就是你們遇到的詭異的環境變量配置了

其實這個吧網上教程多的很,跟着多操作幾遍,自己多思考思考也沒什麼難的,咱們的重點不再這,所以不去說如何進行環境變量配置的問題

當我們把jdk弄好了,也就是可以使用這個Java開發工具包了,那我們怎麼使用呢?就拿我們之前寫的那個Test.java來說吧,我們可以在這個源文件的當前路徑下打開終端,然後輸入

javac Test.java

然後回車,然後你就會發現多出了一個叫做Test.class的文件,然後我們可以繼續使用jdk中提供的命令

java Test

順利的話你就會看到輸出內容了,可是你真覺得你會順利嗎?那很有可能不啊,因爲你少了一樣東西啊?那就是jre

那什麼是jre呢?根據jdk我們可以猜得出來這應該也是縮寫了,趕緊查查,哦,是這個

Java Runtime Environment

翻譯過來就是Java運行環境,哦,從字面意思理解好像有了它才能運行Java程序,那這個怎麼弄呢?jdk我們可以下載下來,配置到環境變量,那麼這個jre呢?其實也是一樣的,而且在jdk中也包含jre。

總之吧,你想成功運行一個Java程序,那就得有jdk和jre,其實還應該有個jvm,不過這個就是底層的東西了,可是我們還是要說說這個jvm的

同理,這個jvm的全稱是

Java Virtual Machine

也就是Java虛擬機,這個相當重要,爲啥?如果你學習Java的話你就一定知道Java的跨平臺,也就是一句非常經典的話

“write once,run everywhere”

要知道Java實現跨平臺可全是jvm的功勞啊

3、Java的跨平臺

下面我們就來好好說說這個跨平臺,也就是來聊聊jvm,說到這個我們需要把c和c++也拿過來一起做個比較。

我們或許知道c語言是一個偏底層的語言,是面向過程編程的語言,對於面向過程語言,它專注的是數據之間的流向,重點在過程兩個字,而c++和Java語言都屬於面向對象編程語言,他們關注的則是不同對象之間的一個關係,重點在對象兩個字。

c語言應該是每個學計算機的都會學的吧,或多或少都會知道一個很厲害的名字叫做“指針”,是可以直接去操作內存的,這帶來的問題就是我們需要自己手動的去釋放內存,而Java我們應該也知道,有了jvm來管理內存,我們不需要自己手動釋放內存

也就是說,Java把c++和c語言中那個神奇的指針給去掉了,爲啥,因爲指針是可以直接操作內存的,因此肯定會帶來很多的編程錯誤,這裏給大家看一張圖來理解吧

在這裏插入圖片描述

c和c++因爲有了指針的存在,所以是可以直接與操作系統進行交互的,從而可以去操作內存,但是誰能保證你寫的沒啥問題,直接操作內存難免會出現問題,而且你還需要自己手動的去釋放內存。

而Java就不同,他去掉了指針,不與操作系統直接交互,而是在中間多了一層jvm,也就是說你寫的程序會先交由jvm去處理,然後纔會與操作系統進行打交道,如此一來,你就可以隨便造,反正有jvm在後面幫你把關,而且內存也會交由jvm來進行自動回收,着實很酷。

我們再來說說這個跨平臺,從圖中我們可以分析得到,無論是c還是c++他們都是嚴重依賴操作系統的,也就是說你在windows上寫的c程序在其他平臺是無法運行的,是無法做到跨平臺的,爲了更好的理解這塊,我們需要補充下知識

編譯和解釋

說到編譯和解釋,可能會遇到編譯器和解釋器的概念,這個不難理解,編譯器的作用就是編譯,而解釋器的作用就是解釋,而他們操作的對象都是程序。

根據編譯和解釋其實可以將程序語言分爲兩大類,分別是編譯型語言和解釋型語言,說到這裏我們還要明白一個問題,那就是爲什麼會有編譯和解釋,要知道,計算機是隻認識機器碼的,也就是0和1,所以我們通常情況下寫的代碼,計算機是不認識的,因此需要把我們寫的程序翻譯成計算機能夠識別的機器碼,那麼翻譯的方式就有兩種

一是編譯
二是解釋

而他們最大的區別就是翻譯的時間不同,對於編譯而言,它是一次性的把源程序翻譯成目標代碼,然後計算機讀取的時候就可以直接以機器碼進行執行,這樣的話效率就會很高,但是對於解釋則不同,解釋的特點是只有在執行的時候纔去翻譯,也就是邊翻譯邊執行。

我記得之前看過很形象的一個回答,就是什麼是編譯和解釋呢?

編譯就是相當於提前做好了一桌子菜等着你吃
而解釋就好比是吃火鍋,邊吃邊下

個人覺得很形象,對理解什麼是編譯和解釋很有幫助。

知道了什麼是編譯和解釋我們就要說道說道什麼是編譯型語言和解釋型語言了。

我們熟知的c和c++就是典型的編譯型語言,對於編譯型語言,有一個專門的編譯過程,是直接把源程序翻譯成目標代碼,而且是一次性的翻譯完成,因此執行效率很高。

而像python和js就是解釋型語言,可能有人會說到腳本語言,腳本語言也是一種解釋型語言,對於解釋型語言,會有一個專門的解釋器在執行的時候進行解釋,也就是執行一句解釋一句。

那麼對於Java屬與哪一種呢?

其實Java屬於兩者的結合,也就是Java即是編譯型也是解釋型,不過說到底,Java應該是解釋型語言,爲什麼呢?可能有點繞,但是也好理解

我們知道Java是一種跨平臺的高級語言,而實現跨平臺的則是jvm,那這個跨平臺到底是怎麼回事呢?

再來看看這段熟悉的代碼

class Test{
public static void main(String[]ithuangqing){
System.out.println("微信搜:編碼之外");
}
}

那麼這段代碼是如何做到一次編譯,到處執行的呢?注意我這裏說的是編譯,那麼在Java程序的執行中肯定存在一個編譯的過程,那麼這個Java應該屬於編譯型語言啊,爲什麼是解釋型語言呢?

不着急,慢慢來,我們來看一張圖

在這裏插入圖片描述

這段代碼我們把它命名叫做Test.java,它是一個Java源程序文件,接着我們可以使用javac命令生成一個Test.class文件,這個叫做字節碼文件。

這裏我們一定要理解的是,javac就是在執行一個編譯的過程,通過javac把Java源程序編譯成目標代碼,只不過與傳統的編譯不同的是這裏的編譯並不是將Java源程序直接翻譯成機器代碼,而是翻譯成來一箇中間代碼class文件,叫做字節碼文件。

很重要的一點,這個字節碼文件是與平臺無關的,這是實現跨平臺非常重要的前提,也就是生成的字節碼文件是與平臺無關的,那麼接下來如何在不同的平臺上進行執行的呢?

這就要靠jvm了,字節碼雖然與平臺無關,但是可以由jvm進行解釋執行,因此只要在不同的平臺上加裝相對應的jvm那就可以實現跨平臺執行來

這裏需要理解的關鍵點就是不同的平臺需要有不同的jvm,也就是在每一個平臺上安裝相對應的jvm,那麼就都可以解釋執行字節碼文件了,字節碼是與平臺無關的,但是這個jvm卻是與平臺相關的,也就是說不同的平臺上的jvm是不同的。

因此,jvm對於Java的跨平臺來講就是一個橋樑,Java源程序首先編譯生成字節碼文件,這個字節碼文件是不能直接運行的,需要由不同平臺上的jvm把這個字節碼翻譯成對應平臺上的機器語言,這裏的翻譯其實就是解釋,在這個過程中,字節碼始終都是一樣的,但是由各個平臺上的jvm翻譯之後的機器碼卻是不同的。

Java正是通過這種機制實現的跨平臺,總結下就是Java是跨平臺的,真正跨平臺的是Java程序,而jvm是c和c++編寫的軟件,是編譯後的機器碼,不同的平臺上jvm的版本是不同的。

經過編譯之後的字節碼文件是存放在我們電腦中的磁盤中的,當對字節碼文件進行解釋的時候,這個字節碼文件就會通過一個類加載器的東西把字節碼文件加載進電腦的內存中,當然這個加載過程是有特定的步驟的,主要就是檢查這個字節碼文件是否符合jvm規範等等,加載成功就會在電腦中的內存中開闢一塊空間,這塊空間其實就是jvm,然後再由內存輸出內容。

到這裏我們就可以發現,一個Java程序的運行必須有以下三個前提條件,那就是

  1. jdk
  2. jre
  3. Jvm

寫到這裏,我突然想了想我這篇文章的價值在哪裏,如果你看完這篇文章能跟別人說保證Java程序運行的三個條件是什麼以及爲什麼,那麼就能證明我這篇文章對你還是有價值的。

4、Java中的引用

Java中的引用有如下幾種:
1、強引用
2、弱引用
3、軟引用
4、虛引用

強引用是最常見,最普通的引用了,我們來舉一個例子,我們看下面這行代碼

Object o=new Object();

以上代碼就是一個強引用,也就是說我們創建了一個Object對象,並把它賦值給了o,那麼,現在這個o其實就是代表着我們創建的這個Object對象,所以說,o其實就是一個引用,代指這個Object對象。

Object o;o=new Object();

這裏的o肯定是同一個,那麼這個o是個對象嗎?我們知道對象的創建是通過new的方式,如果這個o是一個對象的話,那麼爲什麼還需要再次通過new來創建呢?也即是這個o並不是一個對象。

那麼,這個o到底是什麼呢?對,這個o其實就是一個引用,也就是說,我們創建了一個Object對象的引用,然後通過new的方式創建了一個Object對象,然後用這個o指向我們創建的這個Object對象。這個o其實就是一個對象引用。

明白了對象的引用,那什麼是對象呢?這個更簡單,對象其實本質上就是對象的實例。

java語言拋棄了C和C++中的指針,但是,java中的引用其實和指針是很像的,可以說是一種變型!

Object o;o=new Object();o=new Object1();

我們看這段代碼,首先,o指向了Object,然後又指向了Object1,所以說,引用可以指向任何實例對象,但是不同同時指向多個對象,由此,我們想一下,多個引用可不可以指向同一個對象呢?

我們再看這個代碼

Object o=new Object();Object o1=o;

通過以上代碼我們就可以看出,多個引用是可以指向同一個對象的。

其實強引用是最常見的引用了,也是最普遍的,剩下的三種引用都是引用關係逐漸減弱的,所以我們可以得知,對於強引用而言是不會被垃圾回收器給回收的,也就是說只要強引用關係還存在,這個對象就不會被回收。

軟引用和弱引用其實可以理解成那種非必須的對象引用,這些也是會被垃圾回收器優先回收的對象,那虛引用是最弱的一種引用關係,這些暫時還不用深入瞭解,就先不深入的去說了。

5、Java內存結構

java內存結構也就是jvm內存結構,我們經常說的是jvm內存結構,包含了堆內存,棧和方法區等內容,是學習jvm必備的知識,所以jvm內存結構這塊知識的學習是很重要的!

首先要知道的就是java內存結構等同於jvm內存結構!下面是jvm的內存結構圖
在這裏插入圖片描述

然後jvm內存結構包含以下內容:

  1. 程序計數器
  2. java虛擬機棧
  3. 本地方法棧
  4. java堆
  5. 方法區運行時常量池

因此,學習jvm的內存結構也就是要弄懂上面幾個東西!

程序計數器

程序計數器,有的地方也叫作pc計數器,都是它,是在jvm內存中屬於較小的額一個內存空間,但是十分重要,我們知道在多線程中,是靠CPU來切換線程的執行順序的方式實現的,也就是說,從線程A切換到線程B,然後再切換到線程A的時候,你有沒有想過cpu是怎麼知道應該執行線程A中的哪一步,也就是說,之前在線程A中執行到哪了,這就要靠程序計數器去記錄了。

這就是程序計數器了,另外要知道的就是程序計數器是線程私有的,互相獨立,如果被問到什麼是程序計數器,我覺得可以這樣回答:

當前線程所執行的的字節碼的行號指示器!

java虛擬機棧

我們之前應該經常會說或者經常聽到堆內存和棧內存,堆內存想必大家都很熟悉了,這個我們隨堆內存經常說的棧內存準確的來說應該是java虛擬機棧中的局部變量表,在此之前我們應該也知道一個常識就是基本數據類型是存放在棧內存中的,對象是存放在堆內存的,現在準確的去說,基本數據類型是存放在java虛擬機棧中的局部變量表中的。

java虛擬機棧也是線程私有的,生命週期跟隨線程,很重要的一個點就是要明白java虛擬機棧是與java方法相關的,什麼意思呢?

當線程開始,也就產生了這個java虛擬機棧,當一個java方法會調用的時候就會產生一個棧幀,這個棧幀是用來幹嘛的呢?

這個時候產生的一個棧幀就是用來存放局部變量表,操作數棧,動態鏈接和方法出口信息等,注意了,這裏的局部變量表是在棧幀中保存。另外,一個方法從調用到執行結束的整個過程就是一個棧幀在java虛擬機棧中從入棧到出棧,這就把棧幀和java虛擬機棧聯繫起來了。

而這個局部變量表是幹嘛的呢?就是用來存放各種基本數據類型,對象的引用得,想必這個大家都熟悉,就是大家常說的棧內存所做的事情啊。可能這裏還要注意的就是這裏說的基本數據類型都是在編譯期就已經確定下來的,而且局部變量的的空間在編譯期就是確定的,運行時期是不會再改變的。

本地方法棧

想必這個一定會讓大家想到java虛擬機棧,那麼兩者有什麼區別呢?其實極爲相似,不同的是服務的對象不同,java虛擬機棧是爲執行java方法服務,而本地方法棧是爲使用到的Native方法服務,那麼重點來了,什麼是Native方法呢?

關於native方法我這裏簡單說下我的理解,java號稱吸收了c加加和c語言的優點,剔除了較難的指針,不過,我是學java的,我就認爲java好,可是嘞,不得不承認,java語言要比c++的運行慢很多,另外也是因爲沒有指針吧,所以java並不能去直接操作底層,爲了彌補這個缺點,也就有了native方法,來建立這麼一種聯繫。

關於native方法,就說這麼多,大家可以自行搜索學習,等我研究的差不多了再來分享!

堆內存

堆內存是我們要經常與之打交道的一塊內存地址,所有的實例對象和數組都在這裏存儲,也是垃圾回收器主要工作的地方,所以堆內存也叫作gc堆,也就是垃圾堆,哈哈。

當然可能大家也知道,在堆內存中其實也是有劃分的,比如分有新生代和老年代,再細緻一點的話有Eden空間,from和to空間等,我們這裏要把握的是無論怎麼劃分,存放的就是對象實例就ok了。

對java堆中我們後面會單獨拿出來說的,因爲很重要!還需要記住的是堆內存是所有線程共享的。

直接內存(堆外內存)

堆外內存就是在Java堆之外的內存,也叫做直接內存,並不是jvm規範之內的內存區域。使用不多。

直接內存存在一個IO操作方面的優勢,比如:
舉一個例子:在通信中,將存在於堆內存中的數據 flush 到遠程時,需要首先將堆內存中的數據拷貝到堆外內存中,然後再寫入 Socket 中;如果直接將數據存到堆外內存中就可以避免上述拷貝操作,提升性能。類似的例子還有讀寫文件。

直接內存由DirectByteBuffer這個類來分配內存空間,這個類對象位於Java堆中,它鏈接着堆外一大塊的內存塊,這個類對象被回收的話,直接內存也就沒有了。

DirectByteBuffer 中用於分配堆外內存的方法 unsafe.allocateMemory(size) 是個一個 native 方法,本質上是用 C 的 malloc 來進行分配的。分配的內存是系統本地的內存,並不在 Java 的內存中,也不屬於 JVM 管控範圍,所以在 DirectByteBuffer 一定會存在某種特別的方式來操縱堆外內存。

堆外內存主動回收原理

第一種就是基於Jvm gc機制,目的就是回收掉DirectByteBuffer,而它一般都是存在於老年代中,也就是只有在發生Full GC的時候直接內存纔會被回收掉,但是這樣的話會出現的情況就是,堆內存沒有滿而直接內存已經滿了。

第二種就是使用Cleaner對象
從 DirectByteBuffer 裏取出那個 sun.misc.Cleaner,然後調用它的 clean() 就行,而 clean() 執行時實際調用的是被綁定的 Deallocator 類的 run 方法,其中再調用 freeMemory 釋放內存。

方法區和運行時常量池

這塊知識我們需要掌握的一個重點就是在jdk1.7之前字符常量池是存放在方法區中的,但是在jdk1.7及之後就從方法區中移除了字符串常量池,放在了堆中。

方法區也是多個線程共享的,存放已經被虛擬機加載的類信息,常量和靜態變量等數據信息,在方法區中還存在一個運行時常量池,這個是值得好好研究的。

首先是Class文件中除了有類的字段,方法和接口等描述信息之外還有一個常量池,這些內容會在類加載後進入方法區的運行時常量池。

而這個常量池是用於存放編譯階段生成的各種字面量和符號引用

6、Java虛擬機棧

java虛擬機棧是jvm內存結構中的一員,也就是我們平常所說的棧內存,它是線程私有的,每個線程都有屬於自己的一個java虛擬機棧,java虛擬機棧的生命週期和線程相同,也就是說當一個線程開始了,也就產生了一個java虛擬機棧。

既然是棧,肯定有個什麼玩意入棧和出棧,java虛擬機棧主要是用來存放線程運行方法時所需的數據,指令和方法返回地址等,那麼靠什麼存儲?這就需要棧幀,

在這裏插入圖片描述

棧幀的產生必須是一個方法被調用了,也就是說,線程開始,有了一個java虛擬機棧,當一個方法被調用,就產生一個棧幀用來存放運行這個方法所需的一些數據,一個方法從被調用到結束就對應一個棧幀從入棧到出棧的過程,可以得知,棧幀是和方法息息相關的。這個棧幀包含這麼些東西。

  1. 局部變量表
  2. 操作數棧
  3. 動態鏈接
  4. 方法返回地址

java虛擬機棧線程私有,隨線程開始而產生。
java虛擬機棧主要靠方法被調用的時候產生的棧幀來存放數據。
棧幀隨一個方法被調用而產生
一個方法從被調用到結束就對應棧幀在java虛擬機棧中入棧和出棧的過程。

我們之前常說,基本數據類型是保存在棧內存中的,現在要知道的是這是根據時期而定的,因爲如果你單單理解基本數據類型是存放在棧內存的時候,當你遇到運行時常量池的時候你一定會迷,爲啥,運行時常量池也是存放基本數據類型啊,那到底誰存放呢?

這就要根據時期來說了。當你編寫一個java文件,被編譯成class文件之後,這個class文件中就產生了一個class文件常量池,當被加載到內存中的時候,這個class文件常量池就成了運行時常量池,當然,運行時常量池包含的東西要多點,這時候基本數據類型也是存放在這個運行時常量池的。

但是,你要注意了,這個時候並沒有什麼線程開始和方法調用,所以也就沒有什麼棧幀來存放數據,只有當你的方法被調用的時候,纔會產生一個棧幀來存放數據,這時候就會存放基本數據類型的數據,而我猜想這些數據也是從運行時常量池拿來的。那麼棧幀中是如何操作的呢?其實棧幀也分爲這麼幾個部分

  1. 局部變量表
  2. 操作數棧
  3. 動態鏈接
  4. 方法返回地址

而我們說的基本類型存放在棧內存,更加準確的說就是存放在棧幀中的局部變量表。

關於類變量和局部變量有這麼一個區別,對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來說,在使用前必須顯式地爲其賦值,否則編譯時不通過。

這個棧幀中除了局部變量表之外還有操作數棧,動態鏈接和方法返回地址。那什麼是操作數棧呢?在《深入理解java虛擬機》中有這麼一段話“整數加法的字節碼指令iadd在運行的時候操作數棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會將這兩個int值出棧並相加,然後將相加的結果入棧。”也就是說操作數棧也是存儲數據的區域。

每一個棧幀中都會有這麼一個引用,這個引用存放在運行時常量池中,這個引用指向該棧幀,這個引用的目的是爲了支持方法調用過程中的動態鏈接。符號引用在類加載階段或者第一次使用階段會直接轉換爲直接引用,這個叫做靜態解析,還有的是在每一次運行期間轉化爲直接引用,這部分就稱爲動態鏈接。

方法的退出也就意味着棧幀從java虛擬機棧中出棧,方法的退出一般有兩種,一種是正常退出,一種是異常退出,但是,無論是以哪種方式退出,最終都要返回到方法調用的地方,如果是正常退出的話,那麼這個返回地址就是調用者的程序計數器的值,如果是異常退出的話,返回地址是由異常處理器表決定的。

7、對象的創建

對象有這麼幾種創建方式:

  1. new關鍵字

  2. 運用反射手段,調用java.lang.Class或者java.lang.reflect.Constructor類的newInstance()實例方法

  3. 調用對象的clone()方法

  4. 運用序列化手段,調用java.io.ObjectInputStream對象的readObject()方法.

在jvm層面對象的創建是這樣滴

在這裏插入圖片描述

對象的創建?首先,什麼是對象?面向對象編程?萬物皆對象?算了,比如下面一段代碼

Student s=new Student();
以上代碼就創建了一個Student對象,這個s就叫做對象引用,後面的new Student()就在堆內存中開闢一塊新的內存空間用來存放這個Student對象的實例,而這個內存空間有一個內存地址就存放在java虛擬機棧中的棧幀中的局部變量表。

也就是說在這個局部變量表中開闢一個內存空間存放這個堆中存放這個實例對象的內存空間的內存地址,而在這個局部變量表中新開闢的空間,我們就叫它“s”吧!

以上就是我們最爲熟知的創建對象的一種方式,就是通過new這個關鍵字,不過創建對象的方式可不止這一種,還有這麼幾種

new關鍵字

運用反射手段,調用java.lang.Class或者java.lang.reflect.Constructor類的newInstance()實例方法

調用對象的clone()方法

運用序列化手段,調用java.io.ObjectInputStream對象的readObject()方法.

那麼,這幾種都是怎麼實現對象的創建呢?你知道new是如何創建對象的嗎?知道的話,那就可以了,剩下的不急着去研究他們,我們繼續往下說對象的創建,也就是說,你要熟知使用new關鍵字創建對象的方式,然後還知道有其他創建對象的方式就可以了。

接着,我們繼續!

我們在之前知道了jvm的內存結構,知道了當你編寫一個java源文件之後可以使用javac命令將其編譯成class文件,當class文件被加載進內存中,說的粗暴一點,也就是這個class文件會被弄得稀巴爛,然後存放在jvm內存結構中幾個不同的區域之中。

對了,你還記得class文件常量池中都是存放些什麼玩意嗎?答案是字面量和符號引用。

那接下來我們就深入jvm層面去看看這個對象到底是怎麼創建的,我們就以這個new關鍵字創建對象來說。

還拿這段代碼來說

Student s=new Student();
下面開始分析啦,注意了

當你寫了這麼一段代碼,在jvm中是如何執行的呢?首先當jvm發現這個new指令的時候就會先對符號引用進行分析,爲什麼要對符號引用進行分析呢?你可知道在運行時常量池階段,符號引用會被解析成直接引用,也就是指向對象的那個地址,在此之前也就是這個符號引用可並不是這個地址,而是一個特定的符號,這個符號引用代表着你這個類被加載了,所以如果在class文件常量池中如果沒有發現這個符號引用的話,說明了什麼呢?

當然是你這個Student類還沒有被加載呢?所以就需要進行和這個Student類的加載了,關於類加載,我們這裏先不談。

假設現在類加載完成了,找到了這個符號引用,那麼在類的解析階段就會把這個符號引用解析成直接引用,你想啊,直接引用都出來了,是不是獨享就被創建成功了,對象創建在哪呢?

當然是堆啦,所以jvm會在堆中給這個Student對象分配一塊內存來存放這幾個對象,而這個內存是有一個地址的,就是直接引用啦。

jvm爲對象分配完內存之後可沒有閒着,緊接着會對分配的內存進行初始化爲零值,這裏不包括對象頭。

等等,什麼是對象頭,我們常說的這個虛擬機啊一般指的就是HotSpot虛擬機,它實現的對象有這麼三個部分組成。

  1. 對象頭
  2. 實例字段
  3. 對齊填充字段

那麼,如果你要再問這三個是什麼玩意,我吧,就不知道怎麼回答了,也就說,你就記得在HotSpot中實現的對象包含這三個玩意就行了,不用再往深處去研究了,至少現在不用,ok?

最後,對象的創建還差這麼一步就是jvm會調用對象的構造函數,據聽說,這個調用會一直追溯到Object類。

至此,對象算是創建成功了,當然,這裏面還有很多細節,但是,這些細節你需要都把他們搞明白嗎?這個還真的不需要,在學習中要有一個大的前進方向,不要被一些旁枝末節所阻擋,注意,我可沒有說這些旁枝末節不重要。

8、堆內存

因爲我們的對象是放在堆內存的,而我們的gc就是回收垃圾對象的,也就是說我們的gc是在跟堆內存不停的打交道

也許在你學習jvm之前你只知道堆就是一個內存空間嗎,但是在學完jvm或者說學完gc之後你會發現在堆中其實也是分好幾塊的,我們看下這個圖

這就是堆的內部結構了,在堆中分出了這幾塊內容

新生代老年代

其中新生代又被分成了三塊,分別是eden,s0和s1,也叫作from區和to區。我們接下來分別說一下。

在這裏插入圖片描述

這就是堆的內部結構了,在堆中分出了這幾塊內容

新生代老年代

其中新生代又被分成了三塊,分別是eden,s0和s1,也叫作from區和to區。我們接下來分別說一下。

新生代

只有你接觸gc可能纔會接觸到新生代和老年代的概念,那這是什麼意思呢?首先我們要知道,堆中無非就是存放對象的地方,新生代和老年代都是存在堆中,所以也就是都是存放對象的地方,可能存放的對象有所不同而已。

總的來說,記住一點就是,一些新創建的對象都會被放在新生代中,如果這個對象使用頻率比較高就會被放在老年代中。

一般,一個剛剛創建的對象會被存放在eden區中,隨着使用的頻率會被轉移在from或者to中,這兩個區域其實差別不大。

老年代

如果一個對象經常被使用,也就是使用頻率很高,就會被存放在老年代中,老年代中存放的對象都是經常使用的對象。

Gc會經常性的來新生代和老年代中逛一逛,當然,我們可以知道,gc經常關顧的應該是新生代,因爲老年代中存放的都是經常使用的對象,所以被回收的機率較小,而新生代中的對象被回收的機率則較大,所以gc就會不斷的根新生代和老年代打交道,從而進行相應的垃圾回收。

我們知道gc會經常去新生代或者老年代中看看有沒有需要回收的對象,這期間gc需要對新生代和老年代中的對象進行算法分析,查找垃圾對象,從而回收,根據我們隊垃圾回收算法的瞭解和堆新生代老年代中存放對象的瞭解我們可以得知

複製算法比較適合在新生代中,因爲老年代中的有用對象較多,所以會執行較多的複製操作,這樣的話效率就降低了,因此,老年代一般會採用其他的算法,比如標記—整理算法!

9、常量池和引用

在學習java的時候,我們經常會遇到一些很相似的概念,這個簡單來說就是名字很相似,比如我們之前提到的對象和對象引用,還由今天我們要說到的

符號引用
直接引用
class文件常量池
運行時常量池
字符串常量池

有的人可能會覺得幹嘛花費時間精力在這塊,感覺有點摳字眼了,我想說的是,這絕對不是摳字眼,弄清楚這些概念,對以後的學習很重要,而且我們這個專題準備好好的說一說這個java虛擬機,這些概念,對於虛擬機的學習

很重要!!!

首先,我們來看下面一段敘述:

當我們完成一個java文件的編寫,然後經過javac命令的編譯成了class文件,這個class文件除了有類的版本,方法,字段和接口等信息以外,還有一項重要的信息就是常量池,這個叫做class文件常量池,主要就是用來存放

編譯期生成的各種字面值:

文本字符串
八種基本數據類型的值
被聲明爲final的常量等

符號引用:

類和方法的全限定名
字段的名稱和描述符
方法的名稱和描述符

當類從java文件編譯成class文件,這個時候就有了class文件常量池,當被加載到內存中的時候class文件常量池也被加載進去了,這個時候class文件常量池就變成了運行時常量池,此時可以動態的添加字面量,符號引用也可以被解析爲直接引用。

當一個線程開始的時候就產生了一個java虛擬機棧,當線程中的一個方法被調用的時候就會產生一個棧幀,這個棧幀就開始入棧(java虛擬機棧),這個棧幀中有一個局部變量表,用來存放基本數據類型和對象引用,實例對象存放在堆中,但是對象引用在局部變量表中,此對象引用指向堆中的具體對象。

(如果上面有說得不對的地方,煩請指出!謝過!)

在上面這段描述中出現了這麼幾個概念

Class文件常量池
運行時常量池
符號引用
直接引用

我們這裏再加上一個字符串常量池,也就是這次我們一定要弄清楚這幾個概念

常量池:

Class文件常量池
運行時常量池
字符串常量池
引用:

符號引用
直接引用

首先,我們來說說這個Class文件常量池,我們編寫的java文件會被編譯爲class文件,這個class文件除了有類的版本,方法,字段和接口等信息以外,還有一項重要的信息就是常量池,這個叫做class文件常量池,主要就是用來存放

編譯期生成的各種字面值:

文本字符串
八種基本數據類型的值
被聲明爲final的常量等

符號引用:

類和方法的全限定名
字段的名稱和描述符

方法的名稱和描述符

也就是說,我們的java源文件生成的class文件中包含一個常量池,叫做class文件常量池,這裏注意一點的就是這個時候只是從java源文件編譯成class文件,然後其中產生一個class文件常量池,注意還沒有加載到內存。
那什麼是運行時常量池呢?

經過上一步驟,java源文件被編譯成class文件,其中有一個class文件常量池,然後這些會別加載到內存中,也就是jvm的運行時數據區,也就是我們之前說的餓那幾個內存區域,這塊可以看看之前說的jvm內存結構,當被加載到內存中的時候,這個時候會有一個運行時常量池,那麼這個運行時常量池是怎麼來的呢?其實它就是之前的class文件常量池演變過來的,當然這個運行時常量池還包含一些其他內容。

可以這麼說,這個運行時常量池是在被加載到內存之後,而class文件常量池並未涉及內存,還在內存之外!而此時的運行時常量池可以動態的添加字面量,符號引用也可以被解析爲直接引用。

至於字符串常量池,應該是大家最爲熟悉的一個了,我們要記住的一個知識點就是字符串常量池的位置,在jdk1.7以前是存放在方法區中的,但是在jdk1.7及之後就被放在了堆中。

下面我們再來說說引用,

可能我們之前一直在說引用引用,並沒有細分到符號引用和對象引用,那麼現在我們就來學習這兩個概念,讓我們對引用有個新的認識。

那什麼是符號引用呢?

要想知道什麼是符號引用,你必須知道的一個前提就是這裏的符號引用強調的是在java源文件編譯成class文件之後,這個時候你要知道其實一個類的引用並不能確定到底指向的是誰,因此只能使用特定的符號代替,這就叫做符號引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。

我們在上面說過在運行時常量池那個階段就可以將符號引用解析成直接引用,所以,所謂的直接引用是在類加載階段,也就是在內存中了,經過解析會從符號引用解析成直接引用,也就成了一個指向具體目標的內存地址!

10、類加載

類的生命週期:

在這裏插入圖片描述

我們需要熟悉類加載過程:

加載(將類的二進制數據加載到內存)
驗證(確保加載類的正確性)
準備(爲類的靜態變量分配內存,設置默認值)
解析(符號引用轉換爲直接引用)
初始化(設置類的正確初始化值,jvm初始化類)

準備階段還真關係到我們日常編碼的一個注意點呢!是個面試題也不爲過!

在這裏插入圖片描述

對於什麼是類我們比較清楚,那什麼是類的加載呢?java程序是運行在內存中的,而類的加載就是將類的.class文件中的二進制數據存放在內存中,這個內存指的是jvm內存。方法區中有一個運行時常量池就是生成的class文件中的class文件常量池進入內存之後的版本。

類加載的最終結果是在堆中創建一個java.lang.Class文件。這個加載進內存的.class文件就是我們將java源文件動態編譯得到的,也就是javac命令。

首先來看類的生命週期

類的生命週期一共七個步驟,其中加載,驗證,準備,解析和初始化時類加載過程,驗證和準備還有解析三個階段也被叫做連接階段。

這裏有一個需要注意的就是加載,驗證,準備和初始化這四個階段的順序是確定的,但是解析這一階段就不一定了,它也有可能在初始化之後纔開始,這是爲了支持java的運行時綁定,另外以上這幾個階段是按順序開始,但是可沒有說按順序結束,也就是他們一般情況下都是混雜着進行的。

加載階段

加載階段是類的生命週期最開始的步驟,這一階段的目的主要在於將類的二進制數據加載到內存,一般都是通過類的全限定名稱來獲取二進制字節流,然後將這個字節流代表的靜態存儲機構轉換爲方法區中的運行時數據結構,我們知道類加載的最終結果是在java堆中產生一個Class對象,那麼這個對象是用來幹嘛的呢?它就是用於後續對方法區這些數據進行訪問的一個結構,也就是我們可以通過這個類來訪問到方法區中的這些數據。

這個加載過程一般有這麼兩種方式

使用系統已經爲我們提供好的類加載器來進行加載
使用自定義的類加載器來完成加載
連接階段

這個連接階段包括驗證,準備和解析

驗證驗證這一步驟的主要目的就是爲了確保加載的類的正確性,以防加載對虛擬機有危害的類。這樣的話對安全的類就有一個評判標準,一般有如下驗證步驟

• 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。

• 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。

• 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

• 符號引用驗證:確保解析動作能正確執行。

另外一點需要注意的是,驗證這一階段是非常重要的,是用來保證虛擬機的安全,但是這一階段卻不是必須的,什麼意思呢?當你確定你這個類是安全的,也就是經過反覆驗證符合虛擬機規範的話,就可以考慮採用Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備準備階段的主要目的是爲類的靜態變量分配內存,並將其設置默認值。

這一階段會在方法區爲類變量進行默認值賦值,但是這個類變量只是靜態變量,而不是實例變量,實例變量會在對象實例化的時候跟隨對象一塊分配在java堆中。

這裏指的默認值通常情況下基本數據類型就是默認的零值,像0,null和false等。

好好說說這個準備階段

這個準備階段是可以好好研究下的,在說這個準備階段的時候,我們先來看兩個關鍵字,那就是static和final,這個一個是代表靜態,一個是代表常量,上面說過了,在準備階段的主要目的就是給這些靜態變量分配內存,設置初始默認值,這個是什麼意思呢?我們拿代碼來舉例子看下面的代碼

public static int age=2
以上代碼就是簡單定義了一個int變量,並且賦值成2,這個是在我們寫代碼的層面來說的,但是深入jvm,也就是類加載的是時候又是怎麼回事呢?這個主要集中在類加載過程中的準備階段,在準備階段的時候,這個age其實不等於2,而是等於0,因爲在準備階段會爲靜態變量賦初始值,那什麼時候纔是等於2呢?這個是在後面的初始化階段。

這裏就又值得說道說道了,要知道,只有靜態變量在這個準備階段纔會被賦初始值,其他的都靠邊站了,所以這裏就有個知識點了,就是局部變量和全局變量以及靜態變量也就是static修飾的變量,記住了

如果是基本數據類型,在準備階段會爲靜態變量和全局變量賦初始值,也就是說如果你沒有給他們顯示的賦值就直接使用的話,系統會爲他們賦初始值,也就是默認值,這個是在準備階段完成的,但是對於局部變量就不一樣了,如果你要使用局部變量的話,那必須在使用之前就給它賦值,否則編譯你都通過不了,還是舉個例子吧

在這裏插入圖片描述

看這個例子,a是一個靜態變量,b是一個全局變量,這些都可以實現不爲其賦值,但是人家可以直接使用,因爲在準備階段它們會被設置默認值0,但是這個局部變量c就不一樣了,如果你也不賦值,那結果是你編譯都過不了。

解析階段在解析階段最主要的目的就是把類中的符號引用轉換爲直接引用。

另外要知道的是這個符號引用是怎麼回事,當然還有和這個直接引用,知道了什麼是符號引用和直接引用,那麼這個解析階段也就ok了

符號引用:強調的是編譯成class文件之後,這個時候並不能確定一個類的引用到底指向誰,因此只能使用特定的符號代替,這就叫做符號引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。

直接引用:在類加載階段,經過解析將符號引用解析成直接引用,也就成了指向一個具體目標的內存地址。

初始化階段

在這個初始化階段就是將類的靜態變量賦予正確的值了,也就是你想要它表示的值,也就是這個

public static int age=2
你這個在準備階段給我搞個默認值0,但是我想讓他等於2啊,所以在這個初始化階段就給它設置成2了,不過在這個初始化階段可不單單是給類的靜態變量初始化正確的值,在這個階段jvm還會對類進行初始化。

對類進行初始化?是的,這個主要就是對類的變量進行初始化,注意這裏可不只是靜態變量,另外還有一點就是類在什麼時候纔會被初始化呢?這個就是在類被主動使用的時候纔會導致類的初始化,以下幾種情況都會導致類的主動使用。

  1. 使用new來創建一個實例對象
  2. 訪問了類或者接口的靜態變量,
  3. 或者你對靜態變量賦值
  4. 使用類的靜態方法
  5. 你使用了反射 等等

11、java內存結構,java內存模型,java對象模型和jvm內存結構

JVM這塊知識絕對是學習java過程中的重點和難點,我習慣把這塊的知識叫做javaSE高級基礎,在學習jvm這塊的知識,你一定會遇到幾個概念,那就是java內存結構,java內存模型,java對象模型和jvm內存結構!而這幾個概念是很多人搞不清楚的,瞭解了這幾個概念,將對你學習jvm很有幫助!

我們將要了解以下幾個概念:

java內存結構
java內存模型
java對象模型
jvm內存結構

Java內存結構

什麼是JVM內存結構:下面這張圖就是jvm的內存結構
在這裏插入圖片描述
可以看到就是我們平常說的堆棧什麼的!然後下面還有一個更加詳細的圖:
在這裏插入圖片描述
這就是jvm內存結構了,那什麼是java內存結構呢?

記住了:jvm內存結構=java內存結構

記住java內存模型是與多線程相關,也叫作共享內存模型,如果被問什麼是java內存模型可以這樣回答:

Java內存模型簡稱jmm,它定義了一個線程對另一個線程是可見的,另外就是共享變量的概念,因爲Java內存模型又叫做共享內存模型,也就是多個線程會同時訪問一個變量,這個變量又叫做共享變量,共享變量是存放在主內存中的,而且每一個線程都有自己的本地私有內存,如果有多個線程同時去訪問一個變量的時候,可能出現的情況就是一個線程的本地內存中的數據沒有及時刷新到主內存中,從而出現線程的安全問題。

插播重點

我之前一直不理解的就是這個java內存結構和jvm內存結構到底什麼關係,直到有一天我在一個博客中看到這麼一句話。
在這裏插入圖片描述
也就是說java內存結構和jvm內存結構是一樣的!
在這裏插入圖片描述
所以我們就拿jvm內存結構來說,這個是jvm內存結構圖

從這個圖中來看,這個java內存結構和jvm內存結構也就是我們平常經常說的堆內存,棧啊,方法區什麼的,對,就是這個,以後再說起這個我們就知道是在說java內存結構(jvm內存結構了),那麼我們要了解的也就是什麼是堆內存啊,什麼棧內存啊,什麼又是方法區啊,這個我們今天就不詳細說了,我們今天只要明白什麼是java內存結構也即jvm內存結構是什麼就行了,關於其本身的一些知識,由我們經常聽說這幾個名詞就可知他們是非常重要的知識點,所以這個會單獨拿出來講的!

Java內存模型

下面咱們來說說什麼是java的內存模型,這個和java內存結構從字面意思上看真的很相似,但是實際上,這兩者相差不小,要談java的內存模型,那麼這張圖就是必不可少的。

在這裏插入圖片描述
這就是java內存模型結構圖了,我們從圖中就可以直觀的看到java內存模型是與多線程相關的,其中也提到了共享變量。

Java內存模型簡稱JMM,它定義了一個線程對另一個線程是可見的,另外就是共享變量的概念,因爲Java內存模型又叫做共享內存模型,也就是多個線程會同時訪問一個變量,這個變量又叫做共享變量,共享變量是存放在主內存中的,而且每一個線程都有自己的本地私有內存,如果有多個線程同時去訪問一個變量的時候,可能出現的情況就是一個線程的本地內存中的數據沒有及時刷新到主內存中,從而出現線程的安全問題。在Java當中,共享變量是存放在堆內存中的,而對於局部變量等是不會在線程之間共享的,他們不會有內存可見性問題,當然就不會受到內存模型的影響。

那麼如果我們被別人問到什麼是java內存模型的時候我們該怎麼回答呢?這個你最好把這個圖簡單的畫一下,最不濟也要說下這個java內存模型抽象示意圖,然後就想上面提到的你可以這麼回答:

Java內存模型簡稱jmm,它定義了一個線程對另一個線程是可見的,另外就是共享變量的概念,因爲Java內存模型又叫做共享內存模型,也就是多個線程會同時訪問一個變量,這個變量又叫做共享變量,共享變量是存放在主內存中的,而且每一個線程都有自己的本地私有內存,如果有多個線程同時去訪問一個變量的時候,可能出現的情況就是一個線程的本地內存中的數據沒有及時刷新到主內存中,從而出現線程的安全問題。

接下來我們再來簡單的看下這個java內存模型示意圖:從這張圖我們可以看出,線程之間的通信是受jmm控制的,我們就這張圖來說,線程A和線程B如何才能進行通信,假如線程A先執行,它會拿到共享變量,然後進行操作產生共享變量的副本,緊接着將本地內存中的共享變量副本刷新到主內存中,這時共享變量也就得到的更新,同理線程B再去操作共享變量就達到了線程A和線程B之間的通信了。

基本上到這裏你就知道了什麼是java內存模型了,那其實關於到具體的應用當中,java內存模型還有很多內容,比如重排序,volatile關鍵字和鎖等,這其實也牽涉到多線程了,因爲本身java內存模型就是多線程相關的,所以在學習java多線程這快知識的時候,很多地方都是要藉助這個java內存模型的!

在java中,我個人認爲jvm,多線程以及併發編程,這三者是緊密相連的!我們以後慢慢來說。

在這幾個易混淆的概念中,我覺得最不好理解的一個就是java對象模型,說實話這個java對象模型我問過一些人,基本上都不知道,我個人現在對它理解的也不是很透徹,爲了避免誤導大家,我在網上選取一篇大神的文章供你們參考學習,你們可以看看,這java對象模型是否不容易理解!(以下是個鏈接)

深入理解多線程(二)——Java的對象模型

12、Java中的類加載器

上一次我們簡單說了下java中的類加載,那個知識點我們要記住的就是類的加載過程以及那幾個階段主要是幹啥的,不過在談及類加載的時候一定有一個知識點那就是類加載器。

什麼?你不知道類加載器?那你一定知道ClassLoader或者雙親委派機制吧!

我記得我最先知道雙親委派的時候好像是從面試題中看到的,當時就覺得,什麼玩意,還雙親委派,有點高大上,不知道是什麼,那個時候我更不知道雙親委派是關於類加載的,這些知識點在當時都是知識盲區。

不過隨着學習,積累的知識點不斷變多,也就知道了什麼是雙親委派機制。

雙親委派簡單點來說是類加載器之間的一種模式,或者可以說是規則,也就是他們會按照這個模式去加載類,起了個名字叫做雙親委派,要知道什麼是雙親委派,還要知道什麼是類加載器。

那什麼是類加載器呢?一個類如果被使用的話是會被加載進內存的,但是他們是如何加載進內存的呢?這就需要一個媒介,這個媒介就是類加載器,其實從這個類加載器的名字就顯而易見就是用來加載類的。

簡單知道了類加載器,接下來你還要知道,其實類加載器不止一個,有好幾個,談及類加載器,一定會有這麼一個圖

在這裏插入圖片描述
對,就是這個圖,圖上有三個重要的類加載器,可以這麼說,類加載器也就是這幾個了,當然我們還可以自定義類加載器,不過這個都是後話。

我在圖上也標出來了,對於第一個啓動類加載器,它是使用C加加實現的,而且是屬於虛擬機自身的一部分,但是下面的兩個就不同了,擴展類加載器和應用類加載器都是使用java語言實現的,而且是虛擬機之外,他們倆都是要靠啓動類加載器來加載他們的,用C++實現的就是牛啊。

這裏你還要知道的一個知識點就是關於子類和父類這個概念,這裏可不是繼承的關係,而是組合的關係,另外有這麼一個例子,一起來看一下

public class ClassLoaderTest{
public static void main(String[]args){
ClassLoader loader=Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}

這個例子的意思就是看看應用類加載器的父類是誰,再看看擴展類加載器以及啓動類的父類,結果是這樣的
在這裏插入圖片描述
你會發現,應用類的父類是擴展類,但是擴展類的卻是null,這是爲啥呢?也很簡單啊,因爲啓動類加載器是C加加實現的,擴展類是java嘛,壓根就不是一個品種啊。

到這裏,我們似乎熟悉了類加載器的一些知識,接下來我們繼續。

你還要了解的就是這三個類加載器都是用來加載哪些類的,這個你網上一搜一大把,都是很直白的話,你看看,就是這些

啓動類加載器:這個加載器可以說是頂層的,古老的,厲害的,它主要是用來加載放在JDK\jre\lib下或者被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是無法被Java程序直接引用的。

然後我就去看看了這個目錄下都是些什麼玩意

在這裏插入圖片描述
哦,知道了就是用來加載這些類庫的。再來看這個擴展類加載器

擴展類加載器:這個加載器是由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器。

嗯,說的也比較清晰,這個目錄就不去看了

最後的這個應用類加載器可以說就是我們平常使用的默認加載器,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

到這裏,我們也簡單的瞭解了這三個類加載器,其實在實際的加載中是他們三個互相協作進行,這纔有了接下來我們要說的雙親委派模式。

雙親委派機制

那麼,這就來說說這個雙親委派吧!

什麼是雙親委派機制呢?其實也蠻好理解的,這個類加載器是用來加載類的,而這三個類加載器都可以用來加載類啊,那麼如果要加載一個類的話,誰先來加載呢,通常情況下是這樣的,要加載的這個類首先到達這個類的默認類加載器,也就是應用類加載器,但是呢這個應用類加載器並不會去加載它,而是把這個類扔給他的父類也就是擴展類加載器,而這個擴展類加載器也會把這個類再次丟給啓動類加載器來加載。

現在這個類到達了啓動類加載器,沒辦法,上面沒人了,只能自己加載了,如果啓動類可以加載那就成功返回,實在加不了的話就把這個類原路返回。所以也有可能最後還是得這個應用類加載器來加載。

在這個過程中,我就感覺這個類像是個沒人要的孤兒,而這個什麼雙親委派不就是坑爹嗎?

那麼,這個雙親委派有啥用呢?爲什麼要這樣搞?你還別說,用處蠻大,首先一點就是這樣可以避免重複加載,也就是如果父類加載器已經加載過這個類的話,子類加載器就不需要再次加載了,另外還有非常重要的一點就是安全性,要知道,java的核心api類庫都是被啓動類加載器加載的,如果外部突然要加載一個,一個比如java.lang.xxx的話,這個會被傳到啓動類加載器,啓動類加載器發現這個已經加載過啦,所以不管你,直接返回已經加載的,這樣就有效的防止核心api被篡改。

13、Java內存模型詳細

Java內存模型和Java內存結構的區別

首先要知道Java內存模型是多線程相關的。簡稱JMM,也即是共享內存模型,決定了一個線程對共享變量的寫入時,能對另一個線程可見。

很多線程會使用同一個變量,稱爲共享全局變量,存放在主內存中,

在這裏插入圖片描述
從這張圖我們可以看出,線程之間的通信是受jmm控制的,我們就這張圖來說,線程A和線程B如何才能進行通信,假如線程A先執行,它會拿到共享變量,然後進行操作產生共享變量的副本,緊接着將本地內存中的共享變量副本刷新到主內存中,這時共享變量也就得到的更新,同理線程B再去操作共享變量就達到了線程A和線程B之間的通信了。

總結:什麼是內存模型?

Java內存模型簡稱jmm,它定義了一個線程對另一個線程是可見的,另外就是共享變量的概念,因爲Java內存模型又叫做共享內存模型,也就是多個線程會同時訪問一個變量,這個變量又叫做共享變量,共享變量是存放在主內存中的,而且每一個線程都有自己的本地私有內存,如果有多個線程同時去訪問一個變量的時候,可能出現的情況就是一個線程的本地內存中的數據沒有及時刷新到主內存中,從而出現線程的安全問題。

在Java當中,共享變量是存放在堆內存中的,而對於局部變量等是不會在線程之間共享的,他們不會有內存可見性問題,當然就不會受到內存模型的影響。

Java內存模型–volatile關鍵字

首先我們要知道的就是volatile關鍵字的作用是什麼?volatile的作用是使得變量在多個線程之間可見。
示例講解
我們先創建一個線程

在這裏插入圖片描述
我們通過一個while循環來代表線程一直執行,然後通過isRun方法來控制線程的結束,接下來我們在主線程中這樣操作
在這裏插入圖片描述
大家可以想一下,線程會停止嗎?

實際運行的結果是不會,爲什麼呢?我們結合這張圖來說明一下

在這裏插入圖片描述
這裏的tag就是一個共享變量,首先子線程讀取到的是在子線程中的本地內存中的共享變量副本,我們雖然在主線程中通過isRun方法將tag變成false,但是子線程中讀取到的依然存放在本地內存中的副本

依然是ture,也就是說通過isRun已經將主內存中的共享變量tag刷新成false,但是子線程並沒有在主內存中讀取這個刷新後的值,所以線程不會停止,那麼如何解決這個問題呢?

我們可以這樣做

在這裏插入圖片描述
我們對tag使用volatile關鍵字,這樣的話再次執行這個程序就會發現線程立馬就結束了,這是因爲一旦tag加上vola關鍵字,就強制要求每次使用tag都必須從主內存中取值,因此子線程可以拿到主內存中最新更新的tag也就是false,線程就自然而然的停止了。

原因
線程之間是不可見的,讀取的是副本,沒有即使讀取到主內存結果,解決辦法是使用volatile關鍵字解決線程之間的可見性,強制線程每次讀取該值的時候都去主內存中讀取。

Java內存模型–重排序

認識重排序
首先我們需要對重排序有一個簡單的認識,那麼什麼是重排序呢?

我們從字面意思理解重排序肯定是與順序有關,這裏指的就是程序的執行順序了,我們知道一段代碼的執行會有先後順序,但是也有這種情況就是代碼執行的時候不是按照我們既定的順序進行執行,而是發生了變化。

編譯器和處理器就可能會對程序的執行進行重新排序,這就是重排序了。

我們這裏舉一個例子,我們首先要知道,堆內存是共享內存,可以被多個線程同時訪問,假如現在有一個線程,線程中要執行兩個操作,一個是寫入a的值,另一個是寫入b的值(注意a和b都是共享全局變量,存放在主內存中),而且b的值不依賴a的值,我們在線程中書寫的代碼可能是先寫入a然後再寫入b,但是在實際的運行中,處理器就能夠自由的調整他們的順序,而且b有可能會比a更加快的刷新到主內存中

數據依賴性

以上說的是在一個線程中操作兩個變量,如果是兩個操作同時訪問一個變量,其中一個操作爲寫操作,那麼這兩個操作之間就形成了數據依賴,也就是具有數據依賴性。這裏要明確一點,數據依賴性是形容操作之間的。

我們下面以一個示例來說明一下數據依賴性,首先定義一個user類,其中定義一個變量,如下

在這裏插入圖片描述
接下來我們的重點就在這個age上了,我們創建一個線程A

在這裏插入圖片描述
這裏我們看操作1和操作2,可以看出操作2是依賴於操作1的,因爲tag的值是受操作1影響的,我們就說操作1和操作2之間存在數據依賴性。

要知道我們這裏主要說的是重排序,爲什麼要說數據依賴性呢?因爲編譯器和處理器不會對存在數據依賴關係的操作進行重排序,爲什麼?因爲如果對存在數據依賴性的操作進行重排序的話,程序的執行結果就變了,我們來看下實際操作產生的結果。

我們看線程A應該是程序最開始的樣子,也是我們要的結果,輸出結果tag應該是25,但是如果發生重排序也就是操作2先於操作1執行,那麼會發生什麼情況呢?

在這裏插入圖片描述

很顯然,因爲重排序的原因,tag就成了0.

as-if-serial語義
其實無論怎麼重排序,有一個規則是必須遵守的,那就是單線程程序執行的結果是不能被改變的,這個規則的官方叫法就是as-if-serial語義,編譯器和處理器都是不能違背這個規則的,因此我們要記住這個結論:
編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變程序的執行結果。
重排序對多線程的影響
其實可以明顯的知道重排序對多線程一定會有影響的,我們通過一個簡單的代碼來簡單的說一下。

在這裏插入圖片描述
就比如這段代碼,如果線程A執行writer方法,因爲操作1和操作2之間並沒有存在數據依賴關係,所以這裏有可能發生重排序,可以想到,如果線程B執行reader方法,則一定會受到操作1和操作2重排序的影響。

那麼,如何解決重排序問題就是個重要的話題了!

Java內存模型–鎖(synchronized)

synchronized同步方法
首先看一個線程安全問題(實際代碼模擬一個線程安全問題)

首先我們創建一個實體類

在這裏插入圖片描述
接下來在主線程中開啓兩個線程對實體類中的age進行數值修改
在這裏插入圖片描述
這個時候運行我們的代碼可能會出現這樣的問題
在這裏插入圖片描述
難道b線程中age不應該等於88嗎?怎麼都是66呢?這就出現了線程安全問題。

還可能會出現這樣的情況

在這裏插入圖片描述
這種情況就說明線程的實際執行順序並不一定按照代碼書寫的順序。

而且還會出現這樣的問題

在這裏插入圖片描述
這裏也是發生了線程安全問題,那麼我們該如何解決這個線程安全問題呢?要解決這個問題我們還要明白兩個概念,那就是同步和異步,那什麼是同步什麼又是異步呢?

我們先來簡單分析一下上述代碼爲什麼會出現線程安全問題,其實很簡單,對於age是共享內存,兩個線程可以同時對它進行訪問,當線程a訪問它將它的數值修改成66的時候可能會出現的一種情況就是,線程a剛把age修改成66,線程b又把它修改成88了,導致讀取到的都是88,也就是說線程a修改完成age之後被線程b打斷了一下,沒有及時的去讀取到自己修改的值,而是讀取到了被線程b修改的值。

再想一下爲什麼會出現這種情況呢?其實就是在線程a調用setAge之後,線程b又調用了這個setAge,因此發生線程安全問題,此時這個方法就是異步的,也就是可以被兩個線程同時操作,如果這個setAge被線程a調用期間線程b不能調用,只有等線程a調用並且完成相關操作,線程b才能夠調用,此時這個setAge就是同步的,而且也不會發生線程安全問題了

那麼怎麼實現我們上述所說的呢?

我們可以這樣解決

在這裏插入圖片描述
也就是使用synchronized來修飾我們的setAge,這樣的話當線程a調用setAge的時候就會把這個方法加鎖,此時setAge是被鎖住的,線程b是無法調用的,只有當線程a把setAge操作執行完成之後,鎖纔會被打開。

這就是我們要說的使用synchronized來同步方法

synchronized同步代碼塊
你覺得以上的方法就是最優的了嗎?當然不是,你想一下我們使用synchronized來同步方法也就相當於給這個方法加上一個鎖,不能同時被多個線程訪問,但是如果這個方法中含有耗時操作而這個耗時操作又是不涉及線程安全的,那麼我們使用synchronized來同步方法顯然降低了性能,那該怎麼解決這個問題呢?

解決的一個思路就是隻對引起線程安全的代碼進行synchronized同步,可以這樣做

在這裏插入圖片描述
這就是使用synchronized來同步我們發生線程安全的代碼塊,這裏要注意synchronized需要傳入一個對象,這個對象可以是任意對象,但是要保證這個對象是被多個線程共享的,如果你把這個對象定義在了方法裏,那麼每個線程調用方法都會創建一個新的對象,如此一來,多個線程訪問的就不是同一個對象,因此,依然發生線程安全問題,如下圖代碼操作就是錯誤的。
在這裏插入圖片描述

Java內存模型–final

寫final域的重排序規則
在學習Java內存模型—final的時候是讓我感覺最難的一個,不知道爲什麼,對這一塊就是有點搞不懂,其實對於final它也是禁止指令重排序的,在學這個的時候上網搜索相關文章,好像只有一篇一位叫做程曉明的前輩寫的《深入理解Java內存模型—final》,寫的是比較詳細的,也很感謝這位前輩的分享,在學習的時候,我一直糾結這樣的一句話,就是對於final域,編譯器和處理器要遵守的一個規則。

在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

不知道大家對這句話是否理解,我當初是十分不理解,首先你要知道,什麼是重排序,重排序簡單的說就是在程序執行的時候,編譯器和處理器對程序的執行可能不會按照我們既定的順序去執行,這裏舉一個非常簡單的例子就是在一個主線程中,你寫了這樣的代碼

int a=66;
int b=88;

這段代碼從表面上看應該是線對a進行賦值操作,然後對b在進行賦值操作,但是實際的情況,可能是先對b進行賦值操作,這樣的情況就是發生了重排序,而在某些情況,如果發生了重排序是會出現一些問題的。

我們繼續來看這個規則

在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

爲了敘述方便,我們繼續使用程曉明前輩文章中的代碼,後面回帖出原文地址,首先我們看這樣一段代碼
在這裏插入圖片描述
按照規則所說,意思是不是在寫線程A中的創建FinalExample的時候會出現這樣一種情況,那就是這個對象已經創建完成,可是在構造函數中對i和j的寫操作還沒有執行,這樣的話在線程B執行的讀取操作,獲取到的i和j就都是初始化的值而並不是1和2了,而按照規則說的,對於普通變量也就是int i=1可能會發生這種情況,而對於final變量也就是final int b=2則不會發生這種情況

然後我們再想一下重排序,要知道重排序是指操作與操作之間發生了重排序,那麼這裏爲什麼會出現這樣的重排序呢?是哪些操作發生了重排序呢?

那麼重點就在obj=new FinalExample();這段代碼上了

我們分析下這段代碼,其實它包含兩個操作,如下

  1. 構造一個FinalExample類型的對象;
  2. 把這個對象的引用賦值給引用變量obj。

但是,我就是理解不了,這兩個操作會發生重排序?不大可能,那麼這個重排序到底在哪,既然不在這,那就去其他地方找問題,經過分析,我覺得可能在new FinalExample();上,也就是創建FinalExample對象的時候,這個操作其實也有兩個操作

  1. 創建FinalExample對象
  2. 對i和j賦值

想了又想,覺得能夠發生重排序而且比較合理的只有這兩個操作了,然後我們結合下面一張圖來看會更加容易理解。

在這裏插入圖片描述
也就是說在執行構造函數,也就是創建FinalExample對象的時候,這一步創建對象的操作和構造函數中的給變量賦值的操作可能發生重排序,但是對於final的變量則不會發生重排序,也就是構造函數完成,創建對象成功的同時,final變量的值也成功寫入,但是對於普通變量就有可能出現的情況就是,我構造函數已經執行完成,對象也創建成功了,但是還沒有給普通變量賦值呢,等創建完對象之後在構造函數之外才開始對普通變量進行賦值,也就是對普通變量的賦值重排序到了構造函數之外

以上被稱爲寫final域的重排序,下面還有一個讀final域的重排序,這個我還是有疑問的,下面和大家一起看一下

讀final域的重排序規則
首先對於讀線程B執行的reader方法,就是這些代碼

在這裏插入圖片描述
很顯然,這裏有三個操作

  1. 初次讀引用變量obj;
  2. 初次讀引用變量obj指向對象的普通域j。
  3. 初次讀引用變量obj指向對象的final域i。

對了,我們需要知道讀final域的重排序規則

在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。

以上就是讀final域的重排序規則,什麼意思呢?

我們說過重排序是針對操作之間的,那麼這裏就很明確,發生重排序一定是以下操作

  1. 初次讀引用變量obj;
  2. 初次讀引用變量obj指向對象的普通域i。
  3. 初次讀引用變量obj指向對象的final域j。
    我們知道這裏正確的執行順序應該是先執行1也就是初次讀取引用變量obj,不然的話你接下來的讀值都是錯誤的啊,根據讀final域重排序規則,3的執行必定在1之後,那也就是說對於普通變量,可能出現的情況就是,還沒有讀取obj呢,你就開始讀i了,這肯定是錯誤的,也就是發生這樣的重排序必然出錯。

對於我不理解的地方就是,這兩者怎麼會發生重排序,我們知道在重排序裏面有一個數據依賴性規則,也就是對於存在數據依賴關係的操作,不會發生重排序,那什麼是數據依賴性呢?

數據依賴性就是對於兩個操作,如果一個操作依賴於另一個操作,並i企鵝要提個操作爲寫操作,那麼這兩個操作就存在數據依賴性,這個時候我們在看這兩個操作

  1. 初次讀引用變量obj;
  2. 初次讀引用變量obj指向對象的普通域i。

根據讀final域重排序規則,這兩個操作是會發生重排序,但是你仔細觀察這兩個操作的執行代碼

在這裏插入圖片描述
如果你覺得是在使用obj和i,那這都是讀操作,但是對於FinalExample object=obj;來說,object被賦值不就是寫入操作嗎?而且這兩個操作都跟object有關係,這兩個操作之間難道不存在數據依賴性嗎?

14、Java的垃圾回收

什麼是垃圾回收機制

什麼是java垃圾回收機制啊,這個是不是就是跟java虛擬機有關啊?

Java的垃圾回收機制應該是java虛擬機中很重要的知識點,那麼要學習這塊我們首先要搞清楚的就是,什麼是垃圾回收機制,那麼,什麼是垃圾回收機制呢?

垃圾回收機制我們主要需要理解的就是這個垃圾回收,那麼你就要理解兩點,第一點什麼是垃圾,如何定義這個垃圾,另外一點就是什麼是回收,這個回收是什麼含義。

我們先從第一點說起,那就是垃圾,首先從字面意思理解那就是無用的東西,在java中指的就是那些無任何引用的對象,我們知道一個對象一旦被創建出來就會在內存中分配對應的內存,但是,內存空間是有限的,在一個程序中,我們可能需要創建很多的對象,但是,如果創建的對象很多的情況下就必定會出現一種情況,那就是內存空間不夠用了,怎麼辦,像C語言或者C++中都是需要我們手動的去釋放一些內存的,也就是說,這個對象如果沒有用了,就需要將它銷燬,如此一來它所佔用的內存也就得到了釋放,但是在java中,我們是不需要自己手動的去釋放內存的,垃圾回收機制會主動自動的幫我們去做這件事,那麼,到這裏你就要明白,所謂的垃圾,就是一些用不到的對象,但是還佔着內存空間,這就是垃圾。

那麼,我們再來說回收,要注意,這裏的回收不是回收對象本身,而是對象所佔用的內存,這樣說,你可能還是不太理解,簡單來說就是,你這個對象不用了,但是還佔用着內存,那麼,我就把你銷燬掉,然後你所佔用的內存也就是釋放了出來,這就是回收。

到這裏,我們就可以理解垃圾回收機制就是幫我們自動銷燬無用對象,釋放對象所佔用內存的一種機制,這是java中比較重要的一個特性。

哦哦!你這麼一說,我就明白了,java的垃圾回收機制真好,不用像C或C++那樣需要手動釋放內存了!對了,慶哥,我經常聽到jvm和gc,這是什麼啊?

這個其實很簡單,jvm指的就是java虛擬機,而gc指的就是我們的垃圾回收啦,爲什麼,你看這個

垃圾回收(Garbage Collection)Java虛擬機(JVM)

看到沒,其實就是英文的首字母縮寫!

如何判斷垃圾對象

慶哥:終於問到點子上了,那麼,你覺得gc是如何判斷哪些對象有用,哪些對象是沒用的呢?

小白:我覺得,這應該是某種神奇的算法起的作用,哈哈!

慶哥:可以啊,還能想到算法,其實gc就是通過一些垃圾回收算法來判斷哪些是垃圾對象的!

小白:那是什麼算法啊,趕快說說。

慶哥:其實java語言規範並沒有規定要使用哪一種算法來進行垃圾的回收,而是在不斷的演變過程中,另外這一塊需要分清兩點

判斷垃圾對象的算法回收垃圾對象的算法

以上可以說是垃圾回收算法,我們可以想一下,如果讓你來設置一個算法進行垃圾回收,那麼你該怎樣設計呢?首先你肯定要考慮該怎麼將這些垃圾對象給查找出來,然後就是如何對這些垃圾對象進行回收了。

那麼,我們首先來看兩種經典的判斷垃圾對象的算法

引用計數算法

首先就是jvm早期使用的引用計數算法了,這種算法是如何查找出哪些對象是垃圾對象呢?

在引用計數算法當中,每一個對象都有一個計數器,每當這個對象的引用被使用一次,這個計數器就會自動加1,當然,如果這個對象的引用被重新賦值等操作,這個對象被使用的次數也就減少,說以這個計數器也就減一,當一個對象的計數器爲0的時候也就是說這個對象沒有任何地方被引用,可以判斷爲垃圾對象。

你覺得這種算法好嗎?

小白:這個挺好理解的,而且感覺這個算法很不錯啊,應該沒有什麼不好吧!

慶哥:其實任何一個算法都是好壞並存的,又優點肯定也會有缺點,對於這種引用計數算法的有點就是執行起來非常簡單,而且效率還不低,但是缺點就是對循環引用的檢測不是很好,而且增加了程序執行的開銷。

所以在早期的jvm當中是採用這種算法,但是現在更多的是採用根搜索算法。

根搜索算法

小白:根搜索算法?這個是什麼,感覺不那麼好懂啊!

慶哥:在這種算法當中會以一些列的gc跟對象作爲起始點,然後去搜索先關的引用節點,然後通過這些引用節點再去搜索其下面的引用節點,然後這樣重複,最後搜索的路徑被稱爲引用鏈,如果到最後一個對象的引用並沒有跟任何的一個引用鏈相連接的話,就可以判定這個對象是無用對象,也就是垃圾,我在網上找了一張圖,你可以看看。

在這裏插入圖片描述

圖中的GC ROOTS就代表着gc根對象,藍色的點就代表有用的對象,而灰色的就是垃圾對象了。

小白:有點暈啊,那什麼是gc根對象啊?

慶哥:算法本身就是較爲抽象的東西,所以理解起來有點困難,這個gc根對象很重要,對理解根搜索算法很重要,那什麼是gc根對象呢?

gc根對象包括以下內容(1)虛擬機棧中引用的對象(棧幀中的本地變量表);(2)方法區中的常量引用的對象;(3)方法區中的類靜態屬性引用的對象;(4)本地方法棧中JNI(Native方法)的引用對象。(5)活躍線程。

小白:啊,感覺好難理解啊!腦細胞要死光光了!

慶哥:哈哈,正常,慢慢來,多理解理解,我們繼續講哈!

小白:嗯嗯,繼續吧,讓暴風雨來的更猛烈些吧!

慶哥:那就滿足你,我們以上說了判斷垃圾對象的兩種經典算法,其實最主要的就是根搜索算法,這個跟下面我們要說的回收垃圾算法是有關係的,接下來我們就來說一下這個回收垃圾的幾種經典算法

如何回收垃圾對象

標記—清除算法

這種算法我們從字面意思上就能猜出它是分爲兩個階段的,第一個階段就是標記,什麼意思呢?在標記階段其實就是查找垃圾對象的,而這個標記過程其實就是前面我們說到的根搜索算法,第二個階段就是清除了,當第一階段標記完成,會將所有的垃圾對象統一收集起來,然後清除掉。

那麼這種對象的優缺點呢?

標記—清除算法只標記垃圾對象,所以有用對象較多的情況下極爲高效,但是這種算法在執行的過程中標記和清除效率都不是很高,而且會產生大量的內存碎片。

標記—整理算法

這種算法和標記—清除算法中的標記階段是一樣的,但是在此算法中並不是將所有的垃圾對象收集在一起同意的清除掉,而是將所有的垃圾對象移動到一端,然後直接清除掉端邊界以外的內存。

這種算法的有點在於新對象的分配簡單而且不會產生碎片的問題,但是缺點就在於gc暫停的時間會增長。

複製算法

這種算法將內存容量分成大小相等的兩塊,當這一塊內存使用完畢之後,就把還有用的對象統一複製到另外一塊內存上,然後將這塊內存統一清除掉。

這種算法的運行是很高效的,而且實現簡單也不會有碎片問題,但是缺點也很明顯,因爲內存被劃分爲兩半,所以一次性可分配的內存就減少了一半!

jvm gc機制
要分清垃圾回收和垃圾回收機制,垃圾回收機制也就是GC機制,這個主要講的就是如何對堆內存中的對象進行內存回收的,是一個方式或者方法。

也就是先從哪裏回收,什麼時候回收或者是滿了該怎麼辦。

一個jvm實例只會存在一個堆內存,而且大小可以調節。

GC機制分爲兩類:Minor GC和Full GC

注意,這兩個可以理解爲是一個動作,動作執行之後的結果就是釋放相應的內存空間(將對象轉移,空出該對象佔用的空間以便新對象使用,刪除垃圾對象,釋放空間)

具體的情況是這樣的:
在這裏插入圖片描述

感謝閱讀,PDF獲取

謝謝大家的閱讀,我估計你們沒有看完吧,哈哈,3萬多字呢?我這裏準備了PDF,你們可以去我的公衆號獲取,微信搜索“編碼之外”,關注後回覆“虛擬機”即可獲得PDF,啥?你不知道怎麼關注公衆號,那好吧,加我微信H653836923,我親自給你發,另外大家有啥問題可以在這裏留言討論,大家一起學習哈!

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章