JVM

来源:https://www.cnblogs.com/zz01/archive/2022/07/17/16488509.html
-Advertisement-
Play Games

JVM 一、什麼是JVM 定義 Java Virtual Machine,JAVA程式的運行環境(JAVA二進位位元組碼的運行環境) 好處 一次編寫,到處運行 自動記憶體管理,垃圾回收機制 數組下標越界檢查 比較 JVM JRE JDK的區別 二、記憶體結構 整體架構 1、程式計數器 作用 用於保存JVM ...


JVM

一、什麼是JVM

定義

Java Virtual Machine,JAVA程式的運行環境(JAVA二進位位元組碼的運行環境)

好處

  • 一次編寫,到處運行
  • 自動記憶體管理,垃圾回收機制
  • 數組下標越界檢查

比較

JVM JRE JDK的區別

image-20220128120907409

二、記憶體結構

整體架構

img

1、程式計數器

作用

用於保存JVM中下一條所要執行的指令的地址

特點

  • 線程私有
    • CPU會為每個線程分配時間片,噹噹前線程的時間片使用完以後,CPU就會去執行另一個線程中的代碼
    • 程式計數器是每個線程所私有的,當另一個線程的時間片用完,又返回來執行當前線程的代碼時,通過程式計數器可以知道應該執行哪一句指令
  • 不會存在記憶體溢出

2、虛擬機棧

定義

  • 每個線程運行需要的記憶體空間,稱為虛擬機棧
  • 每個棧由多個棧幀組成,對應著每次調用方法時所占用的記憶體
  • 每個線程只能有一個活動棧幀,對應著當前正在執行的方法(棧頂部的第一個方法)

演示

代碼

public class Main {
    public static void main(String[] args) {
        method1();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c = a + b;
        return c;
    }
}Copy

img

在控制臺中可以看到,主類中的方法在進入虛擬機棧的時候,符合棧的特點

問題辨析

  • 垃圾回收是否涉及棧記憶體?
    • 不需要。因為虛擬機棧中是由一個個棧幀組成的,在方法執行完畢後,對應的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收記憶體。
  • 棧記憶體的分配越大越好嗎?
    • 不是。因為物理記憶體是一定的,棧記憶體越大,可以支持更多的遞歸調用,但是可執行的線程數就會越少。
  • 方法內的局部變數是否是線程安全的?
    • 看變數是被線程私有還是共用,共用則不安全
    • 看變數的作用範圍
    • image-20220202214904660

記憶體溢出

Java.lang.stackOverflowError 棧記憶體溢出

發生原因

  • 虛擬機棧中,棧幀過多(無限遞歸)
  • 每個棧幀所占用過大

線程運行診斷

CPU占用過高

  • Linux環境下運行某些程式的時候,可能導致CPU的占用過高,這時需要定位占用CPU過高的線程
    • top命令,查看是哪個進程占用CPU過高
    • ps H -eo pid, tid(線程id), %cpu | grep 剛纔通過top查到的進程號 通過ps命令進一步查看是哪個線程占用CPU過高
    • jstack 進程id 通過查看進程中的線程的nid,剛纔通過ps命令看到的tid來對比定位,註意jstack查找出的線程id是16進位的,需要轉換

3、本地方法棧

一些帶有native關鍵字的方法就是需要JAVA去調用本地的C或者C++方法,因為JAVA有時候沒法直接和操作系統底層交互,所以需要用到本地方法

4、堆

定義

通過new關鍵字創建的對象都會被放在堆記憶體

特點

  • 所有線程共用,堆記憶體中的對象都需要考慮線程安全問題
  • 有垃圾回收機制

堆記憶體溢出

java.lang.OutofMemoryError :java heap space. 堆記憶體溢出

堆記憶體診斷

jps

jmap

jconsole

jvirsalvm

5、方法區

結構

img

記憶體溢出

  • 1.8以前會導致永久代記憶體溢出
  • 1.8以後會導致元空間記憶體溢出

常量池

二進位位元組碼的組成:類的基本信息、常量池、類的方法定義(包含了虛擬機指令)

