JVM-java虛擬機


在JVM中一般會遇到以下倆個概念:

1.JVM 內存區域劃分
物理的區域劃分
2.JVM 內存模型(JMM)
表示多線程的邏輯結構,工作內存與主內存的關係。
所以一般當提到JVM內存怎麼劃分,一定要搞清楚要問的是哪一個。

1.內存區域劃分

1.線程私有內存

  • 程序計數器
    記錄當前線程正在執行的字節碼行號指示器,簡單來說就是當前線程跑到哪行代碼了,這樣子當線程重複調度的時候,就可以知道從哪開始恢復執行。
  • 虛擬機棧
    方法的執行模型,每個方法的調用以及執行完畢對應的棧幀在棧中的入棧和出棧。
    -Xss:設置棧的深度。
  • 本地方法棧
    本地方法棧就是本地方法,虛擬機棧是java方法。

2.線程共享內存


  • 所有的數組元素和對象
    -Xms:設置堆的最小值
    -Xmx:設置堆的最大值
    -Xmn:設置新生代內存大小
  • 方法區
    已加載的類信息以及常量和靜態變量。
    已加載類信息:類的權限,屬性,方法信息。
  • 運行時常量池
    方法區的一部分,存符號引用和自變量。
    例:
class Person{
   private String name;
   private Integer age;
   public void print(){} 
}
當Person per = new Person();後
per和倆個屬性的值都放在堆中。
類的權限,屬性和方法的信息都放在方法區。

10, “abc”----在方法區

符號引用:包名.類名  定位一個類的信息

2.垃圾回收GC*****

1.如何判斷對象是否存活。(知道哪些對象要被回收)
2.如果把對象認爲不再存活,標記位需要回收後,還有一個緩刑階段(Object類的finalize()方法)。
3.如果對象通過finalize()方法判定爲徹底不再存活,要進行回收。
4.如何回收:GC算法。

如何判斷對象是否存活:

1.引用計數
給每個對象加一個引用計數器,當有一個地方引用這個對象,引用計數器+1,當不再引用,也就是引用等於null,該對象計數器-1。任意時刻當計數器等於0,判定這個對象不再存活。
無法解決循環引用問題。循環引用:我中有你你中有我。
test類中有一個Object屬性,Object又指了test2,test2中的Object又指向test1。
2.可達性分析算法–Java
通過一系列稱爲GC Roots的對象開始向下尋找,若從GC Roots到對象有路可走,把這個路叫引用列,認爲這個對象存活。無路可走就認爲不存活。
有路可走,無路可走

哪些對象可以作爲GC Roots:4個
1.本地方法棧、虛擬機棧中的變量。–》局部變量
2.類中的常量和靜態變量。
類中的成員變量一定不是GC Roots,所以循環引用無效。

JDK1.2 後關於引用的擴充。很重要!

  • 強引用
    Person per = new Person()。
    –效果:只要對象被任意一個強引用指向,無論是否發生內存溢出異常(OOM),都不能回收被強引用指向的對象。

  • 軟引用
    –JDK1.2後一個類–SoftReference類描述軟引用。
    —語義:若對象只被軟引用指向,當內存夠用時不回收此對象,當內存不夠用時(即將拋出OOM異常)時,會回收所有僅被軟引用指向的對象。
    –軟引用對象爲有用但不必須對象。(如緩存對象:自動登錄)。
    —如何實現一個線程安全緩存(Map)?
    ①HashMap+ReentrantReadWriteLock。
    ②使用ThreadLocal(線程的本地變量,線程隔離)

  • 弱引用:
    WeakReference類描述弱引用。
    當對象那個只被弱引用指向,當下次DC開始時,無論內存是否夠用,都會回收掉只被弱引用指向的對象。

  • 虛引用
    —PhatomReference類描述虛引用。虛引用不對生存週期產生任何影響,並且也無法通過虛引用取得一個對象。
    —作用:被虛引用指向的對象,在進行GC之前會收到一個回收信息。需要知道類的對象有沒有被回收,就給對象打上虛引用,當垃圾回收開始的時候,如果對象被回收了,JVM會發一個回調信息,告訴你這個對象被回收了。
    ----用到哪裏:調優。想知道那個對象被回收了。
    ---- 輕度依次遞減。-----

對象的緩刑階段–finalize()

final finally finalize區別
final:終接器,常量,不可變類,無法被覆寫的方法。
finally:用在異常體系中,保證finally代碼塊中的語句一定被執行。
考點:帶不帶return語句,return在哪
finalize():用在JVM垃圾回收的對象是否存活裏面。
語義:
當一個對象被標記爲不可達時,GC線程在回收時需要看:
1.若此對象所在的類沒有覆寫finalize()方法,認爲此對象不再存活,可以回收。
2.若此對象所在的類覆寫了finalize方法()
2.1且未被JVM調用,由JVM調用finalize()方法,若在finalize()中與任何一個GC Roots掛鉤,就認爲此對象存活下來了。
2.2已被JVM調用過,認爲此對象不再存活。

GC算法–分代收集

堆分爲新生代和老年代。
新生代:對象存活率非常低。
老年代:對象存活率較高。

