自己對JVM的一點理解

JVM應該是運行在操作系統之上的,和軟件並沒有直接的交互
在我的理解中,JVM是這麼組成的。
首先是一個類加載器,它的作用就是加載一個class文件。
舉個例子,比如現在有一個Student的類。它是抽象的,但是如果我們把它new一下,它就變成一個具體的實例了。
它的對象實例化過程是這樣的:
首先是一個Student.class文件,然後它進入到了類加載器中。類加載器通過加載並實例化把他變成了一個類的模板Student Class。這就是反射的class對象。如果想要用Student的反射對象,就需要用Student.class,合格時候會返回一個Class,也就是會變成這樣:
ClassstudentClass = Studnet.class;
這個Class就是個模板反射對象,全局唯一。也就是說無論new上多少個Studnet都是這一個Class。
然後會進行實例化,實例出來幾個Student實例。這些實例的名字是在棧裏面的,但是他們具體的引用實際上是在堆裏的。
這裏其實還涉及到了一個機制,叫做雙親委派機制
雙親委派機制其實在我看來很好理解。首先要知道一點,就是我們是有好幾個類加載器的,他們是有父子關係的。最底下的是應用加載器,它的父類是擴展類加載器,擴展類加載器的父類的根加載器。
如果說我們需要加載一個類,首先把這個請求讓根加載器去完成,它輩分最大嘛。如果說根加載器說,我不知道這玩意,加載不了,那就給他的子類擴展類加載器去完成,以此類推。
舉個例子,比如我們自己建一個java.lang包,在包底下弄一個String的類去加載,可以加載出來,但是加載出來的不是我們自己寫出來的那個String。這是因爲本來就是java.lang.String,這個在根加載器那裏就加載出來了。我們自己寫出來的java.lang.String需要到應用加載器才能加載出來,但事實上根本就輪不到他。
說完了類加載器,再往下走其實就是我們的運行時數據區。這裏面分成了好幾個部分,方法區,java棧,本地方法棧,堆以及程序計數器。
首先說程序計數器吧。程序計數器其實就是一個很小的內存空間,他的作用就是計數,然後幫助完成循環,跳轉之類的基礎功能。
然後是棧,棧有一個規則叫做先進後出,後進先出,其實也可以把他理解爲一個桶或者說是以前上學時候交作業。最早交的作業會被壓在最底下,也就最後一個被老師批改到。桶也是一個道理。
和他有點像的是隊列。隊列就是桶把底打通了,和管道一樣,所以是先進先出的,就相當於排隊,你先來排隊所以先輪到你。
棧呢,有幾個特點:
1、他的讀取數據是比較快的,起碼比堆要快,然後他的數據是可以共享的
2、他是負責程序的運行,因爲裏面一般放的都是方法,比如mian方法,然後main方法在調用一個test()方法壓在他上頭。
3、棧的生命週期是和線程同步的。線程創建的時候他創建,線程結束了他也就釋放
4、所以說對於棧來說完全沒有垃圾回收的問題。線程在跑你不能把人家東西當垃圾扔了吧,人家線程跑完了自己就扔了不用你幫忙。因爲一般來說垃圾回收,也就是GC,基本都在堆內存。
5、方法如果自己調用自己,就是遞歸嘛,你不能寫死循環。因爲棧這個桶就這麼大,你把他放滿了後面的就沒地方放了,就會報一個StackOverflowError異常,看到這個異常就說明,我們的棧滿了
所以說我們程序正在執行的方法,一定是在棧的頂部。
棧的組成元素就是棧幀。每執行一個方法就會產生一個棧幀,保存到棧頂。如果說這個方法執行完了,那麼就會自動把這個棧幀出棧。
說完了棧說一下方法區吧。
方法區是所有線程共享的。簡單來說就是方法區裏面保存了所有定義的方法的信息,這個區域是一個共享區間。
方法區裏儲存的東西其實很好記。一個static,靜態變量,一個final,常量,一個Class,類信息,還有一個運行時常量池。
常量池分爲三個,一個字符串常量池在堆裏,還有兩個分別爲靜態常量池和運行時常量池,都在方法區裏。jdk8後有了元空間,元空間是方法區的一種落地實現,可以理解爲方法區在元空間中。而元空間又是邏輯上存在於堆裏,物理上存在於本地內存中的。
然後說說堆吧。
堆在JDK7之前都是分爲新生區,養老區和永久區的,JDK7開始逐漸取消永久代,JDK8正式取消永久代,改爲元空間。
新生區分爲三個區,伊甸園區和兩個倖存者區。
首先是伊甸園區,所有的類都是在伊甸園區被new出來的。不斷地創建對象,伊甸園區總是會滿的,這個時候再需要創建對象就需要進行GC垃圾回收,在這裏有一個GC算法叫做複製算法。
複製算法是這樣的。不是有兩個倖存區嘛,空的那個被稱爲倖存區to區,另外一個被稱爲倖存區from區。然後清除伊甸園區裏面不用的垃圾,把倖存下來的,也就是說以後還要用的,這些對象,以及之前from區裏面的東西都複製轉移到to區裏面,這個時候伊甸園區和之前的from區都空了嘛,伊甸園區就可以繼續new了,而之前的to區就變成了from區,之前的from區就變成了to區。
但是這個算法就有一個弊端,就是他會浪費你的內存空間,因爲他始終有一個空的to區是在待命嘛。不過也有好處,就是他不會出現內存碎片。
如果說一個對象經過了一定數量的GC仍然存在,那麼他就會進入到養老區。養老區很好理解,就是不怎麼常用的,但是還在用的。因爲基本上99%的對象都是臨時對象,在新生區就被幹掉了,所以能到養老區的不會很多 。
養老區也會有GC垃圾回收,一般稱之前的GC是minor GC,也就是輕GC,養老區進行的一般都是重GC,也就是full GC。
一般用的GC算法是標記清除算法和標記壓縮算法結合的方式。
什麼是標記清除算法呢?標記清除算法就是對存活的對象進行標記,然後回收不需要的對象。舉個例子,就像最近的健康碼,一人一碼,然後不是綠碼的統統回收。但是這樣就有個問題,確實和複製算法比較起來,不佔用內存空間了,但是又多了內存碎片了,而且因爲要掃描兩次,耗時會比比較舊,效率很低。
這個時候就是標記壓縮算法了。標記壓縮算法就是在標記清除算法的基礎上進行壓縮,這樣就沒有內存碎片了。當然一般是先標記清除幾次再進行壓縮,這樣效率會更高。
就內存效率而言,複製算法>標記清除算法>標記壓縮算法
就內存整齊度而言,複製算法=標記壓縮算法>標記清除算法
就內存利用率而言,標記清除算法=標記壓縮算法>複製算法
還有一個是永久代,這個概念其實現在已經沒有了。這是一個JDK7以前的概念,永久代其實就是方法區的一個概念實現,現在取而代之的是元空間。三個常量池,靜態常量池和運行時常量池都在元空間中,字符串常量池在堆中。而且元空間在邏輯上是在堆中的,只不過物理上是在本地內存中。因爲默認情況下,元空間的大小隻受本地內存的限制。
最後是堆內存調優。
默認情況下,分配的總內存,是電腦內存的1/4,而初始化內存是1/64。
如果說需要進行調優,那麼就可以用過輸入vm參數來實現
-Xms設置初始化內存分配大小 -Xmx設置最大分配內存大小 -XX:+PrintGCDetails

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