通過反編譯來查看類的信息

  • 獲得對應類的.class文件

    • 在JDK對應的bin目錄下運行cmd,也可以在IDEA控制台輸入

      img

    • 輸入 javac 對應類的絕對路徑

      F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy
      

      輸入完成後,對應的目錄下就會出現類的.class文件

  • 在控制台輸入 javap -v 類的絕對路徑

    javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
    
  • 然後能在控制台看到反編譯以後類的信息了

    • 類的基本信息

      image-20220203213504873

    • 常量池

      image-20220203213602701

    • 虛擬機中執行編譯的方法(框內的是真正編譯執行的內容,**#號的內容需要在常量池中查找**)

      image-20220203213712499

運行時常量池

  • 常量池
    • 就是一張表(如上圖中的constant pool),虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量信息
  • 運行時常量池
    • 常量池是*.class文件中的,當該類被載入以後,它的常量池信息就會放入運行時常量池,並把裡面的符號地址變為真實地址

常量池與串池的關係

串池StringTable

特征

  • 常量池中的字元串僅是符號,只有在被用到時才會轉化為對象
  • 利用串池的機制,來避免重覆創建字元串對象
  • 字元串變數拼接的原理是StringBuilder
  • 字元串常量拼接的原理是編譯器優化
  • 可以使用intern方法,主動將串池中還沒有的字元串對象放入串池中
  • 註意:無論是串池還是堆裡面的字元串,都是對象

用來放字元串對象且裡面的元素不重覆

public class StringTableStudy {
    public static void main(String[] args) {
        String a = "a"; 
        String b = "b";
        String ab = "ab";
    }
}

 

常量池中的信息,都會被載入到運行時常量池中,但這是a b ab 僅是常量池中的符號,還沒有成為java字元串

0: ldc           #2                  // String a
2: astore_1
3: ldc           #3                  // String b
5: astore_2
6: ldc           #4                  // String ab
8: astore_3
9: returnCopy

 

當執行到 ldc #2 時,會把符號 a 變為 “a” 字元串對象,並放入串池中(hashtable結構 不可擴容)

當執行到 ldc #3 時,會把符號 b 變為 “b” 字元串對象,並放入串池中

當執行到 ldc #4 時,會把符號 ab 變為 “ab” 字元串對象,並放入串池中

最終StringTable [“a”, “b”, “ab”]

註意:字元串對象的創建都是懶惰的,只有當運行到那一行字元串且在串池中不存在的時候(如 ldc #2)時,該字元串才會被創建並放入串池中。

使用拼接字元串變數對象創建字元串的過程

public class StringTableStudy {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = "ab";
        //拼接字元串對象來創建新的字元串
        String ab2 = a+b; 
    }
}

 


通過拼接的方式來創建字元串的過程是:StringBuilder().append(“a”).append(“b”).toString()

最後的toString方法的返回值是一個新的字元串,但字元串的值和拼接的字元串一致,但是兩個不同的字元串,一個存在於串池之中,一個存在於堆記憶體之中


  • 使用拼接字元串常量的方法來創建新的字元串時,因為內容是常量,javac在編譯期會進行優化,結果已在編譯期確定為ab,而創建ab的時候已經在串池中放入了“ab”,所以ab3直接從串池中獲取值,所以進行的操作和 ab = “ab” 一致。
  • 使用拼接字元串變數的方法來創建新的字元串時,因為內容是變數,只能在運行期確定它的值,所以需要使用StringBuilder來創建
intern方法 1.8

調用字元串對象的intern方法,會將該字元串對象嘗試放入到串池中

  • 如果串池中沒有該字元串對象,則放入成功
  • 如果有該字元串對象,則放入失敗

無論放入是否成功,都會返回串池中的字元串對象

註意:此時如果調用intern方法成功,堆記憶體與串池中的字元串對象是同一個對象;如果失敗,則不是同一個對象


intern方法 1.6

調用字元串對象的intern方法,會將該字元串對象嘗試放入到串池中

  • 如果串池中沒有該字元串對象,會將該字元串對象複製一份,再放入到串池中
  • 如果有該字元串對象,則放入失敗

無論放入是否成功,都會返回串池中的字元串對象

註意:此時無論調用intern方法成功與否,串池中的字元串對象和堆記憶體中的字元串對象都不是同一個對象

StringTable 垃圾回收

StringTable在記憶體緊張時,會發生垃圾回收

StringTable調優

  • 因為StringTable是由HashTable實現的,所以可以適當增加HashTable桶的個數,來減少字元串放入串池所需要的時間

    -XX:StringTableSize=xxxxCopy
    
  • 考慮是否需要將字元串對象入池

    可以通過intern方法減少重覆入池

6、直接記憶體

  • 屬於操作系統,常見於NIO操作時,用於數據緩衝區
  • 分配回收成本較高,但讀寫性能高
  • 不受JVM記憶體回收管理

文件讀寫流程

img

使用了DirectBuffer

img

直接記憶體是操作系統和Java代碼都可以訪問的一塊區域,無需將代碼從系統記憶體複製到Java堆記憶體,從而提高了效率

釋放原理

直接記憶體的回收不是通過JVM的垃圾回收來釋放的,而是通過unsafe.freeMemory來手動釋放

通過

//通過ByteBuffer申請1M的直接記憶體
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);Copy

申請直接記憶體,但JVM並不能回收直接記憶體中的內容,它是如何實現回收的呢?

allocateDirect返回一個DirectByteBuffer對象,裡面申請記憶體,通過虛引用對象釋放記憶體

DirectByteBuffer類

DirectByteBuffer(int cap) {   // package-private
   
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); //申請記憶體
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通過虛引用,來實現直接記憶體的釋放,this為虛引用的實際對象
    att = null;
}Copy