分代收集:新生代採用複製算法,老年代採用標記-整理方法。
1.新生代
java新的複製算法:將新生代分爲一塊較大的Eden區和倆塊較小的Survivor區,每次使用Eden和其中一塊Survivor。Eden:Survivor=8:1:1(有倆個Survivor)
與原來的複製算法相比:利用率由原來的百分之50到現在的百分之90。
複製算法步驟
新生代的倆塊Survivor區一個稱爲From區,另一塊稱爲To區。
step1:當Eden區第一次滿時,將Eden區的存活對象複製到From區,清空From區。
step2:當Eden區第二次滿時,將Eden區和From區的存活對象複製到To區(對象存放是挨着的),一次清空Eden區與From區。
…這是一個來回重複的動作。
若干對象會在From與To區來回複製,默認複製了15次的時候,會將此對象移動到老年代。

2.老年代的標記-清除
老年代的清除:先把存活對象按一端移動,然後把存活空間以外的空間幹掉,避免空間碎片。
爲何老年代不採用複製算法:老年代對象存活率較高,如果採用複製算法,大部分對象都要複製,開銷很大效率較低。

MinorGC:發生在新生代的垃圾算法,採用複製算法,效率較高,發生頻率較高。
FullGC(Major GC):發生在老年代的垃圾回收,採用標記-整理算法,速度一般比MinorGC滿10倍以上,發生頻率較低。一般來說,發生FullGC至少會伴隨一次MinorGC。
老年代什麼時候得回收:Eden存活對象空間太大,無法放入From/To區,會直接進入到老年代,老年代滿了後就必須回收。

JDK內置的JVM工具

Linux下如何查看進程id?
PID+grep
jsp -l:可以輸出包名.類名。
1.jps:使用頻率最高。
查看當前操作系統下所有JVM進程,返回進程ID。
2.jstack
查看指定JVM進程的線程堆棧情況(一般多線程卡死使用此工具定位問題)
jstack pid -l
3.jmap
查看指定JVM的內存情況。

使用以上三個工具分析JVM運行情況。

volatile

  • 保證變量的可見性。
    —可見性:synchronized(Lock)、final、volatile
    —先寫後讀
  • 內存屏障。
    –CPU指令重排:
int i = 1int y = 2;
boolean flag = true;
i = i+3;
y=y+4;
第三行代碼與其他代碼無關,JVM可能會推遲或提前第三行代碼的執行。
1.加了volatile後就可保證第三行代碼不可能被提前延後執行。
2.當CPU運行到第三行代碼,可以保證前面的代碼一定執行完畢,且結果可以被後續可見,並且之後的代碼還未開始執行。內存屏障僅僅保證第三行禁止重排,其他四行不可保證:有可能先21,先54.

Double-Check-Singleton:雙重單例加鎖模式。

1.單例:
一個類的對象有且只有一個。
構造方法私有化。
唯一一個對象在類的內部,被私有化,提供一個返回對象的方法。

餓漢單例:上來就new。
懶漢單例:用時就new。線程不安全,如何安全:new的時候加鎖,然後在鎖內部再次判斷對象是否爲null。

懶漢式單例缺陷:,SINGLETON有可能被初次化了倆次甚至多次。
假設現在有三個線程同時調用getInstance(),他們三個都同時走到了判斷對象是否爲null這一步,那麼SINGLETON至少被new了三次,因爲走到這一步的時候這三個線程看到的SINGLETON都爲null,所以至少new了三次,所以三個線程拿到的SINGLETON都不是一個。
**如何保證new是一個線程安全?**在new的那塊加鎖synchronized(類名.class),這就是所謂的一重加鎖,在這裏一定可以保證對象new了一次。但是他有一個問題:如果此時線程1拿到鎖進去了,線程2和3卡在了獲取鎖外面,線程1new完之後,線程2和3恢復執行的地方在這個鎖之外,當鎖被釋放的時候,線程2和3還是會進去,因此需要在拿到鎖後再進行對象是否爲空的判斷,此時就可以保證不管有多少個線程只產生了一個對象。
Double-Check:爲什麼檢查倆次null?
第一個:保證此時確實是第一次調用。
第二個:保證多線程場景下即便加鎖也保證new只會執行一次。
此時,Double-Check檢查了倆個但是隻加了一次鎖,另外一次鎖在對象的volatile關鍵字上。
那麼爲什麼要加volatile關鍵字?
假設此時線程1拿到了sychronized鎖,正在進行初始化操作。此時線程2準備調用getInstance()方法,想取得一個SINGLETON對象。那麼,由於new操作不是一個原子性操作,new有三個操作:(SINGLETON = new Single())
a.在堆上開空間。
b.SINGLETON 指向堆空間。
c.構造方法完成屬性初始化。
當執行第二步以後,SINGLETON已經不等於null,假設線程1此時走到了第2步。線程2看到SINGLETON已經不等於null了,然後返回了SINGLETON,但是此時返回的SINGLETON可能還未用構造方法初始化,拿到了一個不完整的對象。那麼用volatile保證我拿到的單例一定是初始化之後的。在return SINGLETON上有一個屏障,走到這行代碼,一定能保證前面的所有代碼都已經執行完。
第二層鎖保證我拿到的單例對象一定是屬性全部初始化過後的單例對象。
倆次加鎖:
synchronized:保證線程安全,new的時候只有一個線程在操作。
volatile:保證不會指令重排,return的SINGLETON一定是所有屬性全部初始化完成的單例對象。

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