Java八股文——JVM「內存模型篇」

JVM的內存模型介紹一下

面試官您好,您問的“JVM內存模型”,這是一個非常核心的問題。在Java技術體系中,這個術語通常可能指代兩個不同的概念:一個是JVM的運行時數據區,另一個是Java內存模型(JMM)。前者是JVM的內存布局規范,描述了內存被劃分成哪些區域;后者是并發編程的抽象模型,定義了線程間如何通過內存進行通信。

我先來介紹一下JVM的運行時數據區,這通常是大家更常提到的“內存模型”。

一、 JVM運行時數據區 (The Structure)

根據Java虛擬機規范,JVM在執行Java程序時,會把它所管理的內存劃分為若干個不同的數據區域。這些區域可以分為兩大類:線程共享的和線程私有的。

【線程共享區域】
這些區域的數據會隨著JVM的啟動而創建,隨JVM的關閉而銷毀,并且被所有線程共享。

  1. 堆 (Heap)

    • 這是JVM內存中最大的一塊。它的唯一目的就是存放對象實例數組。我們通過new關鍵字創建的所有對象,都在這里分配內存。
    • 堆是垃圾回收器(GC) 工作的主要區域。為了方便GC,堆內存通常還會被細分為新生代(Eden區、Survivor區)和老年代。
  2. 方法區 (Method Area)

    • 它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器(JIT)編譯后的代碼緩存等數據。
    • 可以把它理解為一個“元數據”區。
    • 在HotSpot JVM中,方法區的實現在不同JDK版本中有所演變:
      • JDK 7及以前:方法區被稱為 “永久代”(Permanent Generation) ,是堆的一部分。
      • JDK 8及以后:永久代被徹底移除,取而代之的是 “元空間”(Metaspace),它使用的是本地內存(Native Memory),而不再是JVM堆內存。這樣做的好處是元空間的大小只受限于本地內存,不容易出現OOM。
  3. 運行時常量池 (Runtime Constant Pool)

    • 它是方法區的一部分。用于存放編譯期生成的各種字面量和符號引用。

【線程私有區域】
這些區域的生命周期與線程相同,隨線程的創建而創建,隨線程的銷毀而銷毀。

  1. Java虛擬機棧 (Java Virtual Machine Stack)

    • 每個線程都有一個獨立的虛擬機棧。它用于存儲棧幀(Stack Frame)
    • 每當一個方法被調用時,JVM就會創建一個棧幀,并將其壓入棧中。棧幀里存儲了局部變量表、操作數棧、動態鏈接、方法出口等信息。
    • 當方法執行完畢后,對應的棧幀就會被彈出。我們常說的“棧內存”就是指這里。如果線程請求的棧深度大于虛擬機所允許的深度,會拋出StackOverflowError
  2. 本地方法棧 (Native Method Stack)

    • 與虛擬機棧非常相似,區別在于它為虛擬機使用到的 native方法(即由非Java語言實現的方法)服務。
  3. 程序計數器 (Program Counter Register)

    • 這是一塊非常小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器
    • 字節碼解釋器工作時,就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
    • 它是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域
二、 Java內存模型 (JMM - The Concurrency Model)

如果說運行時數據區是物理層面的內存劃分,那么Java內存模型(JMM)就是并發編程領域的抽象規范。它不是真實存在的內存結構,而是一套規則。

  • 目的:JMM的核心目的是為了屏蔽各種硬件和操作系統的內存訪問差異,讓Java程序在各種平臺上都能達到一致的內存訪問效果,從而實現“一次編寫,到處運行”的承諾。
  • 核心內容:它定義了線程和主內存之間的抽象關系。
    • 主內存 (Main Memory):所有線程共享的區域,存儲了所有的實例字段、靜態字段等。這可以粗略地對應于堆。
    • 工作內存 (Working Memory):每個線程私有的區域,存儲了該線程需要使用的變量在主內存中的副本拷貝。這可以粗略地對應于CPU的高速緩存。
  • 三大特性:JMM圍繞著在并發過程中如何處理原子性(Atomicity)、可見性(Visibility)和有序性(Ordering)這三個核心問題,定義了一系列的同步規則,比如volatilesynchronizedfinal的內存語義,以及著名的Happens-Before原則

總結一下

  • 運行時數據區是JVM的內存藍圖,告訴我們數據都存放在哪里。
  • Java內存模型(JMM)是并發編程的行為準則,告訴我們線程間如何安全地共享和通信數據。

JVM內存模型里的堆和棧有什么區別?

面試官您好,堆和棧是JVM運行時數據區中兩個最核心、但功能和特性截然不同的內存區域。它們的區別,我通常從以下幾個維度來理解。

一個貫穿始終的比喻:快餐店的點餐與后廚

我們可以把一次程序運行想象成在一家快餐店點餐:

  • 棧(Stack) 就像是前臺的點餐流程單
  • 堆(Heap) 就像是后廚的中央廚房
1. 核心用途與存儲內容 (做什么?)
  • 棧 (點餐流程單)

    • 用途:主要用于管理方法的調用和存儲基本數據類型變量以及對象引用
    • 內容:每當一個方法被調用,JVM就會創建一個“棧幀”(就像流程單上的一行),里面記錄了這個方法的所有局部變量、操作數、方法出口等信息。
    • 比喻:點一個漢堡(調用一個方法),服務員就在流程單上記下一筆。
  • 堆 (中央廚房)

    • 用途:是JVM中唯一用來存儲對象實例數組的地方。
    • 內容:我們通過new關鍵字創建的所有對象,其實體都存放在堆中。棧上的那個對象引用,僅僅是一個指向堆中對象實體的“門牌號”或“地址”。
    • 比喻:流程單上記的“漢堡”,只是一個名字(引用)。真正的漢堡實體(對象實例),是在后廚(堆)里制作和存放的。
2. 生命周期與管理方式 (誰管?怎么管?)
  • 棧 (自動化的流程單)

    • 生命周期:非常規律和確定。一個方法調用開始,其對應的棧幀就被壓入棧頂;方法執行結束,棧幀就自動彈出并銷毀
    • 管理方式:由編譯器和JVM自動管理,無需我們程序員干預,也沒有垃圾回收(GC)
  • 堆 (需要專人管理的廚房)

    • 生命周期:不確定。一個對象的生命周期從new開始,直到沒有任何引用指向它時才結束。
    • 管理方式:由垃圾回收器(GC) 來自動管理。GC會定期地巡視堆,找出那些不再被使用的“無主”對象(垃圾),并回收它們占用的空間。
3. 空間大小與存取速度 (多大?多快?)
    • 空間:通常較小且大小是固定的(可以通過-Xss參數設置)。
    • 速度非常快。因為棧的數據結構簡單(LIFO),內存是連續的,CPU可以高效地進行壓棧和彈棧操作。
    • 空間:通常較大且大小是可動態調整的(可以通過-Xms-Xmx設置)。
    • 速度:相對較慢。因為內存分配是不連續的,并且分配和回收的過程都比棧要復雜。
4. 線程共享性與可見性 (公有還是私有?)
  • 線程私有。每個線程都有自己獨立的虛擬機棧。一個線程不能訪問另一個線程的棧空間,因此棧上的數據天然是線程安全的

  • 所有線程共享。整個JVM進程只有一個堆。這意味著任何線程都可以通過引用訪問堆上的同一個對象。這也正是多線程并發問題的根源所在,我們需要通過各種鎖機制來保證對堆上共享對象訪問的安全性。