這裡調用了一個Cleaner的create方法,且後臺線程還會對虛引用的對象監測,如果虛引用的實際對象(這裡是DirectByteBuffer)被回收以後,就會調用Cleaner的clean方法,來清除直接記憶體中占用的記憶體

public void clean() {
       if (remove(this)) {
           try {
               this.thunk.run(); //調用run方法
           } catch (final Throwable var2) {
               AccessController.doPrivileged(new PrivilegedAction<Void>() {
                   public Void run() {
                       if (System.err != null) {
                           (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                       }

                       System.exit(1);
                       return null;
                   }
               });
           }Copy

對應對象的run方法

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address); //釋放直接記憶體中占用的記憶體
    address = 0;
    Bits.unreserveMemory(size, capacity);
}Copy
直接記憶體的回收機制總結
  • 使用了Unsafe類來完成直接記憶體的分配回收,回收需要主動調用freeMemory方法
  • ByteBuffer的實現內部使用了Cleaner(虛引用)來檢測ByteBuffer。一旦ByteBuffer被垃圾回收,那麼會由ReferenceHandler來調用Cleaner的clean方法調用freeMemory來釋放記憶體

三、垃圾回收

1、如何判斷對象可以回收

引用計數法

弊端:迴圈引用時,兩個對象的計數都為1,導致兩個對象都無法被釋放

img

可達性分析演算法

  • JVM中的垃圾回收器通過可達性分析來探索所有存活的對象
  • 掃描堆中的對象,看能否沿著GC Root對象為起點的引用鏈找到該對象,如果找不到,則表示可以回收
  • 可以作為GC Root的對象
    • 虛擬機棧(棧幀中的本地變數表)中引用的對象。 
    • 方法區中類靜態屬性引用的對象
    • 方法區中常量引用的對象
    • 本地方法棧中JNI(即一般說的Native方法)引用的對象

五種引用

img

強引用

只有GC Root都不引用該對象時,才會回收強引用對象

  • 如上圖B、C對象都不引用A1對象時,A1對象才會被回收
軟引用

當GC Root指向軟引用對象時,在記憶體不足時,會回收軟引用所引用的對象

  • 如上圖如果B對象不再引用A2對象且記憶體不足時,軟引用所引用的A2對象就會被回收
軟引用的使用
public class Demo1 {
    public static void main(String[] args) {
        final int _4M = 4*1024*1024;
        //使用軟引用對象 list和SoftReference是強引用,而SoftReference和byte數組則是軟引用
        List<SoftReference<byte[]>> list = new ArrayList<>();
        SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    }
}Copy

如果在垃圾回收時發現記憶體不足,在回收軟引用所指向的對象時,軟引用本身不會被清理

如果想要清理軟引用,需要使用引用隊列

public class Demo1 {
    public static void main(String[] args) {
        final int _4M = 4*1024*1024;
        //使用引用隊列,用於移除引用為空的軟引用對象
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        //使用軟引用對象 list和SoftReference是強引用,而SoftReference和byte數組則是軟引用
        List<SoftReference<byte[]>> list = new ArrayList<>();
        SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

        //遍歷引用隊列,如果有元素,則移除
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null) {
            //引用隊列不為空,則從集合中移除該元素
            list.remove(poll);
            //移動到引用隊列中的下一個元素
            poll = queue.poll();
        }
    }
}Copy

大概思路為:查看引用隊列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這裡為一個list集合)

弱引用

只有弱引用引用該對象時,在垃圾回收時,無論記憶體是否充足,都會回收弱引用所引用的對象

  • 如上圖如果B對象不再引用A3對象,則A3對象會被回收

弱引用的使用和軟引用類似,只是將 SoftReference 換為了 WeakReference

虛引用

當虛引用對象所引用的對象被回收以後,虛引用對象就會被放入引用隊列中,調用虛引用的方法

  • 虛引用的一個體現是釋放直接記憶體所分配的記憶體,當引用的對象ByteBuffer被垃圾回收以後,虛引用對象Cleaner就會被放入引用隊列中,然後調用Cleaner的clean方法來釋放直接記憶體
  • 如上圖,B對象不再引用ByteBuffer對象,ByteBuffer就會被回收。但是直接記憶體中的記憶體還未被回收。這時需要將虛引用對象Cleaner放入引用隊列中,然後調用它的clean方法來釋放直接記憶體
終結器引用

所有的類都繼承自Object類,Object類有一個finalize方法。當某個對象不再被其他的對象所引用時,會先將終結器引用對象放入引用隊列中,然後根據終結器引用對象找到它所引用的對象,然後調用該對象的finalize方法。調用以後,該對象就可以被垃圾回收了

  • 如上圖,B對象不再引用A4對象。這是終結器對象就會被放入引用隊列中,引用隊列會根據它,找到它所引用的對象。然後調用被引用對象的finalize方法。調用以後,該對象就可以被垃圾回收了
