JVM你了解?
1.談談你對JAVA的理解
- 平台無關性(一次編譯,到處運行)
- GC(不必手動釋放堆記憶體)
- 語言特性(泛型、lambda)
- 面向對象(繼承,封裝,多態)
- 類庫
- 異常處理
2.平台無關性怎麼實現
補充:為什麼JVM不直接將源碼解析成機器碼去執行?
- 準備工作:每次執行都需要各種檢查
- 兼容性:也可以將別的語言解析成位元組碼
3.Java虛擬機
一種抽象化的電腦,通過在實際的電腦上模擬模擬各種電腦功能來實現的。Java虛擬機有自己完善的硬體架構,如處理器、堆棧、暫存器等,還具有相應的指令系統。虛擬機屏蔽了與具體作業系統平台相關的資訊,使得Java程式只需生成在Java虛擬機上運行的目標程式碼(位元組碼),就可以在多種平台上不加修改地運行。
4.反射
補充:
package com.interview.javabasic.reflect; public class Robot { private String name; public void sayHi(String helloSentence){ System.out.println(helloSentence + " " + name); } private String throwHello(String tag){ return "Hello " + tag; } }
package com.interview.javabasic.reflect; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ReflectSample { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { Class rc = Class.forName("com.interview.javabasic.reflect.Robot"); Robot r = (Robot) rc.newInstance(); System.out.println("Class name is " + rc.getName()); Method getHello = rc.getDeclaredMethod("throwHello", String.class); getHello.setAccessible(true); // 調用私有方法需要加,不然會報錯Exception in thread "main" java.lang.IllegalAccessException Object str = getHello.invoke(r, "Bob"); System.out.println("getHello result is " + str); Method sayHi = rc.getMethod("sayHi", String.class); sayHi.invoke(r, "Welcome"); Field name = rc.getDeclaredField("name"); name.setAccessible(true); name.set(r, "Alice"); sayHi.invoke(r, "Welcome"); } }
5.談談classloader
(1)類從編譯到執行的過程
- 編譯器將Forlan.java 源文件編譯為Forlan.class 位元組碼文件
- ClassLoader將位元組碼轉換為JVM中的 Class<Forlan>對象
- JVM利用Class<Forlan>對象實例化為Forlan對象
ClassLoader在java中有著非常重要的作用,Java的核心組件中所有class都是由ClassLoader進行載入的.它主要工作在class裝載的載入階段,其主要作用是從外部系統獲得class二進位數據流,通過將class文件里的二進位數據流裝進系統,然後交給java虛擬機進行連接,初始化等操作。
種類
- BootStrapClassLoader:C++ 編寫,載入核心庫java.*
- ExtClassLoader:Java編寫,載入擴展庫javax.*(不是一次性載入,用到才載入)
- AppClassLoader:Java編寫,載入程式所在目錄(class.path)
- 自定義ClassLoader:Java編寫訂製載入
(2)自定義ClassLoader的實現
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError{ return defineClass(null, b, off, len, null); }
具體實現
public class MyClassLoader extends ClassLoader{ /** 路徑 */ private String path; /** 名稱 */ private String classLoaderName; /** 全參構造函數 */ public MyClassLoader(String path, String classLoaderName) { this.path = path; this.classLoaderName = classLoaderName; } @Override public Class findClass(String name){ byte[] b = loadClassData(name); return defineClass(name,b,0,b.length); } /** 用於載入類文件 */ private byte[] loadClassData(String name) { /** 全路徑1 */ name = path + name + ".class"; /** jdk1.8新特性,不用手動關閉 */ try( InputStream in = new FileInputStream(new File(name)); ByteArrayOutputStream out = new ByteArrayOutputStream();) { int i = 0; while ((i = in.read()) != -1){ out.write(i); } return out.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } } //檢測類 public class ClassLoaderChecker{ public static void main(String[] args){ MyClassLoader m=new MyClassLoader(path:"Users/baidu/Desktop/",classLoaderName:"myClassLoader"); Class c=m.loadClass(name:"Wali"); System.out.println(c.getClassLoader());//結果是MyClassLoader System.out.println(c.getClassLoader().getParent());//結果是AppClassLoader System.out.println(c.getClassLoader());//結果是ExtClassLoader System.out.println(c.getClassLoader());//結果是null c.newInstance(); }
(3)雙親委派機制
當類載入器收到類載入請求時,會去判斷是否載入過,如果載入過,直接返回,否則,就委派給它上級,層層這樣去判斷,到達最底層後,還是沒載入過,就去判斷是否可以被載入到,可以就返回,否則,就層層往下判斷能否載入到,如果最終還是載入不到就跑出異常。
作用
避免多份同樣位元組碼載入,載入過了就不會再載入,重新載入它一定是不同的。
(4)類的載入方式
隱式載入:new
顯示載入:Classloder.loadClass,Class.forName等獲取Class對象,再通過newInstance方法獲取對象
(5)類的裝載過程
主要有三個步驟:裝載(Load),鏈接(Link)和初始化(Initialize)。
載入:通過ClassLoader載入class文件位元組碼,生成Class對象
鏈接:
校驗:檢查載入的class的正確性和安全性
準備:為類變數分配存儲空間並設置類變數初始值
解析:JVM將常量池內的符號引用轉換為直接引用
初始化:執行類變數賦值和靜態程式碼塊
(6)loadClass和forname區別
forname得到的class已經完成初始化
loadclass只是載入,並沒有鏈接和初始化
- JDBC中載入資料庫驅動用到Class.forName(「com.mysql.jdbc.Driver」),Driver中有靜態程式碼塊,所以需要用到forName()
- Spring IOC資源載入器獲取資源(即讀取配置文件時),用到class.getClassLoader(),是為了加快初始化速度,延遲載入
(7)類什麼時候才被初始化
- new對象
- 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
- 調用類的靜態方法
- 反射——Class.forName方法
- 初始化子類,會首先初始化父類)
- JVM啟動時標明的啟動類,即文件名和類名相同的那個類
6.Java記憶體模型
(1)記憶體相關(了解)
簡介:
電腦所有程式都是在記憶體中運行的,只不過這個記憶體可能包括虛擬記憶體,同時也離不開硬碟這樣的外存知識;在程式執行的過程中,需要不斷地將記憶體的邏輯地址和物理地址映射起來,找到相關的指令以及數據去執行;作為作業系統進程,java運行時面臨著和其他進程相同的記憶體限制,即受限於作業系統架構提供的可定址空間。
可定址空間由處理器的位數決定:
- 32位處理器:2^32的可定址範圍(4GB)
- 64位處理器:2^64的可定址範圍
(2)地址空間的劃分:
內核空間:是主要的作業系統程式和C運行時的空間,包含用於連接電腦硬體、調度程式、以及提供聯網和虛擬記憶體服務的邏輯,和基於C的進程;
用戶空間:java進程實際運行時使用的記憶體空間(32位系統用戶進程最多可以訪問3GB,內核程式碼可以訪問所有物理記憶體;而64位系統用戶進程最多可以訪問512GB,內核程式碼也可以訪問所有物理記憶體)
(3)JVM記憶體模型——JDK8
1)程式計數器(邏輯計數器,而非物理計數器)
- 當前執行緒所執行的位元組碼行號指示器(邏輯)
- 改變計數器的值來選取下一條需要執行的位元組碼指令
- 和執行緒是一對一的關係即「執行緒私有」
- 對Java方法計數,如果是Native方法則計數器值為Undefined
- 不會發生記憶體泄露
2)java虛擬機棧(stack)
- Java方法執行的記憶體模型
- 包含多個棧幀(棧幀包含:局部變數表、操作棧、動態連接、返回地址)
3)局部變數表和操作數棧
- 局部變數表:包含方法執行過程中的所有變數
- 操作數棧:入棧、出棧、複製、交換、產生消費變數
執行add(1,2)的過程
public static int add(int a, int b){ int c = 0; c = a + b; return c; }
遞歸為什麼會引發java.lang.StackOverflowError異常
①原因:遞歸過深,棧幀數超出虛擬棧深度
當執行緒執行一個方法時,就會隨之創建一個棧幀,並將棧幀壓入虛擬機棧,當方法執行完後,便會將棧幀出棧,因此可知,執行緒當前執行的方法所對應的棧幀位於棧的頂部,而我們的遞歸函數不斷去調用自身,
每一次方法調用會涉及以下操作:
第一:每新調用一個方法,就會生成一個棧幀;
第二:它會保存當前方法棧幀的狀態,將它放入虛擬機棧中;
第三:棧幀上下文切換的時候,會切換到最新的方法棧幀當中,而由於我們虛擬機棧深度是固定的,遞歸實現將導致棧的深度增加;如果棧幀數超過了最大深度,就會拋出java.lang.StackOverflowError異常。
②解決方法
- 循環方法替代
- 限制遞歸次數
3)本地方法棧
- 與虛擬機棧相似,主要作用於標註了native的方法
4)元空間和永久代
①區別
②MetaSpace相比PermGen的優勢
- 字元串常量池存在永久代中,容易出現性能問題和記憶體溢出
- 類和方法的資訊大小難以確定,給永久代的大小指定帶來困難
- 永久代會為GC帶來不必要的複雜性
- 方便HotSpot與其他JVM如Jrockit的集成
5)Java堆
- 對象實例的分配區域
- GC管理的主要區域
①JVM三大性能調優參數-Xms -Xmx -Xss的含義
- -Xms:堆的初始值(該進程剛創建出來的時候,它的專屬Java堆的大小。一旦對象容量超過Java堆的初始容量,Java堆將會自動擴容,最大擴容大小的-Xmx)
- -Xmx:堆能達到的最大值(在很多情況下,-Xms和-Xmx設置成一樣的。這麼設置,是因為當Heap不夠用時,會發生記憶體抖動,影響程式運行穩定性)
- -Xss:規定了每個執行緒虛擬機棧(堆棧)的大小(一般256k足夠,此配置會影響此進程中並發執行緒數的大小)
②記憶體分配策略
- 靜態存儲:編譯時確定每個數據目標在運行時的存儲空間需求(因而在程式編譯時就可以給它們分配固定的記憶體空間。這種分配策略要求程式程式碼中不允許有可變數據結構的存在,也不允許有嵌套或者遞歸的結構出現,因為他們都會導致編譯程式無法計算準確的存儲空間)
- 棧式存儲:數據區需求在編譯時未知,運行時模組入口前確定(動態的存儲分配,由一個堆棧的運行棧實現的。規定在進入一個程式模組的時候,必須知道這個程式模組所需要的記憶體大小。按照先進後出的原則進行分配)
- 堆式存儲:編譯時或運行時模組入口都無法確定,動態分配(比如可變長度串以及對象實例,堆由大片的可利用塊或空閑塊組成,堆中的記憶體可以按照任意順序分配和釋放)
③聯繫:創建的數據和對象實例都保存在堆中,想要引用對象、數組時,可以在棧里定義變數保存堆中目標的首地址
④堆和棧的區別:
- 管理方式:棧自動釋放,堆需要GC(JVM可以針對記憶體棧進行管理操作,而且該記憶體的釋放是編譯器就可以操作的內容)
- 空間大小:棧比堆小(由本身存儲的數據特性決定)
- 碎片相關:棧產生的碎片遠小於堆(對於堆空間而言,即使垃圾回收器可以進行自動堆記憶體回收,但是堆空間的活動量相對於棧空間而言比較大,很有可能存在長時間的堆空間分配和釋放操作。而且垃圾回收器不是實時的,它有可能使得堆空間的記憶體碎片,逐漸累積起來。針對棧空間而言,因為本身就是一個堆棧的數據結構,操作都是一一對應的,而且每一個最小單位的結構棧幀和堆空間中複雜的記憶體結構不一樣,所以在使用過程很少出現記憶體碎片)
- 分配方式:棧支援靜態和動態分配,而堆僅支援動態分配
- 效率:棧的效率比堆高(因為記憶體塊本身的排列就是一個典型的堆棧結構,所以棧空間的效率自然比起堆空間要高很多,而且電腦底層記憶體空間本身就使用了最基礎的堆棧結構使得棧空間和底層結構更加符合,它的操作也變得簡單,就是最簡單的兩個指令:入棧和出棧;棧空間相對堆空間而言的弱點是靈活程度不夠,特別是在動態管理的時候。而堆空間最大的優勢在於動態分配,因為它在電腦底層實現可能是一個雙向鏈表結構,所以它在管理的時候操作比棧空間複雜很多,自然它的靈活度就高了,但是這樣的設計也使得堆空間的效率不如棧空間,而且低很多)
7.元空間、堆、棧獨佔部份間的聯繫——記憶體角度
public class HelloWorld{ private String name; public void sayHello(){ System.out.println("Hello "+name); } public void setName(String name){ this.name = name; } public static void main(String[] args){ int a = 1; HelloWorld hw = new HelloWorld(); hw.setName("forlan"); hw.sayHello(); } }
元空間
Java堆
執行緒獨佔
8.不同JDK版本之間的intern()方法的區別——JDK6 VS JDK6+
String s = new Stirng("a"); s.intern();
JDK6:當調用 intern 方法時,如果字元串常量池先前已創建出該字元串對象,則返回池中的該字元串的引用。否則,將此字元串對象添加到字元串常量池中,並且返回該字元串的引用。
JDK6+:當調用 intern 方法時,如果字元串常量池先前已創建出該字元串對象,則返回池中的該字元串的引用。否則,如果該字元串對象已經存在於Java堆中,則將堆中此對象的引用添加到字元串常量池中,並且返回該引用;如果堆中不存在,則在池中創建該字元串並返回其引用。
註:在JDK1.6的時候,字元串常量池是存放在Perm Space中的(Perm Space和堆是相隔而開的),在1.6+的時候,移到了堆記憶體中。
public static void main(String[] args) { String s1 = new String("a"); s1.intern(); String s2 = "a"; System.out.println(s1 == s2); //Jdk6:false Jdk6+:false String s3 = new String("a") + new String("a"); s3.intern(); // 不加都是false String s4 = "aa"; System.out.println(s3 == s4); //Jdk6:false Jdk6+:true }