5. 典型的異常

這兩種內存區域如果使用不當,會分別導致兩種最經典的JVM異常:

  • StackOverflowError (棧溢出):通常是由于方法遞歸調用過深(流程單寫得太長,超出了紙的范圍),或者棧幀過大導致的。
  • OutOfMemoryError: Java heap space (堆溢出):通常是由于創建了大量的對象實例,并且這些對象由于被持續引用而無法被GC回收(后廚的東西太多,放不下了),最終耗盡了堆內存。

通過這個全方位的對比,我們就能清晰地理解堆和棧在JVM中所扮演的不同角色和承擔的不同職責了。

棧中存的到底是指針還是對象?

面試官您好,您這個問題問到了JVM內存管理的一個核心細節。最精確的回答是:棧中既不存指針,也不直接存對象,它存的是“基本類型的值”和“對象的引用”。

我們可以通過一個具體的代碼例子和生活中的比喻來理解它。

1. 一個具體的代碼例子

假設我們有下面這樣一個方法:

public void myMethod() {// 1. 基本數據類型int age = 30; // 2. 對象引用類型String name = "Alice"; // 3. 數組引用類型int[] scores = new int[3]; 
}

myMethod()被調用時,JVM會為它在當前線程的虛擬機棧上創建一個棧幀。這個棧幀的“局部變量表”里會存放以下內容:

  • 對于 int age = 30;

    • age 是一個基本數據類型。JVM會直接在棧幀里為age分配一塊空間,并將30 本身存放在這塊空間里。
  • 對于 String name = "Alice";

    • name 是一個對象引用。JVM的處理分為兩步:
      1. 在堆(Heap)中創建一個String對象,其內容是 “Alice”。
      2. 在棧幀中name變量分配一塊空間,這塊空間里存放的不是"Alice"這個字符串本身,而是一個指向堆中那個String對象的內存地址。這個地址,我們就稱之為 “引用”(Reference)
  • 對于 int[] scores = new int[3];

    • scores 也是一個對象引用(在Java中,數組是對象)。
    • 處理方式與String類似:
      1. 在堆中創建一個可以容納3個整數的數組對象。
      2. 在棧幀中scores變量分配空間,存放一個指向堆中那個數組對象的引用
2. 一個生動的比喻:酒店房間與房卡

我們可以把這個過程比喻成入住一家酒店:

  • 堆(Heap):就像是酒店本身,里面有許多實實在在的房間(對象實例)
  • 棧(Stack):就像是你手里的那張房卡(對象引用)
  • 基本類型:就像是你口袋里的零錢(值),你直接就帶在身上。

那么:

  • new String("Alice"):相當于酒店為你分配了一間房間(在堆上創建對象)
  • String name = ...:酒店前臺給了你一張房卡(在棧上創建引用),這張房卡上有房間號,可以讓你找到并打開那間房。
  • 你手里拿的,永遠是房卡(引用),而不是整個房間(對象)。你想找房間里的東西,必須先通過房卡找到房間。
3. 總結:棧到底存了什么?
  • 基本數據類型:直接存儲本身。
  • 引用數據類型:存儲一個引用(內存地址),這個引用指向中存放的對象實例

所以,嚴格來說,棧中存的既不是C++意義上的“指針”(雖然功能類似,但Java的引用是類型安全的,且由JVM管理),更不是對象本身。它存的是一個受JVM管理的、類型安全的、指向堆內存的“門牌號”——我們稱之為“引用”。

堆分為哪幾部分呢?

面試官您好,JVM的堆內存是垃圾回收器(GC)進行管理的主要區域,為了優化GC的效率,特別是為了實現分代回收(Generational GC) 的思想,HotSpot虛擬機通常會將堆劃分為以下幾個主要部分:

1. 新生代 (Young Generation / New Generation)

新生代是絕大多數新創建對象的“第一站”。它的主要特點是對象“朝生夕死”,存活率低。因此,新生代通常采用復制算法(Copying Algorithm) 進行垃圾回收,這種算法在對象存活率低的場景下效率非常高。

新生代內部又被細分為三個區域:

  • a. Eden區 (Eden Space)

    • 這是絕大多數新對象誕生的地方。當我們new一個對象時,它首先會被分配在Eden區。
    • Eden區的空間是連續的,分配速度很快。
  • b. 兩個Survivor區 (Survivor Space)

    • 通常被稱為From區(S0)To區(S1)
    • 這兩個區的大小是完全一樣的,并且在任何時候,總有一個是空閑的
    • 它們的作用:當Eden區進行垃圾回收(這個過程通常被稱為Minor GCYoung GC)時,存活下來的對象會被復制到那個空閑的Survivor區(To區)。同時,另一個正在使用的Survivor區(From區)中還存活的對象,也會被一并復制到這個To區。
    • 復制完成后,Eden區和From區就被完全清空了。然后,From區和To區的角色會發生互換,等待下一次Minor GC。
2. 老年代 (Old Generation / Tenured Generation)

老年代用于存放那些生命周期較長的對象,或者是一些大對象

  • 對象來源

    1. 從新生代晉升:一個對象在新生代的Survivor區之間,每經歷一次Minor GC并且存活下來,它的年齡(Age)就會加1。當這個年齡達到一個閾值(默認是15)時,它就會被“晉升”到老年代。
    2. 大對象直接分配:如果一個對象非常大(比如一個巨大的數組),超過了JVM設定的閾值(可以通過-XX:PretenureSizeThreshold參數設置),為了避免它在新生代的Eden區和Survivor區之間頻繁復制,JVM會選擇將其直接分配在老年代
  • GC算法:老年代的對象特點是存活率高,不適合用復制算法(因為需要復制的對象太多,空間浪費也大)。因此,老年代的垃圾回收(通常被稱為Major GCFull GC)通常采用標記-清除(Mark-Sweep)標記-整理(Mark-Compact) 算法。

一個對象的“一生”

我們可以用一個故事來描繪一個普通對象的生命周期:

  1. 出生:一個對象在Eden區誕生。
  2. 第一次考驗:經歷了一次Minor GC,它幸運地活了下來,被移動到了Survivor的To區,年齡變為1。
  3. 顛沛流離:在接下來的多次Minor GC中,它在S0區和S1區之間來回被復制,每次存活,年齡都會加1。
  4. 晉升:當它的年齡終于達到15歲時,它被認為是一個“穩定”的對象,在下一次Minor GC后,它會被晉升到老年代
  5. 定居與終老:在老年代,它會“定居”下來,不再經歷頻繁的Minor GC。它會等待很久之后,發生Major GC或Full GC時,才會被檢查是否還在被使用。如果最終不再被任何引用指向,它才會被回收,結束其一生。

這種分代的設計,使得JVM可以針對不同生命周期的對象,采用最高效的回收策略,從而大大提升了GC的整體性能。

程序計數器的作用,為什么是私有的?

面試官您好,程序計數器(Program Counter Register)是JVM運行時數據區中一塊非常小但至關重要的內存區域。要理解它,我們可以從 “它是什么”“為什么必須是線程私有” 這兩個角度來看。