引用隊列
  • 軟引用和弱引用可以配合引用隊列
    • 在弱引用和虛引用所引用的對象被回收以後,會將這些引用放入引用隊列中,方便一起回收這些軟/弱引用對象
  • 虛引用和終結器引用必須配合引用隊列
    • 虛引用和終結器引用在使用時會關聯一個引用隊列

2、垃圾回收演算法

標記-清除

img

定義:標記清除演算法顧名思義,是指在虛擬機執行垃圾回收的過程中,先採用標記演算法確定可回收對象,然後垃圾收集器根據標識清除相應的內容,給堆記憶體騰出相應的空間

  • 這裡的騰出記憶體空間並不是將記憶體空間的位元組清0,而是記錄下這段記憶體的起始結束地址,下次分配記憶體的時候,會直接覆蓋這段記憶體

缺點:容易產生大量的記憶體碎片,可能無法滿足大對象的記憶體分配,一旦導致無法分配對象,那就會導致jvm啟動gc,一旦啟動gc,我們的應用程式就會暫停,這就導致應用的響應速度變慢

標記-整理

img

標記-整理 會將不被GC Root引用的對象回收,清楚其占用的記憶體空間。然後整理剩餘的對象,可以有效避免因記憶體碎片而導致的問題,但是因為整體需要消耗一定的時間,所以效率較低

複製

img

img

img

img

將記憶體分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的對象從FROM放入TO中,再回收不被GC Root引用的對象。然後交換FROM和TO。這樣也可以避免記憶體碎片的問題,但是會占用雙倍的記憶體空間。

3、分代回收

img

回收流程

新創建的對象都被放在了新生代的伊甸園中

img

當伊甸園中的記憶體不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC

Minor GC 會將伊甸園和幸存區FROM存活的對象先複製到 幸存區 TO中, 並讓其壽命加1,再交換兩個幸存區

img

img

img

再次創建對象,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他用戶線程,只讓垃圾回收線程工作),這時不僅會回收伊甸園中的垃圾,還會回收幸存區中的垃圾,再將活躍對象複製到幸存區TO中。回收以後會交換兩個幸存區,並讓幸存區中的對象壽命加1

img

如果幸存區中的對象的壽命超過某個閾值(最大為15,4bit),就會被放入老年代中

img

如果新生代老年代中的記憶體都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的對象並回收

GC 分析

大對象處理策略

當遇到一個較大的對象時,就算新生代的伊甸園為空,也無法容納該對象時,會將該對象直接晉升為老年代

線程記憶體溢出

某個線程的記憶體溢出了而拋異常(out of memory),不會讓其他的線程結束運行

這是因為當一個線程拋出OOM異常後,它所占據的記憶體資源會全部被釋放掉,從而不會影響其他線程的運行,進程依然正常

4、垃圾回收器

相關概念

並行收集:指多條垃圾收集線程並行工作,但此時用戶線程仍處於等待狀態。

併發收集:指用戶線程與垃圾收集線程同時工作(不一定是並行的可能會交替執行)。用戶程式在繼續運行,而垃圾收集程式運行在另一個CPU上

吞吐量:即CPU用於運行用戶代碼的時間與CPU總消耗時間的比值(吞吐量 = 運行用戶代碼時間 / ( 運行用戶代碼時間 + 垃圾收集時間 )),也就是。例如:虛擬機共運行100分鐘,垃圾收集器花掉1分鐘,那麼吞吐量就是99%

串列

  • 單線程
  • 記憶體較小,個人電腦(CPU核數較少)

img

安全點:讓其他線程都在這個點停下來,以免垃圾回收時移動對象地址,使得其他線程找不到被移動的對象

因為是串列的,所以只有一個垃圾回收線程。且在該線程執行回收工作時,其他線程進入阻塞狀態

Serial 收集器

Serial收集器是最基本的、發展歷史最悠久的收集器

特點:單線程、簡單高效(與其他收集器的單線程相比),採用複製演算法。對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)

ParNew 收集器

ParNew收集器其實就是Serial收集器的多線程版本

特點:多線程、ParNew收集器預設開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。和Serial收集器一樣存在Stop The World問題

Serial Old 收集器

Serial Old是Serial收集器的老年代版本

特點:同樣是單線程收集器,採用標記-整理演算法

吞吐量優先

  • 多線程
  • 堆記憶體較大,多核CPU
  • 單位時間內,STW(stop the world,停掉其他所有工作線程)時間最短
  • JDK1.8預設使用的垃圾回收器

img

Parallel Scavenge 收集器

與吞吐量關係密切,故也稱為吞吐量優先收集器

特點:屬於新生代收集器也是採用複製演算法的收集器(用到了新生代的幸存區),又是並行的多線程收集器(與ParNew收集器類似)

