JVM你了解?

1.談談你對JAVA的理解

  • 平台無關性(一次編譯,到處運行)
  • GC(不必手動釋放堆記憶體)
  • 語言特性(泛型、lambda)
  • 面向對象(繼承,封裝,多態)
  • 類庫
  • 異常處理

2.平台無關性怎麼實現

0
Java源碼首先被編譯成位元組碼,再由不同平台的JVM解析,Java語言在不同的平台上運行時不需要進行重新編譯,Java虛擬機在執行位元組碼的時候,把位元組碼轉換成具體平台上的機器指令。

補充:為什麼JVM不直接將源碼解析成機器碼去執行?

如果直接解析成機器碼去執行,那麼每次執行,都需要重新做語法分析等操作。
  • 準備工作:每次執行都需要各種檢查
  • 兼容性:也可以將別的語言解析成位元組碼

3.Java虛擬機

一種抽象化的電腦,通過在實際的電腦上模擬模擬各種電腦功能來實現的。Java虛擬機有自己完善的硬體架構,如處理器、堆棧、暫存器等,還具有相應的指令系統。虛擬機屏蔽了與具體作業系統平台相關的資訊,使得Java程式只需生成在Java虛擬機上運行的目標程式碼(位元組碼),就可以在多種平台上不加修改地運行。

0
JVM主要由Class Loader、Runtime Data Area、Execution Engine以及Native Interface這四個部分組成。
它主要通過Class Loader將指定格式的class文件載入到記憶體,並通過Execution Engine去解析class文件里的位元組碼並提交給作業系統去執行。 Class Loader:依據特定格式,載入class文件到記憶體
Execution Engine:對命令進行解析
Native Interface:融合不同開發語言的原生庫為Java所用
Runtime Data Area:JVM記憶體空間結構模型

4.反射

JAVA反射機制是在運行狀態中,
對於任意一個類,都能夠知道這個類的所有屬性和方法;
對於任意一個對象,都能夠調用它的任意一個方法和屬性; 這種動態獲取資訊以及動態調用對象的方法的功能稱為java語言的反射機制。

補充:

public Method[] getDeclareMethods():返回Class對象表示的類或介面的所有的成員方法對象數組,包括pubic,protected,default和private的方法對象,不包括從父類繼承的方法
public Methods[] getMethods(): 返回Class對象表示的類或介面的所有公有成員方法對象數組,包括已聲明的、從父類繼承,實現介面的方法
反射的例子:
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)雙親委派機制

0

當類載入器收到類載入請求時,會去判斷是否載入過,如果載入過,直接返回,否則,就委派給它上級,層層這樣去判斷,到達最底層後,還是沒載入過,就去判斷是否可以被載入到,可以就返回,否則,就層層往下判斷能否載入到,如果最終還是載入不到就跑出異常。

作用

避免多份同樣位元組碼載入,載入過了就不會再載入,重新載入它一定是不同的。

(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的可定址範圍
0

(2)地址空間的劃分:

內核空間:是主要的作業系統程式和C運行時的空間,包含用於連接電腦硬體、調度程式、以及提供聯網和虛擬記憶體服務的邏輯,和基於C的進程;

用戶空間:java進程實際運行時使用的記憶體空間(32位系統用戶進程最多可以訪問3GB,內核程式碼可以訪問所有物理記憶體;而64位系統用戶進程最多可以訪問512GB,內核程式碼也可以訪問所有物理記憶體)

 

0

(3)JVM記憶體模型——JDK8

執行緒私有:程式計數器、虛擬機棧、本地方法棧
執行緒共享:元空間(MetaSpace)、Java堆
0

1)程式計數器(邏輯計數器,而非物理計數器)

  • 當前執行緒所執行的位元組碼行號指示器(邏輯)
  • 改變計數器的值來選取下一條需要執行的位元組碼指令
  • 和執行緒是一對一的關係即「執行緒私有」
  • 對Java方法計數,如果是Native方法則計數器值為Undefined
  • 不會發生記憶體泄露

2)java虛擬機棧(stack)

  • Java方法執行的記憶體模型
  • 包含多個棧幀(棧幀包含:局部變數表、操作棧、動態連接、返回地址)
0

3)局部變數表和操作數棧

  • 局部變數表:包含方法執行過程中的所有變數
  • 操作數棧:入棧、出棧、複製、交換、產生消費變數
執行add(1,2)的過程
public static int add(int a, int b){
    int c = 0;
    c = a + b;
    return c;
}
0
遞歸為什麼會引發java.lang.StackOverflowError異常
①原因:遞歸過深,棧幀數超出虛擬棧深度

  當執行緒執行一個方法時,就會隨之創建一個棧幀,並將棧幀壓入虛擬機棧,當方法執行完後,便會將棧幀出棧,因此可知,執行緒當前執行的方法所對應的棧幀位於棧的頂部,而我們的遞歸函數不斷去調用自身,
