java基本功之JVM:那些不需要死記硬背的jvm基本原理

前言

對基本原理的瞭解,動手是最好的;

哪裡入手

例子

1package com.java.study.jvm; 2 3/** 4 * @author zhangpeng 5 * @since 2020/1/15 3:33 下午 6 */ 7public class JvmHello { 8 public static final int i = 2020; 910 public static void main(String[] args) {11 JvmHello jvmHello = new JvmHello();12 int a = 1;13 int b = 2;14 int c = jvmHello.calculate1(a, b);15 int d = jvmHello.calculate2(a, b);16 }1718 private int calculate2(int a, int b) {19 int x = 666;20 return x / (a + b);21 }2223 private int calculate1(int a, int b) {24 return (a + b) * 2333;25 }26}

這段代碼我就不解釋了 直接編譯字節碼搞起

1# 編譯生成 JvmHello.class文件2javac JvmHello.java3# 反編譯字節碼內容4javap -verbose -p JvmHello.class復記得之前書裡提到的,編譯一次到處執行,那麼首先文件要被加載進來,運行在一個環境裡面;所以我們有了初步的圖

java基本功之JVM:那些不需要死記硬背的jvm基本原理

JvmHello.java -> JvmHello.class -> 類裝載系統加載進來 -> 在虛擬機環境執行

接著我們看下JVMHello.class的內容

1Classfile /Users/zhangpeng/workspacke/mytest/study/src/main/java/com/java/study/jvm/JvmHello.class 2 Last modified 2020-1-15; size 530 bytes 3 MD5 checksum d1725552383bf6c86a00f1517d2b4c51 4 Compiled from "JvmHello.java" 5public class com.java.study.jvm.JvmHello 6 minor version: 0 7 major version: 52 8 flags: ACC_PUBLIC, ACC_SUPER 9Constant pool: 10 #1 = Methodref #6.#22 // java/lang/Object."<init>":()V 11 #2 = Class #23 // com/java/study/jvm/JvmHello 12 #3 = Methodref #2.#22 // com/java/study/jvm/JvmHello."<init>":()V 13 #4 = Methodref #2.#24 // com/java/study/jvm/JvmHello.calculate1:(II)I 14 #5 = Methodref #2.#25 // com/java/study/jvm/JvmHello.calculate2:(II)I 15 #6 = Class #26 // java/lang/Object 16 #7 = Utf8 i 17 #8 = Utf8 I 18 #9 = Utf8 ConstantValue 19 #10 = Integer 2020 20 #11 = Utf8 <init> 21 #12 = Utf8 ()V 22 #13 = Utf8 Code 23 #14 = Utf8 LineNumberTable 24 #15 = Utf8 main 25 #16 = Utf8 ([Ljava/lang/String;)V 26 #17 = Utf8 calculate2 27 #18 = Utf8 (II)I 28 #19 = Utf8 calculate1 29 #20 = Utf8 SourceFile 30 #21 = Utf8 JvmHello.java 31 #22 = NameAndType #11:#12 // "<init>":()V 32 #23 = Utf8 com/java/study/jvm/JvmHello 33 #24 = NameAndType #19:#18 // calculate1:(II)I 34 #25 = NameAndType #17:#18 // calculate2:(II)I 35 #26 = Utf8 java/lang/Object 36{ 37 public static final int i; 38 descriptor: I 39 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 40 ConstantValue: int 2020 41 42 public com.java.study.jvm.JvmHello(); 43 descriptor: ()V 44 flags: ACC_PUBLIC 45 Code: 46 stack=1, locals=1, args_size=1 47 0: aload_0 48 1: invokespecial #1 // Method java/lang/Object."<init>":()V 49 4: return 50 LineNumberTable: 51 line 7: 0 52 53 public static void main(java.lang.String[]); 54 descriptor: ([Ljava/lang/String;)V 55 flags: ACC_PUBLIC, ACC_STATIC 56 Code: 57 stack=3, locals=6, args_size=1 58 0: new #2 // class com/java/study/jvm/JvmHello 59 3: dup 60 4: invokespecial #3 // Method "<init>":()V 61 7: astore_1 62 8: iconst_1 63 9: istore_2 64 10: iconst_2 65 11: istore_3 66 12: aload_1 67 13: iload_2 68 14: iload_3 69 15: invokespecial #4 // Method calculate1:(II)I 70 18: istore 4 71 20: aload_1 72 21: iload_2 73 22: iload_3 74 23: invokespecial #5 // Method calculate2:(II)I 75 26: istore 5 76 28: return 77 LineNumberTable: 78 line 11: 0 79 line 12: 8 80 line 13: 10 81 line 14: 12 82 line 15: 20 83 line 16: 28 84 85 private int calculate2(int, int); 86 descriptor: (II)I 87 flags: ACC_PRIVATE 88 Code: 89 stack=3, locals=4, args_size=3 90 0: sipush 666 91 3: istore_3 92 4: iload_3 93 5: iload_1 94 6: iload_2 95 7: iadd 96 8: idiv 97 9: ireturn 98 LineNumberTable: 99 line 19: 0100 line 20: 4101102 private int calculate1(int, int);103 descriptor: (II)I104 flags: ACC_PRIVATE105 Code:106 stack=2, locals=3, args_size=3107 0: iload_1108 1: iload_2109 2: iadd110 3: sipush 2333111 6: imul112 7: ireturn113 LineNumberTable:114 line 24: 0115}116SourceFile: "JvmHello.java"/<init>/<init>/<init>/<init>/<init>/<init>