1. 程序計數器的作用 (What is it?)
  • 核心定義:程序計數器可以看作是當前線程所執行的字節碼的行號指示器
  • 它的工作:在JVM中,字節碼解釋器就是通過讀取和改變程序計數器的值,來確定下一條需要執行的字節碼指令。分支、循環、跳轉、異常處理、線程恢復等基礎功能,都依賴于這個計數器來完成。
  • 一個重要的細節
    • 如果當前線程正在執行的是一個Java方法,那么程序計數器記錄的就是正在執行的虛擬機字節碼指令的地址
    • 如果當前線程正在執行的是一個 native方法(本地方法),那么這個計數器的值是空(Undefined)。因為native方法是由底層操作系統或其他語言實現的,不受JVM字節碼解釋器的控制。
2. 為什么程序計數器必須是線程私有的?(Why is it private?)

其根本原因就在于Java的多線程是通過CPU時間片輪轉來實現的

  • 場景分析

    1. 現代操作系統都是多任務的,CPU會在多個線程之間高速地進行上下文切換
    2. 比如,線程A的當前時間片用完了,操作系統需要暫停它,然后切換到線程B去執行。
    3. 在暫停線程A之前,必須記錄下它“剛才執行到哪里了”。這個“位置信息”,就是由程序計數器來保存的。
    4. 當未來某個時刻,線程A重新獲得CPU時間片時,它就需要恢復現場,從它上次被中斷的地方繼續執行。這時,它就會去查看自己的程序計數器,找到下一條應該執行的指令。
  • 結論

    • 因為每個線程的執行進度都是獨立且不一樣的,它們在任何時刻都可能被中斷。為了在切換回來后能準確地恢復到正確的執行位置,每個線程都必須擁有自己專屬的、互不干擾的程序計數器
    • 如果所有線程共享一個程序計數器,那么一個線程的執行就會覆蓋掉另一個線程的進度記錄,整個執行流程就會徹底混亂。
3. 一個獨特的特性

值得一提的是,程序計數器是JVM運行時數據區中唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。因為它所占用的內存空間非常小且固定,幾乎可以忽略不計。

總結一下,程序計數器就像是每個線程專屬的 “書簽”,它忠實地記錄著每個線程的閱讀進度,確保了在并發執行和頻繁切換的復雜環境下,每個線程都能準確無誤地繼續自己的執行流程。因此,它的“線程私有”特性,是實現多線程正確性的根本保障。

方法區中的方法的執行過程?

面試官您好,雖然方法本身的代碼是存放在方法區的,但一個方法的執行過程,其主戰場卻是在Java虛擬機棧(JVM Stack) 中。

整個過程,可以看作是一個棧幀(Stack Frame)在虛擬機棧中“入棧”和“出棧” 的旅程。

我通過一個簡單的例子來描述這個動態過程:

public class MethodExecution {public static void main(String[] args) {int result = add(3, 5); // 1. 調用add方法System.out.println(result);}public static int add(int a, int b) { // 2. add方法int sum = a + b;return sum; // 3. 返回}
}
第一步:方法調用與棧幀創建 (入棧)
  1. 解析:當main線程執行到add(3, 5)這行代碼時,JVM首先需要找到add方法在方法區的具體位置(如果之前沒解析過的話)。
  2. 創建棧幀:在調用add方法之前,JVM會在main線程的虛擬機棧中,為add方法創建一個新的棧幀(我們稱之為add-Frame),并將其壓入棧頂
    • 此時,main方法對應的棧幀(main-Frame)就在add-Frame的下方。
    • 這個add-Frame就像一個專屬的工作空間,它里面包含了:
      • 局部變量表:用于存放add方法的參數ab(值分別為3和5),以及局部變量sum
      • 操作數棧:一個臨時的計算區域,用于執行加法等操作。
      • 動態鏈接:指向運行時常量池中該方法所屬類的引用。
      • 方法返回地址:記錄了add方法執行完畢后,應該回到main方法中的哪一行代碼繼續執行。
第二步:方法體執行
  1. 參數傳遞main-Frame中的操作數3和5,會被傳遞到add-Frame的局部變量表中,賦值給ab
  2. 字節碼執行:CPU開始執行add方法的字節碼指令。
    • 將局部變量ab的值加載到add-Frame操作數棧上。
    • 執行加法指令,從操作數棧中彈出兩個數相加,并將結果8再壓入操作數棧。
    • 將操作數棧頂的結果8,存回到局部變量sum中。
    • 執行return指令,將局部變量sum的值再次加載到操作數棧頂,準備作為返回值。
第三步:方法返回與棧幀銷毀 (出棧)

方法執行完畢,需要返回。返回分為兩種情況:

  1. 正常返回 (Normal Return)

    • 像本例中,執行return sum;
    • add方法的棧幀會將返回值(8)傳遞給調用者(main方法)的棧幀,通常是壓入main-Frame的操作數棧中。
    • 然后,add方法的棧幀會從虛擬機棧中被銷毀(出棧)
    • 程序計數器會根據之前保存的方法返回地址,恢復到main方法中調用add的那一行,繼續向后執行(比如將main-Frame操作數棧頂的8賦值給result變量)。
  2. 異常返回 (Abrupt Return)

    • 如果在add方法中發生了未被捕獲的異常。
    • add方法的棧幀同樣會被銷毀(出棧),但它不會有任何返回值給調用者。
    • JVM會把這個異常對象拋給調用者main方法去處理。如果main方法也處理不了,這個異常會繼續向上傳播,直到最終導致線程終止。

總結一下,方法的執行過程,本質上是線程的虛擬機棧中,棧幀不斷入棧和出棧的過程。當前正在執行的方法,其對應的棧幀永遠位于棧頂。這個清晰、高效的棧式結構,是Java方法能夠實現有序調用和遞歸的基礎。

方法區中還有哪些東西?

面試官您好,方法區是JVM運行時數據區中一個非常重要的線程共享區域。正如《深入理解Java虛擬機》中所述,它主要用于存儲已被虛擬機加載的元數據信息

我們可以把方法區想象成一個JVM的 “類型信息檔案館”,當一個.class文件被加載進內存后,它的大部分“檔案信息”都存放在這里。

這些信息主要可以分為以下幾大類:

1. 類型信息 (Type Information)

這是方法區的核心。對于每一個被加載的類(或接口),JVM都會在方法區中存儲其完整的元信息,包括:

  • 類的全限定名 (e.g., java.lang.String)。
  • 類的直接父類的全限定名 (e.g., java.lang.Object)。
  • 類的類型 (是類class還是接口interface)。
  • 類的訪問修飾符 (public, abstract, final等)。
  • 類的直接實現接口的有序列表
  • 字段信息 (Field Info):每個字段的名稱、類型、修飾符等。
  • 方法信息 (Method Info):每個方法的名稱、返回類型、參數列表、修飾符,以及最重要的——方法的字節碼 (Bytecodes)
2. 運行時常量池 (Runtime Constant Pool)
  • 來源:每個.class文件內部都有一個“常量池表(Constant Pool Table)”,用于存放編譯期生成的各種字面量符號引用。當這個類被加載到JVM后,這個靜態的常量池表就會被轉換成方法區中的運行時常量池

  • 內容

    • 字面量:比如文本字符串("Hello, World!")、final常量的值等。
    • 符號引用 (Symbolic References):這是一種編譯時的、用字符串表示的間接引用。它包括:
      • 類和接口的全限定名。
      • 字段的名稱和描述符。
      • 方法的名稱和描述符。
    • 在程序實際運行時,JVM會通過這些符號引用,去動態地查找并鏈接到真實的內存地址(這個過程叫動態鏈接)。
  • 動態性:運行時常量池的一個重要特性是它是動態的。比如String.intern()方法,就可以在運行時將新的常量放入池中。

3. 靜態變量 (Static Variables)
  • 也稱為“類變量”。被static關鍵字修飾的字段,會存放在方法區中。
  • 這些變量與類直接關聯,而不是與類的某個實例對象關聯,因此被所有線程共享。
4. 即時編譯器(JIT)編譯后的代碼緩存
  • 為了提升性能,HotSpot虛擬機會將頻繁執行的“熱點代碼”(HotSpot Code)通過JIT編譯器編譯成本地機器碼。
  • 這部分編譯后的、高度優化的本地機器碼,也會被緩存存放在方法區中,以便下次直接執行,無需再解釋字節碼。
方法區的演進:永久代與元空間

值得一提的是,方法區是一個邏輯上的概念,它的具體物理實現在不同JDK版本中是不同的:

  • JDK 7及以前:HotSpot JVM使用永久代(Permanent Generation)來實現方法區。永久代是堆內存的一部分,它有固定的大小上限,容易導致OutOfMemoryError: PermGen space
  • JDK 8及以后:永久代被徹底移除,取而代之的是元空間(Metaspace)。元空間使用的是本地內存(Native Memory),而不是JVM堆內存。這樣做的好處是,元空間的大小只受限于操作系統的可用內存,極大地降低了因元數據過多而導致OOM的風險。

總結一下,方法區就像是JVM的“圖書館”,里面存放著所有加載類的“戶口本”(類型信息)、“字典”(運行時常量池)、“公共財產”(靜態變量)以及“最優操作手冊”(JIT編譯后的代碼)。它是Java程序能夠運行起來的基礎。

String保存在哪里呢?

情況一:通過字面量直接賦值 (String s = "abc";)
  • 存儲位置:當您像這樣直接用雙引號創建一個字符串時,這個字符串"abc"會被存放在一個特殊的內存區域,叫做字符串常量池(String Constant Pool)

  • 工作機制

    1. JVM在處理這行代碼時,會先去字符串常量池里檢查,看是否已經存在內容為"abc"的字符串
    2. 如果存在,JVM就不會創建新的對象,而是會直接將常量池中那個字符串的引用返回,賦值給變量s
    3. 如果不存在,JVM才會在常量池中創建一個新的String對象,內容是"abc",然后將它的引用返回。
  • 特性:這種方式創建的字符串,是共享的。例如:

    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2); // 輸出: true
    

    這里的s1s2指向的是常量池中同一個對象。

情況二:通過new關鍵字創建 (String s = new String("abc");)
  • 存儲位置:這種方式的行為就和普通的Java對象一樣了,它會涉及到兩個內存區域。

  • 工作機制

    1. new String("abc")這行代碼,首先,JVM還是會去檢查字符串常量池,確保池中有一個"abc"的對象(如果沒有就創建一個)。
    2. 然后,最關鍵的一步是,new關鍵字會在Java堆(Heap) 上,創建一個全新的String對象。這個新的String對象內部的字符數組,會復制常量池中那個"abc"對象的數據。
    3. 最后,將堆上這個新對象的引用返回給變量s
  • 特性:這種方式總是在堆上創建一個新對象,即使字符串的內容已經存在于常量池中。

    String s1 = "abc"; // 在常量池
    String s2 = new String("abc"); // 在堆上
    System.out.println(s1 == s2); // 輸出: false
    

    這里的s1s2指向的是兩個完全不同的對象,一個在常量池,一個在堆。

字符串常量池的演進

值得一提的是,字符串常量池的物理位置在JDK版本中是有變遷的:

  • JDK 6及以前:字符串常量池是方法區(永久代) 的一部分。
  • JDK 7:字符串常量池被從方法區移到了Java堆中。
  • JDK 8及以后:永久代被元空間取代,但字符串常量池仍然在Java堆中。

將常量池移到堆中,一個主要的好處是方便GC對常量池中的字符串進行回收。

intern() 方法的作用

String類還提供了一個intern()方法,它是一個與常量池交互的橋梁:

  • 當一個堆上的String對象(比如通過new創建的)調用intern()方法時,JVM會去字符串常量池里查找是否存在內容相同的字符串。
    • 如果存在,就返回常量池中那個字符串的引用
    • 如果不存在,就會將這個字符串的內容放入常量池,并返回新放入的那個引用。

總結一下

  • 直接用字面量創建的String,對象在字符串常量池中。
  • new關鍵字創建的String,對象主體在Java堆中。

理解這個區別,對于我們優化內存使用和正確判斷字符串相等性(特別是用==時)至關重要。

引用類型有哪些?有什么區別?

面試官您好,Java中的引用類型,除了我們最常用的強引用,還提供了軟、弱、虛三種不同強度的引用,它們的設計主要是為了讓我們可以更靈活地與垃圾回收器(GC) 進行交互,從而實現更精細的內存管理。

我們可以把這四種引用的強度,比作一段關系的“牢固程度”:

1. 強引用 (Strong Reference) —— “生死相依”
  • 定義與特點:這就是我們日常編程中最常見的引用形式,比如 Object obj = new Object();。只要一個對象還存在強引用指向它,那么垃圾回收器永遠不會回收這個對象,即使系統內存已經非常緊張,即將發生OutOfMemoryError
  • 生命周期:直到這個強引用被顯式地設置為null(比如obj = null;),或者超出了其作用域,它與對象之間的“強關聯”才會斷開。
  • 應用場景:所有常規對象的創建和使用。
2. 軟引用 (Soft Reference) —— “情有可原,可有可無”
  • 定義與特點:用SoftReference類來包裝對象。軟引用關聯的對象,是那些有用但并非必需的對象。
  • GC回收時機:當系統內存即將發生溢出(OOM)之前,垃圾回收器會把這些軟引用關聯的對象給回收掉,以釋放內存,嘗試挽救系統。如果回收之后內存仍然不足,才會拋出OOM。
  • 應用場景:非常適合用來實現高速緩存。比如,一個圖片加載應用,可以將加載到內存的圖片用軟引用包裝起來。內存充足時,圖片可以一直保留在內存中,加快下次訪問速度;內存緊張時,這些圖片緩存可以被自動回收,而不會導致系統崩潰。
3. 弱引用 (Weak Reference) —— “萍水相逢,一碰就忘”
  • 定義與特點:用WeakReference類來包裝對象。弱引用的強度比軟引用更弱。
  • GC回收時機:只要垃圾回收器開始工作,無論當前內存是否充足,被弱引用關聯的對象都一定會被回收。也就是說,它只能“活”到下一次GC發生之前。
  • 應用場景
    • ThreadLocal的KeyThreadLocalMap中的Key就是對ThreadLocal對象的弱引用,這有助于在ThreadLocal對象本身被回收后,防止一部分內存泄漏。
    • 各種緩存和監聽器注冊:在一些需要避免內存泄漏的緩存或回調注冊場景中,使用弱引用可以確保當目標對象被回收后,相關的緩存條目或監聽器也能被自動清理。最典型的就是WeakHashMap
4. 虛引用 (Phantom Reference) —— “若有若無,形同虛設”
  • 定義與特點:也叫“幻影引用”,是所有引用類型中最弱的一種。它由PhantomReference類實現,并且必須和引用隊列(ReferenceQueue)聯合使用
  • 核心特性
    • 一個對象是否有虛引用,完全不影響其生命周期。就像沒有這個引用一樣,該被回收時就會被回收。
    • 我們永遠無法通過虛引用來獲取到對象實例phantomRef.get()方法永遠返回null
  • 它的唯一作用:當一個對象被GC確定要回收時,如果它有關聯的虛引用,那么JVM會在真正回收其內存之前,將這個虛引用對象本身(而不是它引用的對象)放入與之關聯的ReferenceQueue中。
  • 應用場景:它主要用于跟蹤對象被垃圾回收的活動。最經典的應用就是管理堆外內存(Direct Memory)。比如DirectByteBuffer,它在Java堆上只是一個很小的對象,但它在堆外分配了大量的本地內存。我們可以為這個DirectByteBuffer對象創建一個虛引用。當GC回收這個對象時,虛引用會入隊。我們的后臺清理線程可以監視這個隊列,一旦發現有虛引用入隊,就知道對應的堆外內存已經不再被使用,就可以安全地調用free()方法來釋放這塊本地內存了。

通過這四種不同強度的引用,Java賦予了開發者與GC協作的能力,讓我們能夠根據對象的生命周期和重要性,設計出更健壯、內存使用更高效的程序。

弱引用了解嗎?舉例說明在哪里可以用?

面試官您好,我了解弱引用。它是一種比軟引用“更弱”的引用類型,其核心特點是:一個只被弱引用指向的對象,只要垃圾回收器開始工作,無論當前內存是否充足,它都一定會被回收。

弱引用提供了一種讓我們能夠“監視”一個對象生命周期,但又“不干涉”其被回收的方式。

弱引用最經典的應用案例剖析

在Java的API和各種框架中,弱引用有很多巧妙的應用。我舉兩個最著名的例子來說明它在哪里用,以及如何用:

案例一:ThreadLocal 中的內存泄漏“防線”

這是弱引用最廣為人知的一個應用。

  • 背景ThreadLocal的內部,每個線程都持有一個ThreadLocalMap。這個Map的Entry(鍵值對)被設計為:
    • Key:是對ThreadLocal對象的弱引用 (WeakReference<ThreadLocal>)。
    • Value:是對我們實際存儲的值的強引用
  • 為什么用弱引用?
    • 假設我們在代碼中將一個ThreadLocal變量置為null了 (myThreadLocal = null;),這意味著我們不再需要它了。
    • 如果沒有弱引用,而是強引用,那么即使myThreadLocal被置為null,只要這個線程還存活,ThreadLocalMap中的Entry就會一直強引用著這個ThreadLocal對象,導致它永遠無法被回收。
    • 而使用了弱引用后,當myThreadLocal在外部的強引用消失,下一次GC發生時,ThreadLocalMap中那個作為Key的ThreadLocal對象就會被自動回收,Entry的Key就變成了null
  • 作用:這為清理Value創造了條件。雖然Value本身還是強引用,但ThreadLocal在調用get(), set()時,會順便檢查并清理掉這些Key為null的Entry。弱引用的使用,是ThreadLocal能夠進行部分自我清理、防止內存泄漏的第一道防線。

案例二:WeakHashMap —— 構建“會自動清理的緩存”

這是一個更直接體現弱引用價值的例子。

  • WeakHashMap是什么?
    • 它是一個鍵(Key)是弱引用的HashMap
  • 它是如何工作的?
    • 當我們向WeakHashMapput(key, value)時,這個key對象被弱引用所包裹。
    • 當外部不再有任何強引用指向這個key對象時,在下一次GC后,這個key對象就會被回收。
    • WeakHashMap內部有一個機制(通過ReferenceQueue),當它發現某個key被回收后,它會自動地將整個Entry(包括key和value)從Map中移除
  • 應用場景
    • 非常適合用來做緩存。我們可以把緩存的鍵作為key,緩存的內容作為value
    • 好處:當緩存的鍵(比如某個業務對象)在程序的其他地方不再被使用、被GC回收后,WeakHashMap中對應的這條緩存記錄也會自動地、安全地被清理掉,我們完全不需要手動去維護緩存的過期和清理,從而完美地避免了因緩存引發的內存泄漏。
總結

弱引用的核心用途,就是構建一種非侵入式的、依賴于GC的關聯關系。它允許我們“依附”于一個對象,但又不會強行延長它的生命周期。這在實現緩存、元數據存儲、監聽器管理等需要避免內存泄漏的場景中,是非常有價值的工具。


內存泄漏和內存溢出的理解?

面試官您好,內存泄漏和內存溢出是Java開發者必須面對的兩個核心內存問題。它們是兩個不同但又緊密相關的概念。

我可以用一個 “水池注水” 的比喻來解釋它們:

  • 內存(堆):就像一個容量固定的水池
  • 創建新對象:就像往水池里注入新的水
  • 垃圾回收(GC):就像是水池的排水口,會自動排掉不再需要的水。
  • 內存泄漏:就像是排水口被一些垃圾(無用的引用)堵住了一部分
  • 內存溢出:就是水池最終被灌滿了,水溢了出來
1. 內存泄漏 (Memory Leak) —— “該走的不走”
  • 定義:內存泄漏指的是,程序中一些已經不再被使用的對象,由于仍然存在著某個(通常是無意的)強引用鏈,導致垃圾回收器(GC)無法將它們回收
  • 本質:這些對象邏輯上已經是“垃圾”了,但GC不這么認為。它們就像“僵尸”一樣,占著茅坑不拉屎,持續地、無效地消耗著寶貴的堆內存。
  • 后果:一次小小的內存泄漏可能不會立即產生影響,但如果這種泄漏發生在頻繁執行的代碼路徑上,日積月累,就會導致可用內存越來越少。
  • 常見原因
    • 長生命周期的對象持有短生命周期對象的引用:最典型的就是靜態集合類。一個靜態的HashMap,如果不手動remove,它里面存放的對象的生命周期就和整個應用程序一樣長,即使這些對象早就不需要了。
    • 資源未關閉:比如數據庫連接、網絡連接、文件IO流等,如果沒有在finally塊中正確關閉,它們持有的底層資源和緩沖區內存就無法被釋放。
    • 監聽器和回調未注銷:一個對象注冊了監聽器,但自身銷毀前沒有去注銷,導致被監聽的目標對象一直持有它的引用。
    • ThreadLocal使用不當:沒有在finally中調用remove()方法,導致在線程池場景下,Value對象無法被回收。
2. 內存溢出 (OutOfMemoryError, OOM) —— “想來的來不了”
  • 定義:內存溢出是一個結果,是一個錯誤(Error)。它指的是,當程序需要申請更多內存時(比如new一個新對象),而JVM發現堆內存已經耗盡,并且經過GC后也無法騰出足夠的空間,最終只能拋出OutOfMemoryError,導致應用程序崩潰。
  • 常見原因
    • 內存泄漏的累積:這是最隱蔽、最常見的原因。持續的內存泄漏最終會“吃光”所有可用內存,導致OOM。
    • 瞬時創建大量對象:程序在某個時刻需要處理大量數據,一次性加載了海量對象到內存中,直接超出了堆的上限。比如,一次性從數據庫查詢一百萬條記錄并映射成對象。
    • 堆空間設置不合理:JVM啟動時,通過-Xmx參數設置的堆最大值,對于應用的實際需求來說太小了。
    • StackOverflowError:雖然這也是OOM的一種,但它特指棧內存溢出,通常是由于無限遞歸或方法調用鏈過深導致的。
3. 關系總結
  • 內存泄漏是原因,內存溢出是結果
  • 持續的、未被發現的內存泄漏,最終必然會導致內存溢出
  • 但是,發生內存溢出,并不一定是因為內存泄漏。也可能是因為數據量確實太大,或者JVM參數配置不當。
4. 如何排查?

在實踐中,排查這類問題,我會使用專業的內存分析工具:

  1. 通過JVM參數(-XX:+HeapDumpOnOutOfMemoryError)讓JVM在發生OOM時,自動生成一個堆轉儲快照(Heap Dump)文件
  2. 使用內存分析工具(如 MAT (Memory Analyzer Tool)、JProfiler等)來打開和分析這個dump文件。
  3. 在MAT中,可以查看支配樹(Dominator Tree)和查找泄漏嫌疑(Leak Suspects),工具會自動幫我們分析哪些對象占用了大量內存,以及是什么樣的引用鏈導致它們無法被回收,從而快速定位到問題的根源。

JVM內存結構有哪幾種內存溢出的情況?

面試官您好,JVM的內存結構在不同區域都可能發生內存溢出,這通常意味著程序申請內存超出了JVM所能管理的上限。我主要熟悉以下四種最常見的內存溢出情況:

1. 堆內存溢出 (Heap OOM)
  • 異常信息java.lang.OutOfMemoryError: Java heap space

  • 原因分析:這是最常見的一種OOM。正如您所說,根本原因是在堆中無法為新創建的對象分配足夠的空間。這通常由兩種情況導致:

    1. 內存泄漏(Memory Leak):程序中存在生命周期過長的對象(如靜態集合),它們持有了不再使用的對象的引用,導致GC無法回收,可用內存越來越少。
    2. 內存確實不夠用:程序需要處理的數據量確實非常大,比如一次性從數據庫查詢了數百萬條記錄并加載到內存中,直接超出了堆的容量上限。
  • 代碼示例

    // 模擬內存確實不夠用
    List<byte[]> list = new ArrayList<>();
    while (true) {// 不斷創建大對象,直到耗盡堆內存list.add(new byte[1024 * 1024]); // 1MB
    }
    
  • 解決方案

    1. 分析Heap Dump:使用MAT等工具分析OOM時生成的堆轉儲文件,查看是哪些對象占用了大量內存,并檢查其引用鏈,判斷是否存在內存泄漏。
    2. 優化代碼:如果是數據量過大,需要優化代碼邏輯,比如使用流式處理、分批加載等方式,避免一次性加載所有數據。
    3. 調整JVM參數:如果確認業務上需要這么多內存,可以通過增大-Xmx參數來調高堆的最大值。
2. 虛擬機棧和本地方法棧溢出 (Stack OOM)
  • 異常信息:通常是 java.lang.StackOverflowError,在極少數無法擴展棧的情況下可能是OutOfMemoryError

  • 原因分析:每個線程都有自己的虛擬機棧,用于存放方法調用的棧幀。棧溢出通常不是因為內存“不夠大”,而是因為棧的深度超過了限制

    • 最常見的原因就是無限遞歸方法調用鏈過深
  • 代碼示例

    public class StackOverflowTest {public static void recursiveCall() {recursiveCall(); // 無限遞歸}public static void main(String[] args) {recursiveCall();}
    }
    
  • 解決方案

    1. 檢查代碼邏輯:仔細檢查代碼,找出導致無限遞歸或過深調用的地方并修復它。這是最根本的解決辦法。
    2. 調整棧大小:如果確認業務邏輯需要很深的調用棧,可以通過-Xss參數來增大每個線程的棧空間大小,但這治標不治本。
3. 元空間溢出 (Metaspace OOM)
  • 異常信息java.lang.OutOfMemoryError: Metaspace

  • 原因分析:元空間(在JDK 8之前是永久代)主要存儲類的元數據信息。元空間溢出意味著加載的類太多了

    • 常見原因包括:系統本身非常龐大,加載了大量的類和第三方jar包;或者在運行時通過動態代理、反射、CGLIB等技術,動態生成了大量的類,但這些類又沒能被及時卸載。
  • 代碼示例

    // 使用CGLIB等字節碼技術不斷生成新類
    while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(MyClass.class);enhancer.setUseCache(false);enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));enhancer.create();
    }
    
  • 解決方案

    1. 排查類加載情況:檢查是否有動態類生成相關的庫(如CGLIB)被濫用。
    2. 優化依賴:精簡項目依賴,移除不必要的jar包。
    3. 調整JVM參數:通過增大-XX:MaxMetaspaceSize參數來調高元空間的最大值。
4. 直接內存溢出 (Direct Memory OOM)
  • 異常信息java.lang.OutOfMemoryError: Direct buffer memory

  • 原因分析:這是由于使用了NIO(New I/O) 中的ByteBuffer.allocateDirect()方法,在堆外(本地內存) 分配了大量內存,而這部分內存又沒能被及時回收。

    • 直接內存的回收,依賴于與之關聯的DirectByteBuffer對象被GC回收時,觸發一個清理機制(通過虛引用和Cleaner)。如果堆內存遲遲沒有觸發GC,那么堆外的直接內存就可能一直得不到釋放,最終耗盡。
  • 代碼示例

    // 不斷分配直接內存,但不觸發GC
    List<ByteBuffer> buffers = new ArrayList<>();
    while (true) {buffers.add(ByteBuffer.allocateDirect(1024 * 1024)); // 1MB
    }
    
  • 解決方案

    1. 檢查NIO代碼:確保合理使用直接內存,并在不需要時及時清理。
    2. 適時手動GC:在一些極端情況下,如果確認直接內存壓力大,可以考慮在代碼中調用System.gc()來“建議”JVM進行一次Full GC,但這通常不被推薦。
    3. 調整JVM參數:通過-XX:MaxDirectMemorySize參數來明確指定直接內存的最大容量。

通過對這幾種OOM的理解和分析,我們可以在遇到問題時,根據不同的異常信息,快速地定位到可能的原因,并采取相應的解決措施。

有具體的內存泄漏和內存溢出的例子么請舉例及解決方案?

案例一:靜態集合類導致的內存泄漏

這是最常見、也最容易被忽視的一種內存泄漏。

1. 問題場景代碼

假設我們有一個需求,需要臨時緩存一些用戶信息,但開發人員錯誤地使用了一個靜態的HashMap來存儲。

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;// 模擬一個用戶服務
class UserService {// 【問題根源】使用了一個靜態的Map來緩存用戶對象private static Map<String, User> userCache = new HashMap<>();public void cacheUser(User user) {if (!userCache.containsKey(user.getId())) {userCache.put(user.getId(), user);System.out.println("緩存用戶: " + user.getId() + ", 當前緩存大小: " + userCache.size());}}// 缺少一個移除緩存的方法!
}// 用戶對象
class User {private String id;private String name;// ... 構造函數, getter/setter ...public User(String id, String name) { this.id = id; this.name = name; }public String getId() { return id; }
}// 模擬Web請求不斷調用
public class StaticLeakExample {public static void main(String[] args) {UserService userService = new UserService();while (true) {// 模擬每次請求都創建一個新的User對象并緩存String userId = UUID.randomUUID().toString();User newUser = new User(userId, "User-" + userId);userService.cacheUser(newUser);// 為了不讓程序瞬間OOM,稍微 sleep 一下try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}}
}
2. 泄漏原因分析
  1. 靜態變量的生命周期userCache是一個static變量,它的生命周期和整個UserService類的生命周期一樣長,通常也就是整個應用程序的運行時間。
  2. 持續的強引用while (true)循環不斷地創建新的User對象并調用cacheUser方法。每調用一次,這個新的User對象就被put進了靜態的userCache中。
  3. 無法被GC回收userCache這個Map一直強引用著所有被放進去的User對象。即使這些User對象在業務邏輯上早就不需要了,但只要userCache還引用著它們,垃圾回收器就永遠不會回收這些User對象。
  4. 最終結果:隨著時間推移,userCache越來越大,占用的堆內存越來越多,最終耗盡所有堆內存,拋出 java.lang.OutOfMemoryError: Java heap space
3. 解決方案
  1. 明確移除(治標):最直接的辦法是,在確定不再需要某個緩存對象時,手動從userCache中調用remove()方法將其移除,切斷強引用。但這依賴于開發者必須記得去調用,容易遺漏。

  2. 使用弱引用(治本):這是一個更優雅、更自動化的解決方案。我們可以使用WeakHashMap來替代HashMap

    // 解決方案:使用WeakHashMap
    private static Map<String, User> userCache = new WeakHashMap<>();
    
    • WeakHashMap的特性:它的鍵(Key)是弱引用。當一個User對象在程序的其他地方不再有任何強引用指向它時(比如,處理完一個Web請求,相關的User對象都變成了垃圾),即使它還存在于WeakHashMap中,GC也會將它回收。WeakHashMap在檢測到Key被回收后,會自動地將整個鍵值對從Map中移除。
    • 這樣,緩存的生命周期就和它所緩存的對象的生命周期自動綁定了,完美地避免了內存泄漏。
  3. 使用專業的緩存框架(最佳實踐):在生產環境中,我們不應該手寫緩存。應該使用專業的緩存框架,如Guava Cache, Caffeine, 或 Ehcache。這些框架不僅內置了基于弱引用、軟引用的自動清理機制,還提供了更豐富的功能,如基于大小的淘汰、基于時間的過期、統計等。

案例二:ThreadLocal使用不當導致的內存泄漏

這個案例在線程池環境下尤其常見。

1. 問題場景代碼
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadLocalLeakExample {// 創建一個ThreadLocal來存儲大對象static ThreadLocal<byte[]> localVariable = new ThreadLocal<>();public static void main(String[] args) {// 使用固定大小的線程池ExecutorService executor = Executors.newFixedThreadPool(1);for (int i = 0; i < 100; i++) {executor.submit(() -> {// 【問題根源】為ThreadLocal設置了一個大對象localVariable.set(new byte[1024 * 1024 * 5]); // 5MBSystem.out.println("線程 " + Thread.currentThread().getName() + " 設置了值");// 【關鍵問題】任務執行完畢后,沒有調用remove()方法!// localVariable.remove(); // 正確的做法應該是加上這一行});try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}// ...}
}
2. 泄漏原因分析
  1. 線程池與線程復用newFixedThreadPool(1)創建了一個只有一個線程的線程池。這意味著,所有100個任務,都是由同一個線程來輪流執行的。
  2. ThreadLocal的存儲原理ThreadLocal的值實際上是存儲在Thread對象自身的ThreadLocalMap中的。
  3. 引用鏈分析
    • 當第一個任務執行時,它在線程T1ThreadLocalMap中放入了一個5MB的字節數組。
    • 任務結束后,localVariable這個ThreadLocal對象可能會因為方法結束而被回收(它的Key是弱引用)。
    • 但是,線程T1并不會被銷毀,它會被歸還給線程池,等待下一個任務。
    • 此時,一條強引用鏈依然存在:線程池 -> 線程T1 -> T1.threadLocals(ThreadLocalMap) -> Entry -> Value(5MB的byte[])
    • 這個5MB的數組就因為這條強引用鏈而無法被GC回收
  4. 最終結果:當后續的任務在這個線程上執行,又調用localVariable.set()時,它會覆蓋掉舊的值,但如果后續任務不再使用這個ThreadLocal,那么最后一次設置的那個5MB的數組就會永久地留在這個線程里,直到線程池被關閉。如果線程池很大,或者ThreadLocal存儲的對象更多,就會慢慢地耗盡內存,導致OOM。
3. 解決方案

解決方案非常簡單,但必須強制遵守:

  • 養成在finally塊中調用remove()的習慣