每一次方法調用會涉及以下操作:

第一:每新調用一個方法,就會生成一個棧幀;

第二:它會保存當前方法棧幀的狀態,將它放入虛擬機棧中;

第三:棧幀上下文切換的時候,會切換到最新的方法棧幀當中,而由於我們虛擬機棧深度是固定的,遞歸實現將導致棧的深度增加;如果棧幀數超過了最大深度,就會拋出java.lang.StackOverflowError異常。

②解決方法
  • 循環方法替代
  • 限制遞歸次數

3)本地方法棧

  • 與虛擬機棧相似,主要作用於標註了native的方法

4)元空間和永久代

①區別
元空間使用本地記憶體,永久代使用的是JVM記憶體
②MetaSpace相比PermGen的優勢
jdk1.8之後,jvm移除了永久代,使用元空間。
  • 字元串常量池存在永久代中,容易出現性能問題和記憶體溢出
  • 類和方法的資訊大小難以確定,給永久代的大小指定帶來困難
  • 永久代會為GC帶來不必要的複雜性
  • 方便HotSpot與其他JVM如Jrockit的集成

5)Java堆

  • 對象實例的分配區域
  • GC管理的主要區域
0
①JVM三大性能調優參數-Xms -Xmx -Xss的含義
  • -Xms:堆的初始值(該進程剛創建出來的時候,它的專屬Java堆的大小。一旦對象容量超過Java堆的初始容量,Java堆將會自動擴容,最大擴容大小的-Xmx)
  • -Xmx:堆能達到的最大值(在很多情況下,-Xms和-Xmx設置成一樣的。這麼設置,是因為當Heap不夠用時,會發生記憶體抖動,影響程式運行穩定性)
  • -Xss:規定了每個執行緒虛擬機棧(堆棧)的大小(一般256k足夠,此配置會影響此進程中並發執行緒數的大小)
②記憶體分配策略
程式運行時,有三種記憶體分配策略:靜態的、棧式的、堆式的。
  • 靜態存儲:編譯時確定每個數據目標在運行時的存儲空間需求(因而在程式編譯時就可以給它們分配固定的記憶體空間。這種分配策略要求程式程式碼中不允許有可變數據結構的存在,也不允許有嵌套或者遞歸的結構出現,因為他們都會導致編譯程式無法計算準確的存儲空間)
  • 棧式存儲:數據區需求在編譯時未知,運行時模組入口前確定(動態的存儲分配,由一個堆棧的運行棧實現的。規定在進入一個程式模組的時候,必須知道這個程式模組所需要的記憶體大小。按照先進後出的原則進行分配)
  • 堆式存儲:編譯時或運行時模組入口都無法確定,動態分配(比如可變長度串以及對象實例,堆由大片的可利用塊或空閑塊組成,堆中的記憶體可以按照任意順序分配和釋放)
③聯繫:創建的數據和對象實例都保存在堆中,想要引用對象、數組時,可以在棧里定義變數保存堆中目標的首地址
0
④堆和棧的區別:
  • 管理方式:棧自動釋放,堆需要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();
    }
}
綜上述程式碼分析:各部分的存儲情況

元空間

Class:HelloWorld – Method:sayHello\setName\main – Field:name
Class:System 類對象、成員變數和方法

Java堆

Object:String(「forlan」) 實例
Object:HelloWorld

執行緒獨佔

Parameter reference:「forlan」 to String object 地址引用
Variable reference:「hw」 to HelloWorld object 地址引用
Local Variables:a with 1 值,lineNo 行號

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 
}
Jdk6
s創建的時候,首先在常量池中創建「a」,然後在heap中創建了string對象。當使用intern時,企圖將string對象放到常量池中,但是常量池已經有a了,所以放置失敗。定義s2時,直接使用了常量池中的a,所以兩個地址不一樣。 創建s3時,在heap中創建string對象,使用intern函數,將對象的副本放置到常量池中,s4引用常量池中的aa,但是由於是string對象的副本,所以兩個的地址不同
Jdk6+
s創建的時候,首先在常量池中創建「a」,然後在heap中創建了string對象。當使用intern時,企圖將string對象放到常量池中,但是常量池已經有a了,所以放置失敗。定義s2時,直接使用了常量池中的a,所以兩個地址不一樣。
創建s3時,在heap中創建string對象,使用intern函數,將對象的引用放置到常量池中,s4引用常量池中的aa,由於是string對象的引用,所以兩個的地址相同