字節碼分析

1-8行

描述了類的基本信息

  • 它是由哪個 *.java 文件編譯而成的
  • 最後編譯時間
  • 編譯後的大小
  • MD5校驗值
  • 遵循的java版本
  • 訪問標識,ACC_PUBLIC字面意思公有的嘛;ACC_SUPER不清楚是什麼,但是應該和super方法有關係

9-35行 Constant pool

運行時常量池

我們先分析下第一個常量,位於JVMHello.class第10行,我們會發現後面有關聯項 一起放進來

1 #1 = Methodref #6.#22 // java/lang/Object."<init>":()V2 #6 = Class #26 // java/lang/Object3 #11 = Utf8 <init>4 #12 = Utf8 ()V5 #22 = NameAndType #11:#12 // "<init>":()V6 #26 = Utf8 java/lang/Object/<init>/<init>/<init>

Methodref表示方法定義,右側的註釋內容(表示是由這幾行組合起來的)

1java/lang/Object."<init>":()V複製代碼/<init>

這段可以理解為該類的實例父類構造器的聲明,此處也說明了JvmHello類的直接父類是Object.該方法默認返回值是V,也就是void,無返回值

同理分析下第二個常量,位於JVMHello.class第12行

1 #2 = Class #23 // com/java/study/jvm/JvmHello2 #3 = Methodref #2.#22 // com/java/study/jvm/JvmHello."<init>":()V3 #11 = Utf8 <init>4 #12 = Utf8 ()V5 #22 = NameAndType #11:#12 // "<init>":()V6 #23 = Utf8 com/java/study/jvm/JvmHello/<init>/<init>/<init>

這裡描述的是默認的構造器JvmHello(),因為後面在main()方法裡面new了對象 所以這裡會初始化到常量池

同理分析下第三個常量,位於JVMHello.class第13行

1 #2 = Class #23 // com/java/study/jvm/JvmHello 2 #4 = Methodref #2.#24 // com/java/study/jvm/JvmHello.calculate1:(II)I3 #18 = Utf8 (II)I4 #19 = Utf8 calculate15 #24 = NameAndType #19:#18 // calculate1:(II)I

這裡描述的是JvmHello類裡面calculate1方法的定義

1 com/java/study/jvm/JvmHello.calculate1:(II)I複製代碼

(II) 表示入參為兩個基本類型int

(II)I

右邊的這個I表示返回值也是基本類型int

連起來說就是 calculate1方法入參是兩個int,返回值是int

那麼同理可得 位於JVMHello.class第14行的變量表示的是 calculate2方法入參也是兩個int,返回值也是int

上述就是運行時常量池信息的分析,常量池用於存放編譯期生成的各種字面量和符號引用,常量池是被劃分在了方法區這個裡面,方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據到這我們補充一下我們的jvm圖