    executor.submit(() -> {localVariable.set(new byte[1024 * 1024 * 5]);try {// ... 執行業務邏輯 ...System.out.println("線程 " + Thread.currentThread().getName() + " 設置了值");} finally {// 確保在任務結束時,無論正常還是異常,都清理ThreadLocal的值localVariable.remove();System.out.println("線程 " + Thread.currentThread().getName() + " 清理了值");}
    });
    
  • 調用remove()方法會徹底地將ThreadLocalMap中對應的Entry移除,從而切斷整個引用鏈,讓Value對象可以被正常地垃圾回收。這是使用ThreadLocal時必須遵守的鐵律。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/86714.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/86714.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/86714.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RabbitMQ 高可用與可靠性保障實現

RabbitMQ 高可用與可靠性保障實現詳解 一、高可用架構設計1.1 集群部署模式1.2 鏡像隊列&#xff08;Mirrored Queue&#xff09; 二、可靠性保障機制2.1 消息持久化2.2 確認機制&#xff08;Confirm & Ack&#xff09;2.3 死信隊列&#xff08;DLX&#xff09; 三、容災與…

12.7Swing控件6 JList

在 Java Swing 中&#xff0c;列表框&#xff08;JList&#xff09;是用于顯示一組選項的組件&#xff0c;用戶可以從中選擇一個或多個項目。以下是關于 Swing 列表框的詳細介紹&#xff1a; 1. 基本概念與用途 作用&#xff1a;以垂直列表形式展示選項&#xff0c;支持單選或…

C++: condition_variable: wait_for -> unlock_wait_for_lock?

作為C++的初學者,面臨的一個很大的問題,就是很多的概念并不是可以通過名稱直觀的預知它要完成的細節,比如這里的condition_variable的wait_for。C++的設計意圖好像是,我告訴你這樣用,你只要這樣做就行,又簡單還實用!而且需要記住的規則量又大的驚人。最后看起來,更像是…

HTML版英語學習系統

HTML版英語學習系統 這是一個完全免費、無需安裝、功能完整的英語學習工具&#xff0c;使用HTML CSS JavaScript實現。 功能 文本朗讀練習 - 輸入英文文章&#xff0c;系統朗讀幫助練習聽力和發音&#xff0c;適合跟讀練習&#xff0c;模仿學習&#xff1b;實時詞典查詢 - 雙…

【JUC面試篇】Java并發編程高頻八股——線程與多線程

目錄 1. 什么是進程和線程&#xff1f;有什么區別和聯系&#xff1f; 2. Java的線程和操作系統的線程有什么區別&#xff1f; 3. 線程的創建方式有哪些? 4. 如何啟動和停止線程&#xff1f; 5. Java線程的狀態模型&#xff08;有哪些狀態&#xff09;&#xff1f; 6. 調用…

LSTM-SVM多變量時序預測(Matlab完整源碼和數據)

LSTM-SVM多變量時序預測&#xff08;Matlab完整源碼和數據&#xff09; 目錄 LSTM-SVM多變量時序預測&#xff08;Matlab完整源碼和數據&#xff09;效果一覽基本介紹程序設計參考資料 效果一覽 基本介紹 代碼主要功能 該代碼實現了一個LSTM-SVM多變量時序預測模型&#xff0c…

ES6——數組擴展之Set數組

在ES6&#xff08;ECMAScript 2015&#xff09;中&#xff0c;JavaScript的Set對象提供了一種存儲任何值唯一性的方式&#xff0c;類似于數組但又不需要索引訪問。這對于需要確保元素唯一性的場景非常有用。Set對象本身并不直接提供數組那樣的方法來操作數據&#xff08;例如ma…

日志收集工具-logstash

提示&#xff1a;Windows 環境下 安裝部署 logstash 采集日志文件 文章目錄 一、下載二、解壓部署三、常用插件四、常用配置 Logstash 服務器數據處理管道&#xff0c;能夠從多個來源采集數據&#xff0c;轉換數據&#xff0c;然后將數據發送到您最喜歡的存儲庫中。Logstash 沒…

6個月Python學習計劃 Day 21 - Python 學習前三周回顧總結

? 第一周&#xff1a;基礎入門與流程控制&#xff08;Day 1 - 7&#xff09; “打地基”的一周&#xff0c;我們走完了從變量、輸入輸出、判斷、循環到第一個小型系統的完整鏈路。 &#x1f4d8; 學習重點&#xff1a; Python 基礎語法&#xff1a;變量類型、字符串格式化、注…

Spring Boot SQL數據庫功能詳解

Spring Boot自動配置與數據源管理 數據源自動配置機制 當在Spring Boot項目中添加數據庫驅動依賴&#xff08;如org.postgresql:postgresql&#xff09;后&#xff0c;應用啟動時自動配置系統會嘗試創建DataSource實現。開發者只需提供基礎連接信息&#xff1a; 數據庫URL格…

java每日精進 6.11【消息隊列】

1.內存級Spring_Event 1.1 控制器層&#xff1a;StringTextController /*** 字符串文本管理控制器* 提供通過消息隊列異步獲取文本信息的接口*/ RestController RequestMapping("/api/string-text") public class StringTextController {Resourceprivate StringTex…

【凌智視覺模塊】rv1106 部署 ppocrv4 檢測模型 rknn 推理

PP-OCRv4 文本框檢測 1. 模型介紹 如有需要可以前往我們的倉庫進行查看 凌智視覺模塊 PP-OCRv4在PP-OCRv3的基礎上進一步升級。整體的框架圖保持了與PP-OCRv3相同的pipeline&#xff0c;針對檢測模型和識別模型進行了數據、網絡結構、訓練策略等多個模塊的優化。 從算法改…

uniapp Vue2 獲取電量的獨家方法:繞過官方插件限制

在使用 uniapp 進行跨平臺應用開發時&#xff0c;獲取設備電量信息是一個常見的需求。然而&#xff0c;uniapp 官方提供的uni.getBatteryInfo方法存在一定的局限性&#xff0c;它不僅需要下載插件&#xff0c;而且目前僅支持 Vue3&#xff0c;這讓使用 Vue2 進行開發的開發者陷…

Go語言中的if else控制語句

if else是Go語言中最基礎也最常用的條件控制語句&#xff0c;用于根據條件執行不同的代碼塊。下面我將詳細介紹Go語言中if else的各種用法和特性。 1. 基本語法 1.1. 最簡單的if語句 if 條件表達式 {// 條件為true時執行的代碼 } 示例&#xff1a; if x > 10 {fmt.Prin…

[Spring]-AOP

AOP場景 AOP: Aspect Oriented Programming (面向切面編程) OOP: Object Oriented Programming (面向對象編程) 場景設計 設計: 編寫一個計算器接口和實現類&#xff0c;提供加減乘除四則運算 需求: 在加減乘除運算的時候需要記錄操作日志(運算前參數、運算后結果)實現方案:…

Web3 借貸與清算機制全解析:鏈上金融的運行邏輯

Web3 借貸與清算機制全解析&#xff1a;鏈上金融的運行邏輯 超額抵押借款 例如&#xff0c;借款人用ETH為抵押借入DAI&#xff1b;借款人的ETH的價值一定是要超過DAI的價值&#xff1b;借款人可以任意自由的使用自己借出的DAI 穩定幣 第一步&#xff1a;借款人需要去提供一定…

RK3588開發筆記-GNSS-RTK模塊調試

目錄 前言 一、什么是GNSS/RTK 二、硬件連接 三、內核配置 四、模塊調試 五、ntripclient使用 總結 前言 在RK3588平臺上集成高精度定位功能是許多工業級應用的需求。本文記錄了我調試GNSS-RTK模塊的全過程,包含硬件連接、驅動移植、數據解析和精度優化等關鍵環節,希望對…

Vue.js $emit的介紹和簡單使用

前言 在 Vue.js 開發中&#xff0c;組件化是核心思想之一。但組件間的通信是一個重要課題&#xff0c;特別是子組件向父組件傳遞數據的場景。Vue 提供了多種通信方式&#xff0c;而$emit正是實現子→父通信的關鍵方法。本文將深入解析$emit的原理、使用場景及最佳實踐。 一、$e…

【Linux 學習計劃】-- 簡易版shell編寫

目錄 思路 創建自己的命令行 獲取用戶命令 分割命令 檢查是否是內建命令 cd命令實現 進程程序替換執行程序 總代碼 結語 思路 int main() {while (1){// 1. 自己的命令行PrintCommandLine();// 2. 獲取用戶命令char command[SIZE];int n GetUserCommand(command, si…

一個完整的日志收集方案:Elasticsearch + Logstash + Kibana+Filebeat (二)

&#x1f4c4; 本地 Windows 部署 Logstash 連接本地 Elasticsearch 指南 ? 目標 在本地 Windows 上安裝并運行 Logstash配置 Logstash 將數據發送至本地 Elasticsearch測試數據采集與 ES 存儲流程 &#x1f9f0; 前提條件 軟件版本要求安裝說明Java17Oracle JDK 下載 或 O…