不可錯過的JVM深度好文-純乾貨詳解JVM!-JVM概述&內存結構1

1 JVM概述

1.1 前言

本文爲本人的JVM的學習記錄,適合有一定的java編程基礎( J2SE)並希望進一步理解java的程序員,虛擬機愛好者,jvm實踐者。

大多數java開發工程師,都是處於使用框架階段,使用java api開發系統,很少有關注java底層核心技術jvm,對其瞭解的很少。如果我們把java核心類庫的API比做數學公式的話,那麼java虛擬機jvm的知識就好比公式的推導過程
在這裏插入圖片描述
關於學習資料推薦:

  • 《深入Java虛擬機》
  • 《深入理解Java虛擬機JVM高級特性與最佳實踐》
  • 《Java虛擬機規範》

關於官方文檔:

https://docs.oracle.com/javase/specs/index.html

1.2 JVM簡介

1.2.1 虛擬機是什麼?

所謂虛擬機(Virtual Machine),就是一臺虛擬的計算機。它是一款軟件,用來執行一系列虛擬計算機指令。大體上,虛擬機可以分爲系統虛擬機程序虛擬機

  • 大名鼎鼎的Visula Box,VMware就屬於系統虛擬機,它們完全是對物理計算機的仿真,提供了一 個可運行完整操作系統的軟件平臺。
  • 程序虛擬機的典型代表就是java虛擬機,它專⻔爲執行單個計算機程序而設計,在java虛擬機中執行的指令我們稱之爲java字節碼指令。

無論是系統虛擬機還是程序虛擬機,在上面運行的軟件都被限制於虛擬機提供的資源中。

1.2.2 Java虛擬機

java技術的核心就是java虛擬機**(JVM,Java Virtual Machine)**,因爲所有的java程序都運行在java虛擬機內部。

優勢

跨平臺性、優秀的垃圾回收器,以及可靠的即時編譯器。

作用

java虛擬機就是二進制字節碼的運行環境,負責裝載字節碼到其內部,解釋/編譯爲對應平臺上的機器指令執行。每一條java指令,java虛擬機規範中都有詳細定義,如怎麼取操作數,怎麼處理操作數, 處理結果存儲在哪裏。

特點

  1. 一次編譯,到處運行
  2. 自動內存管理(內存分配)
  3. 自動垃圾回收功能(內存回收)

1.2.3 JVM的位置

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
JVM是運行在操作系統之上的,它與硬件沒有直接的交互。

1.2.4 一次編譯,到處運行

在這裏插入圖片描述

1.2.5 JVM跨語言的平臺

在這裏插入圖片描述

隨着java7的正式發佈,java虛擬機的設計者們通過JSR-292規範基本實現:在java虛擬機平臺上運行非java語言編寫的程序。

java虛擬機根本不關心運行在其內部的程序是使用何種編程語言編寫的,它只關心“字節碼”文件。也就是java虛擬機擁有語言無關性,並不會單純地與java語言“終身綁定”,只要其他編程語言的編譯結果滿足幷包含java虛擬機的內部指令集、符號表以及其他的輔助信息,它就是一個有效的字節碼文件,就能被虛擬機所識別並裝載運行。

多語言混合編程

java平臺上的多語言混合編程逐漸稱爲主流,通過特定領域的語言去解決特定領域的問題是當前軟件開發應對日趨複雜的項目需求的一個方向。

試想一下,在一個項目之中,並行處理用Clojure語言編寫,展示層使用JRuby/Rails,中間層是java,每個應用層都將使用不同的編程語言來完成,而且,接口對每一層的對於開發者都是透明的,各種語言之間的交互不存在任何困難,就使用自己語言的原生API一樣方便,因爲他們最終都運行在一個虛擬機上。

對這些運行與java虛擬機之上、java之外的語言,來自系統級的、底層的支持正在迅速增強,以 JSR-292爲核心的一系列項目和功能改進(如Davinci Machine項目、Nashorn引擎),推動java虛擬機 從**"java語言的虛擬機""多語言虛擬機’’**的方向發展。

1.2.6 字節碼

javac Demo.java 
//javac:命令將其編程成字節碼文件
//java:命令來執行class字節碼文件
javap -v Demo
//javap:是將字節碼進行反編譯(與javac對應),可以查看java編譯器爲我們生成的字節碼。
package com.nyf;

public class Demo {
    private int a = 1;
    public void testMethod(){
        System.out.println("testMethod");
    }
}

運行出來大概是這樣:

~/IdeaProjects/day30                                                                                                                                                        ⍉
▶ cd src/com/nyf/

src/com/nyf                                                                                                                                                                  
▶ javac Demo.java

src/com/nyf                                                                                                                                                                  
▶ javap -v Demo
警告: 文件 ./Demo.class 不包含類 Demo
Classfile /Users/monologuist/IdeaProjects/day30/src/com/nyf/Demo.class
  Last modified 2020年6月16日; size 423 bytes
  SHA-256 checksum fd90b5ec57cf96746d17b0ca80967ccb1b9db4fae909b8cf9a76cbfed216744c
  Compiled from "Demo.java"
public class com.nyf.Demo
  minor version: 0
  major version: 57
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // com/nyf/Demo
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/nyf/Demo.a:I
   #8 = Class              #10            // com/nyf/Demo
   #9 = NameAndType        #11:#12        // a:I
  #10 = Utf8               com/nyf/Demo
  #11 = Utf8               a
  #12 = Utf8               I
  #13 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Class              #16            // java/lang/System
  #15 = NameAndType        #17:#18        // out:Ljava/io/PrintStream;
  #16 = Utf8               java/lang/System
  #17 = Utf8               out
  #18 = Utf8               Ljava/io/PrintStream;
  #19 = String             #20            // testMethod
  #20 = Utf8               testMethod
  #21 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #22 = Class              #24            // java/io/PrintStream
  #23 = NameAndType        #25:#26        // println:(Ljava/lang/String;)V
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (Ljava/lang/String;)V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               SourceFile
  #30 = Utf8               Demo.java
{
  public com.nyf.Demo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #7                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4

  public void testMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #19                 // String testMethod
         5: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}
SourceFile: "Demo.java"

我們平時說的java字節碼指的是用java語言編譯成的字節碼。準確的說任何能在jvm平臺上執行的字節碼格式都是一樣的。所以應該統稱爲: jvm字節碼

不同的編譯器,可以編譯出相同的字節碼文件,字節碼文件也可以在不同的jvm上運行。

java虛擬機與java語言並沒有必然的聯繫,它只與特定的二進制文件格式-class文件格式所關聯, class文件中包含了java虛擬機指令集(或者稱爲字節碼,Bytecodes)和符號表,還有一些其他輔助信息。

字節碼和機器碼的區別

在這裏插入圖片描述

高級語言:最接近人類語言,但機器是無法執行的,需要最終編譯度連接成二進制的機器代碼纔可被計 算機執行

彙編語言:是將二進制的機器碼通過抄助記符的方式讓人可以更方便的編寫並檢查的低級語言,彙編語言接近機器語言,可以看做是機襲器語言的另一種形式,計算機在運行時也需要將其變爲機器語言的二進制纔可運行

字節碼:是一種中間狀態(中間碼)的二進制代碼(文件),需要直譯器轉譯後才能成爲機器碼。 機器語言:是計算機可以識別並運行的二進制代碼,機器碼是電腦CPU直接讀取運行的機器指令,運行速度最快,但是非常晦澀難懂,也比較難編寫,一般從業人員接觸不到。

1.2.7 HotSpot VM

Oracle/Sun JDK 中使用的 JVMHotSpot VM.

提起HotSpot VM,相信所有Java程序員都知道,它是Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用範圍最廣的Java虛擬機。

➜ ~ java -version
 java version "1.8.0_121"
 Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
 Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
  • SUN 的 JDK 版本從 1.3.1 開始採用 HotSpot 虛擬機, 並於 2006 年底開源, 主要使用C++實現, JNI 接 口部分用C實現.
  • HotSpot 是較新的 JVM, 用來替代 JIT (Just in Time, 即時編譯), 可以大大提高 Java 的運行性能, 即: Java 起初是把源代碼編譯爲 .class 格式的字節碼在虛擬機上執行, 速度較慢;
  • HotSpot 將常用的部分代碼編譯爲本地(native)代碼, 顯著提高了性能。
  • 其他的VM: JRockit VM,IBM J9 VM,SUN Classic VM等等。

1.2.8 OracleJDK和OpenJDK

Oracle JDK

Oracle JDK由Oracle公司開發,該公司是Sun許可證,基於Java標準版規範實現。它以二進制產品 的形式發佈。它支持多種操作系統,如Windows,Linux,Solaris,MacOS等。它支持不同的平臺,如 Intel 32位和64位架構,ARM架構和SPARC。它完全基於Java編程語言。之後,該許可證宣佈將根據 GPL(通用公共許可證)許可證發佈。Oracle JDK包含許多組件作爲庫形式的編程工具集合。

OpenJDK

歷史上的原因是,OpenJDK是JDK的開放源碼版本,以GPL協議的形式發佈。(General Public License)

在JDK7的時候,OpenJDK已經成爲JDK7的主幹開發版,SUN JDK7是在OpenJDK7的基礎上發佈的,其大部分源碼都相同,只有少部分源碼被替換掉。
使用JRL(Java Research License,Java研究授權協議)發佈。

OpenJDK是Java SE平臺版的開源和免費實現,它是Sun Corporation(現在的Oracle Corporation)於2006年開始的開發結果。它是根據GNU GPL許可證授權的。它最初於2007年發佈。 它由Oracle Corporation,Red Hat,IBM,Apple Inc.,OpenJDK和Java Community等開發。它是使 用C ++和Java編程語言編寫的。它支持不同的操作系統,如FreeBSD,Linux,Microsoft Windows, Mac OS X。OpenJDK是Java SE Platform Edition的官方參考實現。

對比

Oracle JDK可用於開發Java Web應用程序,獨立應用程序以及許多其他圖形用戶界面以及其他開發 工具。Oracle JDK執行的所有操作或任務也可以由OpenJDK執行,但只有Oracle與OpenJDK之間的區別 在於Open JDK在現有Oracle JDK之上的許可和其他工具集成和實現。使用OpenJDK的優點是可以根據 應用程序的要求修改性能,可伸縮性和實現,以根據需要調整Java虛擬機。

OpenJDK的優勢更多,Oracle JDK的使用在Oracle JDK實現中使用的標準方面也有一些好處,這將確保應用程序穩定和良好維護。

在這裏插入圖片描述

注意: 對於Java 11之前,兩者有少部分的區別,Oracle JDK有一些自己獨有的東⻄。但是Java 11 之後這兩者幾乎沒有區別,圖中提示了兩者共同代碼的佔比要遠高於圖形上看到的比例, 所以我們編譯 的OpenJDK基本上可以認爲性能、功能和執行邏輯上都和官方的Oracle JDK是一致的.

2 內存結構

說明

內存是非常重要的系統資源,是硬盤和CPU的中間倉庫及橋樑,承載這操作系統和應用程序的實時 運行。JVM內存佈局規定了java在運行過程中內存申請、分配、管理的策略,保證了JVM的高效穩定運 行。不同的JVM對於內存的劃分方式和管理機制存在着部分差異。結合JVM虛擬機規範,來探討一下經 典的JVM佈局。

HotSpot VM是目前市面上高性能虛擬機的代表之一。它採用解釋器即時編譯器並存的架構。在 今天,java程序的運行性能早已脫胎換⻣,已經到了可以和C/C++程序一較高下的地步。

進程與線程

​ 進程(process)是具有一定獨立功能的程序,操作系統利用進程把工作劃分爲一些功能單元。 進 程是進行資源分配和調度的一個獨立單位。它還擁有一個私有的虛擬地址空間,該空間僅能被它所包含 的線程訪問。 一個應用程序(application)是由一個或多個相互協作的進程組成的。

​ 線程(thread)是進程中所包含的一個或多個執行單元。它只能歸屬於一個進程並且只能訪問該進 程所擁有的資源。 它進程中執行運算的最小單位,是進程中的一個實體,是被進程獨立調度和分派的基 本單位。 線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源(計數器、寄存器和棧),但 它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同 一進程中的多個線程之間可以併發執行。 當操作系統創建一個進程後,該進程會自動申請一個名爲主線 程(首要線程)的線程。

​ 首先,進程和線程如同列⻋和⻋廂,沒有可比性,但是他們有一定的相關性:

  1. 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。
  2. 資源分配給進程,同一進程的所有線程共享該進程的所有資源。
  3. 虛擬機分給線程,即真正在虛擬機上運行的是線程。
  4. 線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。

在這裏插入圖片描述

方法區是Java虛擬機規範中的定義,是一種規範,而永久代和元空間是 HotSpot VM 不同版本的兩種實現。

jdk1.7及之前,HotSpot虛擬機對於方法區的實現稱之爲“永久代”, Permanent Generationjdk1.8之後,HotSpot虛擬機對於方法區的實現稱之爲“元空間”, Meta Space

在這裏插入圖片描述

2.1 運行時數據區

在這裏插入圖片描述

java虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啓動而創建,隨着虛擬機退出而銷燬,這些是線程共享的;另外一些則是與線程一一對應的,這些與線程對應 的數據區域會隨着線程開始和結束而創建和銷燬,這些是屬於線程獨享的。

  • 每個線程(線程私有、線程獨享):程序計數器、虛擬機棧、本地方法棧
  • 線程間共享(線程共有、線程共享):堆、堆外內存(方法區(永久代或元空間)、代碼緩存)

代碼緩存:JVM在運行時會將頻繁調用方法的字節碼編譯爲本地機器碼。這部分代碼所佔用的內存 空間成爲CodeCache區域。Java進行JIT的時候,會將編譯的本地代碼放在codecache中。

在這裏插入圖片描述
Runtime

在這裏插入圖片描述
每個JVM只有一個Runtime實例,即爲運行時環境,相當於內存結構的中間的那個框框:運行時環境。

這裏源碼也可以看到,Runtime採用的是單例模式,也就是說只會new一個Runtime對象,且用private修飾,不允許開發者自己去new,你只能去調用它的靜態方法。

2.2 PC寄存器

官方地址: https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

在這裏插入圖片描述

JVM中的程序計數寄存器(Program Counter Register),Register的命名源於CPU寄存器,寄存器存儲指令地址(或者稱偏移地址),CPU只有把數據裝載到寄存器才能運行,

在這裏,並非是廣義上指的物理寄存器,或許翻譯成PC計數器(或指令計數器)會更加貼切(也稱爲程序鉤子),並且也不容易引起一些不必要的誤會。JVM中的PC寄存器是對物理PC寄存器的一種抽象模擬。

2.2.1 作用

PC寄存器用來存儲指向下一條指令的地址,也就是即將要執行的指令代碼。由執行引擎讀取下一條指令。

在這裏插入圖片描述

2.2.2 示例

public class PCRegisterTest {
    public static void main(String[] args){
        int i = 100;
        int j = 200;
        int m = i + j;

        String str = "a";
        System.out.println(m);
        System.out.println(str);
    }
}

在這裏插入圖片描述

2.2.3 特點

  • 它是一塊很小的內存空間,幾乎可以忽略不計。也是運行速度最快的存儲區域。
  • 在JVM規範中,每個線程都有它的程序計數器,是線程私有的,它的生命週期與線程的生命週期保 持一致。
  • 任何時間一個線程都有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的java方法JVM指令地址;或者,如果是執行native方法,則是未指定值(undefined)。
  • 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
  • 字節碼解釋器工作時就是通過改變這個計數器的值來讀取下一條需要執行的字節碼指令。 它是唯一一個在java虛擬機規範中沒有規定任何OutOfMemoryError(OOM)情況的區域