java基本功之JVM:那些不需要死記硬背的jvm基本原理

36-115行 類內部方法描述

方法表集合

36-41行 靜態常量i的定義

先看下靜態常量的定義,位於JVMHello.class

1 public static final int i;2 descriptor: I3 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL4 ConstantValue: int 2020

  • 聲明瞭一個公有變量i,類型為int
  • 返回值為int
  • 訪問標識公共的、靜態的、最終的
  • 常量值為2020

42-52行 類的構造器定義

1 public com.java.study.jvm.JvmHello(); 2 descriptor: ()V 3 flags: ACC_PUBLIC 4 Code: 5 stack=1, locals=1, args_size=1 6 0: aload_0 7 1: invokespecial #1 // Method java/lang/Object."<init>":()V 8 4: return 9 LineNumberTable:10 line 7: 0/<init>

  • ()V 參考之前方法的定義描述 這裡是空參的方法;V表示特殊類型void無返回值
  • ACC_PUBLIC 訪問標識公共的
  • stack 最大操作數棧 JVM會根據這個值來分配幀棧的操作棧深度,這裡是1
  • locals 局部變量所需存儲空間,單位Slot,1Slot=4B,那麼這裡就是4個字節
  • args_size方法參數的個數,這裡是1,因為每個實例方法都會有一個隱藏參數this
  • aload_0 當中的0正是局部變量表裡的Slot 0的含義。意思是將局部變量表裡的Slot 0的東西壓入操作數棧,這個Slot 0裡的東西name正是this,也就是JvmHello的實例
  • invokespecial #1invokespecial表示根據編譯時類型來調用實例方法 #1表示執行 常量池裡面定義的實例方法,即JvmHello();
  • return 從方法中返回,返回值為void
  • LineNumberTable 該屬性的作用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關係

這裡我們產生了另一個概念,方法執行會進行壓棧出棧

53-84行

main方法分析

1 public static void main(java.lang.String[]);// main方法 2 descriptor: ([Ljava/lang/String;)V // 入參String[],出參V(void) 3 flags: ACC_PUBLIC, ACC_STATIC // 公共的、靜態的 4 Code: 5 stack=3, locals=6, args_size=1 // 操作數棧3,局部變量6 Slot,參數個數為1 6 0: new #2 // class com/java/study/jvm/JvmHello new對象 7 3: dup // 複製棧頂部一個字長內容 8 4: invokespecial #3 // Method "<init>":()V 執行JvmHello構造器 9 7: astore_1 // 將returnAddress類型(引用類型)存入到局部變量[1]10 8: iconst_1 // 將int類型常量[1]壓入到操作數棧11 9: istore_2 // 將int類型值存入局部變量[2]12 10: iconst_2 // 將int類型常量[2]壓入到操作數棧13 11: istore_3 // 將int類型值存入局部變量[3]14 12: aload_1 // 從局部變量[1]中裝載引用類型值15 13: iload_2 // 從局部變量[2]中裝載int類型值 16 14: iload_3 // 從局部變量[3]中裝載int類型值 17 15: invokespecial #4 // Method calculate1:(II)I 執行calculate1方法18 18: istore 4 // 將int類型值存入局部變量[4]19 20: aload_1 // 從局部變量[1]中裝載引用類型值20 21: iload_2 // 從局部變量[2]中裝載int類型值 21 22: iload_3 // 從局部變量[3]中裝載int類型值 22 23: invokespecial #5 // Method calculate2:(II)I 執行calculate2方法23 26: istore 5 // 將int類型值存入局部變量[5]24 28: return // void返回25 LineNumberTable: 26 line 11: 027 line 12: 828 line 13: 1029 line 14: 1230 line 15: 2031 line 16: 28/<init>

從第一行new對象說起

1 JvmHello jvmHello = new JvmHello();2 // 這裡的jvmHello就是局部變量[1];

那麼new出來的對象放在哪裡的,看過jvm相關內容的同學都知道對象是分配在裡面的

關於

的定義

對於大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裡分配內存。這一點在Java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換 優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了。

接著看下面兩行