該收集器的目標是達到一個可控制的吞吐量。還有一個值得關註的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)

GC自適應調節策略:Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。

Parallel Scavenge收集器使用兩個參數控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
  • XX:GCRatio 直接設置吞吐量的大小
Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

特點:多線程,採用標記-整理演算法(老年代沒有幸存區)

響應時間優先

  • 多線程
  • 堆記憶體較大,多核CPU
  • 儘可能讓單次STW時間變短(儘量不影響其他線程運行)

img

CMS 收集器

Concurrent Mark Sweep,一種以獲取最短回收停頓時間為目標的老年代收集器

特點:基於標記-清除演算法實現。併發收集、低停頓,但是會產生記憶體碎片

應用場景:適用於註重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程式、b/s服務

CMS收集器的運行過程分為下列4步:

初始標記:標記GC Roots直接關聯的對象

併發標記:遍歷整個對象圖

重新標記:修正併發標記期間由於用戶線程運作而標記產生變動的那一部分對象。仍然存在Stop The World問題

併發清除:對標記的對象進行清除回收

CMS收集器的記憶體回收過程是與用戶線程一起併發執行的

G1

定義:

Garbage First

JDK 9以後預設使用,而且替代了CMS 收集器

img

適用場景
  • 同時註重吞吐量和低延遲(響應時間)
  • 超大堆記憶體(記憶體大的),會將堆記憶體劃分為多個大小相等的區域
  • 整體上是標記-整理演算法,兩個區域之間是複製演算法

相關參數:JDK8 並不是預設開啟的,所需要參數開啟

img

G1垃圾回收階段

img

新生代伊甸園垃圾回收—–>記憶體不足,新生代回收+併發標記—–>回收新生代伊甸園、幸存區、老年代記憶體——>新生代伊甸園垃圾回收(重新開始)

Young Collection

分區演算法region

分代是按對象的生命周期劃分,分區則是將堆空間劃分連續幾個不同小區間,每一個小區間獨立回收,可以控制一次回收多少個小區間,方便控制 GC 產生的停頓時間

E:伊甸園 S:幸存區 O:老年代

  • 會STW

img

img

img

Young Collection + CM

CM:併發標記

  • 在 Young GC 時會對 GC Root 進行初始標記
  • 在老年代占用堆記憶體的比例達到閾值時,對進行併發標記(不會STW),閾值可以根據用戶來進行設定

img

Mixed Collection

會對E S O 進行全面的回收

  • 最終標記
  • 拷貝存活

-XX:MaxGCPauseMills:xxx 用於指定最長的停頓時間

為什麼不是所有的老年代會被回收?

因為指定了最大停頓時間,如果對所有老年代都進行回收,耗時可能過高。為了保證時間不超過設定的停頓時間,會回收最有價值的老年代(回收後,能夠得到更多記憶體)

img

Full GC

G1在老年代記憶體不足時(老年代所占記憶體超過閾值)

  • 如果垃圾產生速度慢於垃圾回收速度,不會觸發Full GC,還是併發地進行清理
  • 如果垃圾產生速度快於垃圾回收速度,便會觸發Full GC
 

5、GC 調優

查看虛擬機參數命令

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"Copy

可以根據參數去查詢具體的信息

調優領域

  • 記憶體
  • 鎖競爭
  • CPU占用
  • IO
  • GC

確定目標

低延遲/高吞吐量? 選擇合適的GC

  • CMS G1 ZGC
  • ParallelGC
  • Zing

最快的GC是不發生GC

首先排除減少因為自身編寫的代碼而引發的記憶體問題

  • 查看Full GC前後的記憶體占用,考慮以下幾個問題
    • 數據是不是太多?
    • 數據表示是否太臃腫
      • 對象圖
      • 對象大小
    • 是否存在記憶體泄漏

新生代調優

  • 新生代的特點
    • 所有的new操作分配記憶體都是非常廉價的
      • TLAB
    • 死亡對象回收零代價
    • 大部分對象用過即死(朝生夕死)
    • MInor GC 所用時間遠小於Full GC
  • 新生代記憶體越大越好麽?
    • 不是
      • 新生代記憶體太小:頻繁觸發Minor GC,會STW,會使得吞吐量下降
      • 新生代記憶體太大:老年代記憶體占比有所降低,會更頻繁地觸發Full GC。而且觸發Minor GC時,清理新生代所花費的時間會更長
    • 新生代記憶體設置為內容納[併發量*(請求-響應)]的數據為宜

幸存區調優

  • 幸存區需要能夠保存 當前活躍對象+需要晉升的對象
  • 晉升閾值配置得當,讓長時間存活的對象儘快晉升

老年代調優

四、類載入與位元組碼技術

img

1、類文件結構

首先獲得.class位元組碼文件

方法:

  • 在文本文檔里寫入java代碼(文件名與類名一致),將文件類型改為.java
  • java終端中,執行javac X:…\XXX.java

img

以下是位元組碼文件

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14Copy

根據 JVM 規範,類文件結構如下

u4              magic
u2             minor_version;    
u2             major_version;    
u2             constant_pool_count;    
cp_info        constant_pool[constant_pool_count-1];    
u2             access_flags;    
u2             this_class;    
u2             super_class;   
u2             interfaces_count;    
u2             interfaces[interfaces_count];   
u2             fields_count;    
field_info     fields[fields_count];   
u2             methods_count;    
method_info    methods[methods_count];    
u2             attributes_count;    
attribute_info attributes[attributes_count];Copy

 

2、位元組碼指令

 

javap工具

Oracle 提供了 javap 工具來反編譯 class 文件

javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
F:\Thread_study>javap -v F:\Thread_study\src\com\nyima\JVM\day5\Demo1.class
Classfile /F:/Thread_study/src/com/nyima/JVM/day5/Demo1.class
  Last modified 2020-6-6; size 434 bytes
  MD5 checksum df1dce65bf6fb0b4c1de318051f4a67e
  Compiled from "Demo1.java"
public class com.nyima.JVM.day5.Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // com/nyima/JVM/day5/Demo1
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Demo1.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               com/nyima/JVM/day5/Demo1
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public com.nyima.JVM.day5.Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
}Copy

 

通過位元組碼指令來分析問題


構造方法

cinit()V
public class Demo3 {
    static int i = 10;

    static {
        i = 20;
    }

    static {
        i = 30;
    }

    public static void main(String[] args) {
        System.out.println(i); //結果為30
    }
}Copy

編譯器會按從上至下的順序,收集所有 static 靜態代碼塊和靜態成員賦值的代碼,合併為一個特殊的方法 cinit()V :

stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #3                  // Field i:I
         5: bipush        20
         7: putstatic     #3                  // Field i:I
        10: bipush        30
        12: putstatic     #3                  // Field i:I
        15: returnCopy
init()V
public class Demo4 {
    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Demo4(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo4 d = new Demo4("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}Copy

編譯器會按從上至下的順序,收集所有 {} 代碼塊和成員變數賦值的代碼,形成新的構造方法,但原始構造方法內的代碼總是在後

Code:
     stack=2, locals=3, args_size=3
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: aload_0
        5: ldc           #2                  // String s1
        7: putfield      #3                  // Field a:Ljava/lang/String;
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       //原始構造方法在最後執行
       28: aload_0
       29: aload_1
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2
       35: putfield      #4                  // Field b:I
       38: returnCopy

3、編譯期處理

所謂的 語法糖 ,其實就是指 java 編譯器把 *.java 源碼編譯為 *.class 位元組碼的過程中,自動生成和轉換的一些代碼,主要是為了減輕程式員的負擔,算是 java 編譯器給我們的一個額外福利

 

預設構造函數

public class Candy1 {

}Copy

經過編譯期優化後

public class Candy1 {
   //這個無參構造器是java編譯器幫我們加上的
   public Candy1() {
      //即調用父類 Object 的無參構造方法,即調用 java/lang/Object." <init>":()V
      super();
   }
}Copy

自動拆裝箱

基本類型和其包裝類型的相互轉換過程,稱為拆裝箱

在JDK 5以後,它們的轉換可以在編譯期自動完成

public class Demo2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}Copy

轉換過程如下

public class Demo2 {
   public static void main(String[] args) {
      //基本類型賦值給包裝類型,稱為裝箱
      Integer x = Integer.valueOf(1);
      //包裝類型賦值給基本類型,稱謂拆箱
      int y = x.intValue();
   }
}Copy

泛型集合取值

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型代碼後會執行 泛型擦除 的動作,即泛型信息在編譯為位元組碼之後就丟失了,實際的類型都當做了 Object 類型來處理:

public class Demo3 {
   public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();
      list.add(10);
      Integer x = list.get(0);
   }
}Copy

對應位元組碼

Code:
    stack=2, locals=3, args_size=1
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: bipush        10
      11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      //這裡進行了泛型擦除,實際調用的是add(Objcet o)
      14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

      19: pop
      20: aload_1
      21: iconst_0
      //這裡也進行了泛型擦除,實際調用的是get(Object o)   
      22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//這裡進行了類型轉換,將Object轉換成了Integer
      27: checkcast     #7                  // class java/lang/Integer
      30: astore_2
      31: returnCopy

所以調用get函數取值時,有一個類型轉換的操作

Integer x = (Integer) list.get(0);Copy

如果要將返回結果賦值給一個int類型的變數,則還有自動拆箱的操作

int x = (Integer) list.get(0).intValue();Copy

可變參數

public class Demo4 {
   public static void foo(String... args) {
      //將args賦值給arr,可以看出String...實際就是String[] 
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo("hello", "world");
   }
}Copy

可變參數 String… args 其實是一個 String[] args ,從代碼中的賦值語句中就可以看出來。 同 樣 java 編譯器會在編譯期間將上述代碼變換為:

public class Demo4 {
   public Demo4 {}

    
   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo(new String[]{"hello", "world"});
   }
}Copy

註意,如果調用的是foo(),即未傳遞參數時,等價代碼為foo(new String[]{}),創建了一個空數組,而不是直接傳遞的null

foreach

public class Demo5 {
    public static void main(String[] args) {
        //數組賦初值的簡化寫法也是一種語法糖。
        int[] arr = {1, 2, 3, 4, 5};
        for(int x : arr) {
            System.out.println(x);
        }
    }
}Copy

編譯器會幫我們轉換為

public class Demo5 {
    public Demo5 {}

    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        for(int i=0; i<arr.length; ++i) {
            int x = arr[i];
            System.out.println(x);
        }
    }
}Copy

如果是集合使用foreach

public class Demo5 {
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      for (Integer x : list) {
         System.out.println(x);
      }
   }
}Copy

集合要使用foreach,需要該集合類實現了Iterable介面,因為集合的遍歷需要用到迭代器Iterator

public class Demo5 {
    public Demo5 {}
    
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      //獲得該集合的迭代器
      Iterator<Integer> iterator = list.iterator();
      while(iterator.hasNext()) {
         Integer x = iterator.next();
         System.out.println(x);
      }
   }
}Copy

switch字元串

public class Demo6 {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}Copy

在編譯器中執行的操作

public class Demo6 {
   public Demo6() {
      
   }
   public static void main(String[] args) {
      String str = "hello";
      int x = -1;
      //通過字元串的hashCode+value來判斷是否匹配
      switch (str.hashCode()) {
         //hello的hashCode
         case 99162322 :
            //再次比較,因為字元串的hashCode有可能相等
            if(str.equals("hello")) {
               x = 0;
            }
            break;
         //world的hashCode
         case 11331880 :
            if(str.equals("world")) {
               x = 1;
            }
            break;
         default:
            break;
      }

      //用第二個switch在進行輸出判斷
      switch (x) {
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}Copy

過程說明:

  • 在編譯期間,單個的switch被分為了兩個
    • 第一個用來匹配字元串,並給x賦值
      • 字元串的匹配用到了字元串的hashCode,還用到了equals方法
      • 使用hashCode是為了提高比較效率,使用equals是防止有hashCode衝突(如BM和C.)
    • 第二個用來根據x的值來決定輸出語句

switch枚舉

public class Demo7 {
   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      switch (sex) {
         case MALE:
            System.out.println("man");
            break;
         case FEMALE:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}Copy

編譯器中執行的代碼如下

public class Demo7 {
   /**     
    * 定義一個合成類(僅 jvm 使用,對我們不可見)     
    * 用來映射枚舉的 ordinal 與數組元素的關係     
    * 枚舉的 ordinal 表示枚舉對象的序號,從 0 開始     
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1     
    */ 
   static class $MAP {
      //數組大小即為枚舉元素個數,裡面存放了case用於比較的數字
      static int[] map = new int[2];
      static {
         //ordinal即枚舉元素對應所在的位置,MALE為0,FEMALE為1
         map[SEX.MALE.ordinal()] = 1;
         map[SEX.FEMALE.ordinal()] = 2;
      }
   }

   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      //將對應位置枚舉元素的值賦給x,用於case操作
      int x = $MAP.map[sex.ordinal()];
      switch (x) {
         case 1:
            System.out.println("man");
            break;
         case 2:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}Copy

枚舉類

enum SEX {
   MALE, FEMALE;
}Copy

轉換後的代碼

public final class Sex extends Enum<Sex> {   
   //對應枚舉類中的元素
   public static final Sex MALE;    
   public static final Sex FEMALE;    
   private static final Sex[] $VALUES;
   
    static {       
        //調用構造函數,傳入枚舉元素的值及ordinal
        MALE = new Sex("MALE", 0);    
        FEMALE = new Sex("FEMALE", 1);   
        $VALUES = new Sex[]{MALE, FEMALE}; 
   }
     
   //調用父類中的方法
    private Sex(String name, int ordinal) {     
        super(name, ordinal);    
    }
   
    public static Sex[] values() {  
        return $VALUES.clone();  
    }
    public static Sex valueOf(String name) { 
        return Enum.valueOf(Sex.class, name);  
    } 
   
}Copy

匿名內部類

public class Demo8 {
   public static void main(String[] args) {
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println("running...");
         }
      };
   }
}Copy

轉換後的代碼

public class Demo8 {
   public static void main(String[] args) {
      //用額外創建的類來創建匿名內部類對象
      Runnable runnable = new Demo8$1();
   }
}

//創建了一個額外的類,實現了Runnable介面
final class Demo8$1 implements Runnable {
   public Demo8$1() {}

   @Override
   public void run() {
      System.out.println("running...");
   }
}Copy

如果匿名內部類中引用了局部變數

public class Demo8 {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}Copy

轉化後代碼

public class Demo8 {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

final class Demo8$1 implements Runnable {
   //多創建了一個變數
   int val$x;
   //變為了有參構造器
   public Demo8$1(int x) {
      this.val$x = x;
   }

   @Override
   public void run() {
      System.out.println(val$x);
   }
}Copy

4、類載入階段

載入

  • 將類的位元組碼載入

    方法區

    (1.8後為元空間,在本地記憶體中)中,內部採用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:

    • _java_mirror 即 java 的類鏡像,例如對 String 來說,它的鏡像類就是 String.class,作用是把 klass 暴露給 java 使用
    • _super 即父類
    • _fields 即成員變數
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即類載入器
    • _vtable 虛方法表
    • _itable 介面方法
  • 如果這個類還有父類沒有載入,先載入父類

  • 載入和鏈接可能是交替運行的

img

  • instanceKlass保存在方法區。JDK 8以後,方法區位於元空間中,而元空間又位於本地記憶體中
  • _java_mirror則是保存在堆記憶體中
  • InstanceKlass和*.class(JAVA鏡像類)互相保存了對方的地址
  • 類的對象在對象頭中保存了*.class的地址。讓對象可以通過其找到方法區中的instanceKlass,從而獲取類的各種信息

鏈接

驗證

驗證類是否符合 JVM規範,安全性檢查

準備

為 static 變數分配空間,設置預設值

  • static變數在JDK 7以前是存儲與instanceKlass末尾。但在JDK 7以後就存儲在_java_mirror末尾了
  • static變數在分配空間和賦值是在兩個階段完成的。分配空間在準備階段完成,賦值在初始化階段完成
  • 如果 static 變數是 final 的基本類型,以及字元串常量,那麼編譯階段值就確定了,賦值在準備階段完成
  • 如果 static 變數是 final 的,但屬於引用類型,那麼賦值也會在初始化階段完成
解析

將常量池中的符號引用解析為直接引用

  • 未解析時,常量池中的看到的對象僅是符號,未真正的存在於記憶體中
  • <
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • web系統架構體系 B/S(Browser/Server):瀏覽器實現 優點: 規範、使用方便、本身實現成本低 容易升級、便於維護 缺點: 沒有網路,無法使用 保存數據量有限,和伺服器交互頻率高、耗費流量 安全性差一點 C/S(Client/Server):客戶端實現 優點: 可以在無網路環境下使用 ...
  • 這是一款支持多種語言的源代碼編輯器editrocket,這款編輯器有著非常強的相容性,易學易用,具有語法高亮,代碼創建和sidekicks ,導航等多種功能,是初學代碼小伙伴們的得力助手。 詳情:EditRocket for Mac(源代碼編輯器) 功能亮點 源代碼編輯器 包括巨集,編碼插入,插件,超 ...
  • 實現簡單的信息錄入系統: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport ...
  • 參數過長 影響: 方法不易被理解、使用,方法簽名容易不穩定,不易維護 解決方法:反覆使用提煉方法+內聯方法,消除多餘參數 ​ 儘量把方法移進相關的類中 ​ 如實體類中的get方法在其他類中沒有被調用可以刪除 ​ 實際工作中,可以結合參數數量、以及自身對業務的理解,在 最小知道 和 保持對象完整性 之 ...
  • 很多開發同學對SQL優化如數家珍,卻對MySQL架構一知半解。豈不是只見樹葉,不見森林,終將陷入細節中不能自拔。 今天就一塊學習MySQL分層架構,深入瞭解MySQL底層實現原理,以及每層的作用,我們常見的SQL優化到底在哪一層做了優化? ...
  • 整體流程分析 SpringBoot的配置文件有兩種 ,一種是 properties文件,一種是yml文件。在SpringBoot啟動過程中會對這些文件進行解析載入。在SpringBoot啟動的過程中,配置文件查找和解析的邏輯在listeners.environmentPrepared(environ ...
  • 1、一些C++基礎知識 模板類string的設計屬於底層,其中運用到了很多C++的編程技巧,比如模板、迭代器、友元、函數和運算符重載、內聯等等,為了便於後續理解string類,這裡先對涉及到的概念做個簡單的介紹。C++基礎比較扎實的童鞋可以直接跳到第三節。 1.1 typedef 1.1.1 四種常 ...
  • [] 在運行代碼時,我們常常想要知道代碼運行記憶體占用情況。很多情況下我們想知道是不是自己哪個迴圈寫的太繁瑣了,或者是在哪一步可以用更小的記憶體來實現等。所以需要我們對程式整體、某一部分代碼、某一變數運行記憶體使用有更直觀的瞭解。 在本地運行代碼如PyCharm和遠程運行代碼如VScode中都是如何查看內 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...