2.2.4 面試問題

1 爲什麼使用PC寄存器記錄當前線程的執行地址?

因爲CPU需要不停地切換線程,這時候切換回來以後,線程就得知道接着從哪開始繼續執行。JVM的字節碼解釋器就需要通過改變PC寄存器的值來明確下一條應該執行什麼樣的字節碼指令。

2 PC寄存器爲什麼被設定爲線程私有?

我們都知道所謂的多線程在一個特定的時間段內只會執行其中某一個線程的方法,CPU會不停地做任務切換,這樣必然導致經常中斷或恢復,如何保證分毫無差呢?

爲了能夠準確地記錄各個線程正在執行的當前字節碼指令地址,最好的辦法自然是爲每一個線程都分配一個PC寄存器,這樣一來各個線程之間便可以進行獨立計算,從而不會出現相互干擾的情況。

由於CPU時間片輪換限制,衆多線程在併發執行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。

這樣必然導致經常中斷或恢復,如何保證分毫不差呢?每個線程在創建後,都會產生自己的程序計數器和棧幀,程序計數器在各個線程之間互不影響。

2.3 虛擬機棧

2.3.1 背景

​ 由於跨平臺性的設計,java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計爲基於寄存器的。

優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。

2.3.2 內存中的棧與堆

棧是運行時的單位,而堆是存儲的單位;

即:棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。
在這裏插入圖片描述

2.3.3 虛擬機棧基本內容

java虛擬機棧是什麼?

​ java虛擬機棧(java virtual Machine Stack),早期也叫java棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的java方法調用。它是線程私有的。

生命週期

​ 生命週期和線程一致。

作用