1 int a = 1;2 int b = 2;3 // 1.這裡先將常量[1] = 1壓入到操作數棧4 // 2.再將整個常量[1]的int類型的值賦值給 局部變量[2]也就是 a = 1;5 // 同理 b=2也是同樣的過程

然後看執行calculate1、calculate2方法

1int c = jvmHello.calculate1(a, b);2int d = jvmHello.calculate2(a, b);3// 1.從局部變量[1]中裝載引用類型值 即jvmHello的值4// 2.從局部變量[2]中裝載int類型值 即值為25// 3.從局部變量[3]中裝載int類型值 即值為26// 4.使用jvmHello執行calculate1方法7// 同理 calculate2執行過程類似

上面這段我們知道,jvm在執行代碼的時候,是基於的執行,也就是操作棧

每個棧裡面有局部變量,局部變量是分配在局部變量表裡面

關於java棧的定義,他有兩個棧:java虛擬機棧和本地方法棧

Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame )用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務

既然虛擬機棧裡面提到線程,那麼這裡順便介紹下程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裡(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

知道了,繼續補充一下我們的圖

java基本功之JVM:那些不需要死記硬背的jvm基本原理

我們繼續分析下calculate1和calculate2

1private int calculate2(int, int); 2 descriptor: (II)I // 入參2個int類型 出參int類型 3 flags: ACC_PRIVATE 4 Code: 5 stack=3, locals=4, args_size=3 // 操作數棧3,局部變量4 Slot,參數個數3個 6 0: sipush 666 // 將16位帶符號整數(這裡指666)壓入棧 7 3: istore_3 // 將int類型值(即666)存入局部變量[3] 8 4: iload_3 // 從局部變量[3]中裝載int類型值 9 5: iload_1 // 從局部變量[1]中裝載int類型值10 6: iload_2 // 從局部變量[3]中裝載int類型值11 7: iadd // 執行int類型的加法,即 1+212 8: idiv // 執行int類型的除法,即 666/313 9: ireturn // 返回int類型的值14 LineNumberTable:15 line 19: 016 line 20: 41718 private int calculate1(int, int); 19 descriptor: (II)I // 入參2個int類型 出參int類型20 flags: ACC_PRIVATE // 私有的21 Code: 22 stack=2, locals=3, args_size=3 // 操作數棧2,局部變量3 Slot,參數個數3個23 0: iload_1 // 從局部變量[1]中裝載int類型值 24 1: iload_2 // 從局部變量[2]中裝載int類型值25 2: iadd // 執行int類型的加法,即 1+226 3: sipush 2333 // 將16位帶符號整數(這裡指2333)壓入棧27 6: imul // 執行int類型的乘法 3*233328 7: ireturn // 返回int類型的值29 LineNumberTable:30 line 24: 0

其實到這裡我有個疑問 為什麼calculate1和calculate2的入參明明只有2個,反編譯後會顯示2個呢?我去搜了下

原來在計算args_size時,有判斷方法是否為static方法,如果不是static方法,則會在方法原有參數數量上再加一,這是因為非static方法會添加一個默認參數到參數列表首位:方法的真正執行者,即方法所屬類的實例對象。那對應我們這多出來的參數就是 jvmHello了

最後關於操作棧的過程 這裡我以calculate1為例

java基本功之JVM:那些不需要死記硬背的jvm基本原理

上面提到的虛擬機棧的概念也提過,方法執行的同時會創建棧幀,存儲局部變量表、操作數棧、動態鏈接、方法出口;所以上圖就是一個棧幀在虛擬機中入棧到出棧的過程.基於這點最後補充一下棧裡面的信息內容

java基本功之JVM:那些不需要死記硬背的jvm基本原理

116行

表示源文件JvmHello.java

技術總結

通過分析字節碼,可以加深對虛擬機內存結構,java代碼從編譯到加載,和運行的整個過程,而不是去死記書裡的那些概念。

最後小編還準備了400集學習視頻教程一起分享給大家、希望大家早日精通拿高薪!

獲取方式:

只需轉發➕關注小編、再來私信小編關鍵詞“學習”即可免費領取啦!


分享到:


相關文章: