1.JVM體繫結構 JVM的位置 JVM體繫結構 2.類載入器 雙親委派機制 package java.lang; /** * 測試自定義java.lang.String類能否運行成功 * 體會雙親委派機制 * * 類載入器逐級向上檢查:app->ext->boot * 發現boot類載入器中也有S ...
1.JVM體繫結構
JVM的位置
JVM體繫結構
2.類載入器
雙親委派機制
package java.lang;
/**
* 測試自定義java.lang.String類能否運行成功
* 體會雙親委派機制
*
* 類載入器逐級向上檢查:app->ext->boot
* 發現boot類載入器中也有String類,但是沒有main方法,於是報錯
* app:應用程式載入器
* ext:擴展類載入器
* boot:啟動類(根)載入器
*
* 檢查什麼?每一級類載入器能夠載入的類是固定的,不能越級載入。
* boot能載入的類,app,ext就不能載入;同理,exit能載入的,app就不能載入。
* 一個形象的比喻:類,app,ext,boot分別對應平民,村長,鄉長,縣長。
* 村長會向鄉長彙報,鄉長向縣長彙報。如果這個案子很特殊,應該有縣長來處理,那麼村長和鄉長當然不能管了。
*
* 通過驗證“hello”是否會輸出,可以知道:先載入類,再去執行main方法。
* 類是方法的載體,包括main方法,想要用方法,就得先載入類
*/
public class String {
public static void main(String[] args) {
System.out.println("hello"); //這一行會輸出嗎?
String s = "";
System.out.println(s.getClass().getClassLoader());
}
}
運行結果:
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應用程式類必須擴展javafx.application.Application
public class Cat {
/**
* 測試自定義普通類使用哪個類載入器
*
* 從app到boot檢查是否有Cat類,發現ext和boot中都沒有Cat類
* 所有直接還是用app載入器
*/
public static void main(String[] args) {
System.out.println("Hello");
System.out.println(Cat.class.getClassLoader()); //運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
3.沙箱安全機制
4.native
native
是Java的一個關鍵字,使用它可以調用本地方法,訪問本地資源。
Thread類中的一個用法:
private native void start0();
執行到start0()時,start0()進入本地方法棧,然後調用本地方法介面JNI(Java Native Interface),然後調用本地方法庫。
5.方法區
存哪些東西
- static變數
- final變數
- Class對象
- 常量池
實例變數存放在堆中。
面試題:一個實例的創建過程?
- 類載入,Class對象存放在方法區中。
- 父類靜態成員
- 子類靜態成員
- 父類代碼塊
- 父類構造器
- 子類代碼塊
- 子類構造器
- 實例的聲明 Object obj,存放在棧中。
- 實例的創建 new Object(),存放在堆中。
- 將對象實例的地址賦給對象的引用(棧中的變數名指向堆中具體的對象)。obj = new Object();
- 對對象的屬性賦值。
- 調用方法。
6.棧
特點:先進後出,後進先出
為什麼main方法先執行,後結束?
進棧順序:main(),test1(),test2()
出棧順序:test2(),test1(),main()
為什麼遞歸會引起棧溢出?
當調用遞歸出現死迴圈的情況時,棧溢出也就出現了。
測試代碼:
package stack_;
public class TestStack {
public static void main(String[] args) {
test1();
}
static void test1(){
test2();
}
static void test2(){
test1();
}
}
運行結果:
Exception in thread "main" java.lang.StackOverflowError
at stack_.TestStack.test2(TestStack.java:12)
at stack_.TestStack.test1(TestStack.java:8)
……
此處省略1000多行(馬德,typora軟體都乾卡死了)
……
at stack_.TestStack.test2(TestStack.java:12)
at stack_.TestStack.test1(TestStack.java:8)
Process finished with exit code 1
7.堆
堆:heap
堆中的三個區域
- 新生區
- 伊甸園
- 幸存區0
- 幸存區1
- 養老區
- 永久區
新生區
伊甸園:類的創建,應用,甚至消亡。
伊甸園滿了之後,觸發GC,有一部分被銷毀,有一部分進入幸存區。幸存區0,1又會發生數據交換。
老年區
新生區滿了之後,觸發Full GC,進入老年區。
老年區和新生區都滿了,就會發生OOM。
一般不會出現OOM,因為99%的數據都是臨時的,用完就不再使用,在伊甸園或幸存區就被回收了。
永久區
JDK1.6及以前:永久代,常量池位於方法區
JDK1.7:永久代,常量池位於堆
JDK1.8及以後,永久區更名為:元空間,常量池位於元空間
記憶體調優
//虛擬機需要的最大記憶體
long max = Runtime.getRuntime().maxMemory();
//虛擬機初始化時的總記憶體
long original = Runtime.getRuntime().totalMemory();
System.out.println("max:" + max + "Byte," + max / 1024 / 1024 + "MB");
System.out.println("original:" + original + "Byte," + original / 1024 / 1024 + "MB");
/**
* 調優之前:
* max / 電腦記憶體 ≈ 1 / 4
* original / 電腦記憶體 ≈ 1 / 64
*記憶體調優:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
*/
運行結果:
max:1029177344Byte,981MB
original:1029177344Byte,981MB
Heap
PSYoungGen total 305664K, used 20971K
eden space 262144K, 8% used
from space 43520K, 0% used
to space 43520K, 0% used
ParOldGen total 699392K, used 0K
object space 699392K, 0% used
Metaspace used 3282K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 356K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
(305664K+699392K)/ 1024 = 981M
元空間的記憶體邏輯上存在,物理上不存在。
OOM 記憶體溢出
案例演示
package heap_;
import java.util.Random;
/**
* 測試堆記憶體溢出
*/
public class TestOOM {
public static void main(String[] args) {
String s = "hello";
while (true){
s += s + s + new Random().nextInt(999999999);
}
}
}
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at heap_.TestOOM.main(TestOOM.java:9)
Process finished with exit code 1
Java中的字元串可以不停的增加長度,但是JVM中的堆記憶體空間是有限的。
對虛擬機參數進行調整(-Xms8m -Xmx8m -XX:+PrintGCDetails
),並觀察運行結果。
可以看到很快就會運行結束,並報OOM錯誤。
出現OOM,如何解決?
- 記憶體調大一點
- 如果還是有問題,就要研究代碼,是否有bug
使用Jprofiler分析記憶體
Jprofiler安裝教程:https://www.cnblogs.com/zhangxl1016/articles/16220183.html
測試代碼:
package heap_;
import java.util.ArrayList;
public class TestDump {
byte[] arr = new byte[1024 * 1024]; //共1MB的空間
public static void main(String[] args) {
ArrayList<TestDump> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new TestDump());
count++;
}
}
//OOM要用Error來捕獲
catch (Error error) {
System.out.println("count=" + count);
error.printStackTrace();
}
}
}
運行結果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3996.hprof ...
Heap dump file created [7778880 bytes in 0.024 secs]
count=6
java.lang.OutOfMemoryError: Java heap space
at heap_.TestDump.<init>(TestDump.java:6)
at heap_.TestDump.main(TestDump.java:13)
Process finished with exit code 0
併在當前項目的根目錄下生成了Jprofiler文件:
雙擊打開:
可以看到最上邊的列表項(ArrayList)占用的記憶體是最高的,說明是它出了問題。
那麼具體是代碼中的哪一行有問題呢?
因為這個案例只有main方法一個線程,所以直接點main,然後在下方可以看到是源代碼第13行出了問題。
為什麼到count=6時就報錯了呢?因為在第13行每加一個對象,就會增加1MB的空間,我們分配的最大空間是8MB,所以加到6的時候,就會發生記憶體溢出。
記得刪除生成的Jprofiler文件,因為比較占用空間。
虛擬機參數
-Xms
設置初始化記憶體大小 預設1/64
-Xmx
設置最大分配記憶體 預設1/4
-XX:PrintGCDetails
列印GC垃圾回收信息
-XX:HeapDumpOnOutOfMemoryError
轉儲OOM異常信息
上一個案例設置為:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
垃圾回收
哪些東西是垃圾?
不需要的,而且占了空間的對象。
在哪裡回收?
堆
為什麼不在棧回收?因為棧里沒有垃圾,棧不存對象,只存儲變數的引用和方法。
舉個例子:假設堆中存的是具體的人,那麼棧中存的是人的姓名。人去世之後,這個具體的對象依然存在堆中,而且占用空間,所以它需要被回收。棧里存的名字,用完了就自動出棧了,所以棧中不存在垃圾,亦無需回收。
如何回收?
引用計數法
每一個對象都有一個計數器,某個對象被使用一次,相應的計數器就會加1。垃圾回收時,計數器為0的對象被回收。
缺點:當對象很多時,計數器也會占用大量的資源。
複製演算法
主要針對新生區
假設當前伊甸園有兩個對象:o1,o2,GC之後,o1被銷毀,o2進入幸存區。幸存區有兩個,要去哪個呢?哪個是空的,就去哪個。另外一個幸存區中,如果有對象,也會進入to區。然後當前的to區又變成from區,from區又變成to區。一定要保證有一個幸存區是空的,這個區就是to區。
”誰空誰是to“
一個對象在新生區經歷15次(預設次數)還沒有消亡,那麼它會進入老年區,類似於久經沙場的老兵,活到最後就可以養老了。
這裡涉及到一個JVM參數:-XX:MaxTenuringThreshold=15
,預設值是15,如果這個值調到很大,那麼新生區的對象會很難進入到老年區中。
- 優點:沒有記憶體碎片。
- 缺點:浪費空間。
- 總是要保證to區是空的。
- 假設對象100%存活,to區就要面臨無法容納所有對象的情況。to區同時要面臨伊甸園和form區兩個方向的對象。
- 最佳使用場景:對象存活度較低的區域---新生區。
標記壓縮清除演算法
第一次掃描:標記存活的對象,沒被標記的就是不需要的
第二次掃描:清除沒用的對象,會產生碎片
最後一次掃描:被標記的對象向一側移動,另一側就是被清除掉的
優點:不會增加額外的空間
缺點:掃描會浪費時間,會有記憶體碎片產生
優化:先進行幾次標記清除,最後再統一壓縮
演算法比較
-
記憶體效率:複製演算法>標記清除>標記壓縮(時間複雜度)
- 複製是一個動作,清除是兩個動作,壓縮有三個動作
-
記憶體整齊度:複製演算法=標記壓縮>標記清除
- 複製和壓縮都沒有記憶體碎片
-
記憶體利用率:標記清除=標記壓縮>複製
- 清除和壓縮都在原有空間中操作,複製演算法總是要保證to區是空的
有沒有最優演算法呢?
沒有,只有最合適的:分代收集演算法
新生代:存活率低,適合複製演算法。不用擔心to區的空間不夠。
老年代:區域大,存活率高,適合標記清除+標記壓縮混合實現。碎片不是很多的時候用標記清除,碎片積累到一定程度,就需要壓縮,這其中就要涉及到記憶體調優。