​ 主管java程序的運行,它保存方法的局部變量**(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的返回和調用**

2.3.4 棧與隊列

棧是一種動態集合,它是一種LIFO(last in first out後進先出)結構
在這裏插入圖片描述

隊列

與棧不同,它是一種FIFO(first in first out先進先出)結構

在這裏插入圖片描述

2.3.5 示例

public class StackTest {
    public static void main(String[] args) {
      StackTest stackTest = new StackTest();
      stackTest.methodA(); 
      System.out.println("main方法結束");
    }

    public void methodA(){
        int i = 10;
        int j = 20;
        methodB(); System.out.println("i:"+i+",j:"+j);
    }

    public void methodB(){ 
        int k = 30;
        int m = 40;
        System.out.println("k:"+k+",m:"+m);
    }
}

在這裏插入圖片描述

2.3.6 特點

  1. 棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器。

  2. JVM直接對java棧的操作只有兩個:

  • 每個方法的執行,伴隨着進棧(入棧、壓棧)
  • 執行結束後的出棧工作

在這裏插入圖片描述

  1. 對於棧來說不存在垃圾回收問題

2.3.7 可能出現的異常

Java虛擬機規範允許虛擬機棧的大小是動態的或者是固定不變的

  • 如果採用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一 個StackOverflowError異常
  • 如果Java虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常

2.3.8 設置棧大小

我們可以使用參數**-Xss**選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。 JDK5.0以後每個線程棧大小爲1M,以前每個線程棧大小爲256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一 個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。

/**
 * 演示棧中的異常
 *
 * 默認情況下:count 10823
 * 設置棧的大小: -Xss256k count 1874 */
public class StackErrorTest { private static int count = 1;

    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}

在這裏插入圖片描述

在IDEA中設置一個類的VM參數:

在這裏插入圖片描述
在這裏插入圖片描述
設置完成後點Apply以應用。

2.3.9 棧中存儲什麼?

  • 每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。
  • 在這個線程上,正在執行的每個方法都各自對應一個棧幀
  • 棧幀是一個內存區塊,是一個數據集,維繫着方法執行過程中的各種數據信息

2.3.10 棧運行原理

​ JVM直接對java棧的操作只有兩個,就是對棧幀的壓棧出棧,遵循先進後出/後進先出的原則。

​ 在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀**(Current Frame)**,與當前棧幀對應的方法就是當前方法 (Current Frame)

  • 執行引擎運行的所有字節碼指令只針對當前棧幀進行操作
  • 如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成爲新的當前棧幀。
  • 不同線程中所包含的棧幀是不允許相互引用的,即不可能在另一個棧幀中引用另外一個線程的棧幀。
  • 如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀, 接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成爲當前棧幀

Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。

public class StackFrameTest {
    public static void main(String[] args) {
        StackFrameTest test = new StackFrameTest();
        test.method1();
//輸出 method1()和method2()都作爲當前棧幀出現了兩次,method3()一次
//        method1()開始執行。。。
//        method2()開始執行。。。
//        method3()開始執行。。。
//        method3()執行結束。。。
//        method2()執行結束。。。
//        method1()執行結束。。。
    }
    public void method1(){
        System.out.println("method1()開始執行。。。");
        method2();
        System.out.println("method1()執行結束。。。");
            //return 可以省略
    }
    public int method2(){
        System.out.println("method2()開始執行。。。");
        int i = 10;
        int m = (int) method3();
        System.out.println("method2()執行結束。。。");
        return i+m;
    }
    public double method3(){
        System.out.println("method3()開始執行。。。");
        double j = 20.0;
        System.out.println("method3()執行結束。。。");
        return j;
    }
}

2.3.11 棧幀的內部結構

每個棧幀中存儲着

  • 局部變量表(Local Variable Table)
  • 操作數棧(Operand Stack)(或表達式棧)
  • 動態連接(Dynamic Linking)(或指向運行時常量池的方法引用)
  • 方法返回地址 (Return Address) (或方法正常退出或方法異常退出的定義)
  • 附加信息

2.3.12 相關面試題

1.舉例棧溢出的情況?(StackOverflowError)

  • 遞歸調用等,通過-Xss設置棧的大小;

2.調整棧的大小,就能保證不出現溢出麼?

  • 不能 如遞歸無限次數肯定會溢出,調整棧大小隻能保證溢出的時間晚一些,極限情況會導致OOM內存溢出(Out Of Memory Error)注意是Error

3.分配的棧內存越大越好麼?

  • 不是 會擠佔其他線程的空間

4.垃圾回收是否會涉及到虛擬機棧?

  • 不會

5.方法中定義的局部變量是否線程安全?

具體情況具體分析


/** * * * * * *
 題
 *
 面試題:
 方法中定義的局部變量是否線程安全?具體情況具體分析
 何爲線程安全?
 如果只有一個線程可以操作此數據,則必定是線程安全的。 如果有多個線程操作此數據,則此數據是共享數據。如果不考慮同步機制的話,會存在線程安全問
 我們知道StringBuffer是線程安全的源碼中實現synchronized,StringBuilder源碼未實現synchronized,在多線程情況下是不安全的
 * 二者均繼承自AbstractStringBuilder *
 */
public class StringBuilderTest {
    //s1的聲明方式是線程安全的,s1在方法method1內部消亡了
    public static void method1(){
    StringBuilder s1 = new StringBuilder();
    s1.append("a");
    s1.append("b");
    }

    //stringBuilder的操作過程:是不安全的,因爲method2可以被多個線程調用
    public static void method2(StringBuilder stringBuilder){
        stringBuilder.append("a");
        stringBuilder.append("b");
    }

    //s1的操作:是線程不安全的 有返回值,可能被其他線程共享
    public static StringBuilder method3(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }

    //s1的操作:是線程安全的 ,StringBuilder的toString方法是創建了一個新的String,s1在內部消亡了
    public static String method4(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1.toString();
    }

    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        new Thread(()->{
            s.append("a");
            s.append("b");
        }).start();

        method2(s);
    }
}

後續內容請看JVM概述&內存結構2
關注作者不迷路,持續更新高質量Java內容~
原創不易,您的支持/轉發/點贊/評論是我更新的最大動力!

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