1. equels和==的區別 equals方法用於比較對象的內容是否相等,可以根據自定義的邏輯來定義相等的條件,而==操作符用於比較對象的引用是否相等,即它們是否指向同一塊記憶體地址。equals方法是一個 實例方法,可以被所有的Java對象調用,而==操作符可以用於比較對象的引用或基本數據類型的值 ...
1. equels和==的區別
equals
方法用於比較對象的內容是否相等,可以根據自定義的邏輯來定義相等的條件,而==
操作符用於比較對象的引用是否相等,即它們是否指向同一塊記憶體地址。equals
方法是一個
實例方法,可以被所有的Java對象調用,而==
操作符可以用於比較對象的引用或基本數據類型的值。equals
方法的行為可以被重寫,以適應特定的比較需求,而==
操作符的行為不可修改。
2. 垃圾回收機制
垃圾回收是一種在堆記憶體中找出哪些對象在被使用,還有哪些對象沒被使用,並且將後者回收掉的機制。所謂使用中的對象,指的是程式中還有引用的對象;而未使用中的對象,指的是程
序中已經沒有引用的對象,該對象占用的記憶體也可以被回收掉。垃圾回收的第一步是標記。垃圾回收器此時會找出記憶體哪些在使用中,哪些不是。垃圾回收的第二步是清除,這一步會刪掉標記
出的未引用對象。記憶體分配器會保留指向可用記憶體中的引用,以分配給新的對象。垃圾回收的第三步是壓縮,為了提升性能,刪除了未引用對象後,還可以將剩下的已引用對象放在一起(壓
縮),這樣就能更簡單快捷地分配新對象了。逐一標記和壓縮 Java 虛擬機中的所有對象非常低效:分配的對象越多,垃圾回收需要的時間就越久。不過,根據統計,大部分的對象,其實用沒
多久就不用了。
Java 堆(Java Heap)是 JVM 所管理的記憶體中最大的一塊,堆又是垃圾收集器管理的主要區域,這裡我們主要分析一下 Java 堆的結構。
Java 堆主要分為 2 個區域-年輕代與老年代,其中年輕代又分 Eden 區和 Survivor 區,其中 Survivor 區又分 From 和 To 2 個區。可能這時候大家會有疑問,為什麼需要 Survivor 區,為什麼 Survivor 還要分 2 個區。
大多數情況下,對象會在新生代 Eden 區中進行分配。當 Eden 區沒有足夠空間進行分配時,虛擬機會發起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也更快。
通過 Minor GC 之後,Eden 會被清空,Eden 區中絕大部分對象會被回收,而那些無需回收的存活對象,將會進到 Survivor 的 From 區(若 From 區不夠,則直接進入 Old 區)。
Survivor 區相當於是 Eden 區和 Old 區的一個緩衝,類似於我們交通燈中的黃燈。Survivor 又分為 2 個區,一個是 From 區,一個是 To 區。每次執行 Minor GC,會將 Eden 區和 From 存活的對象放到 Survivor 的 To 區(如果 To 區不夠,則直接進入 Old 區)。
之所以有 Survivor 區是因為如果沒有 Survivor 區,Eden 區每進行一次 Minor GC,存活的對象就會被送到老年代,老年代很快就會被填滿。而有很多對象雖然一次 Minor GC 沒有消滅,但其實也並不會蹦躂多久,或許第二次,第三次就需要被清除。這時候移入老年區,很明顯不是一個明智的決定。
所以,Survivor 的存在意義就是減少被送到老年代的對象,進而減少 Major GC 的發生。Survivor 的預篩選保證,只有經歷 16 次 Minor GC 還能在新生代中存活的對象,才會被送到老年代。
設置兩個 Survivor 區最大的好處就是解決記憶體碎片化。
我們先假設一下,Survivor 如果只有一個區域會怎樣。Minor GC 執行後,Eden 區被清空了,存活的對象放到了 Survivor 區,而之前 Survivor 區中的對象,可能也有一些是需要被清除的。問題來了,這時候我們怎麼清除它們?在這種場景下,我們只能標記清除,而我們知道標記清除最大的問題就是記憶體碎片,在新生代這種經常會消亡的區域,採用標記清除必然會讓記憶體產生嚴重的碎片化。因為 Survivor 有 2 個區域,所以每次 Minor GC,會將之前 Eden 區和 From 區中的存活對象複製到 To 區域。第二次 Minor GC 時,From 與 To 職責互換,這時候會將 Eden 區和 To 區中的存活對象再複製到 From 區域,以此反覆。
這種機制最大的好處就是,整個過程中,永遠有一個 Survivor space 是空的,另一個非空的 Survivor space 是無碎片的。那麼,Survivor 為什麼不分更多塊呢?比方說分成三個、四個、五個?顯然,如果 Survivor 區再細分下去,每一塊的空間就會比較小,容易導致 Survivor 區滿,兩塊 Survivor 區可能是經過權衡之後的最佳方案。
老年代占據著 2/3 的堆記憶體空間,只有在 Major GC 的時候才會進行清理,每次 GC 都會觸發“Stop-The-World”。記憶體越大,STW 的時間也越長,所以記憶體也不僅僅是越大就越好。在記憶體擔保機制下,無法安置的對象會直接進到老年代,以下幾種情況也會進入老年代。
1)大對象,指需要大量連續記憶體空間的對象,這部分對象不管是不是“朝生夕死”,都會直接進到老年代。這樣做主要是為了避免在 Eden 區及 2 個 Survivor 區之間發生大量的記憶體複製。
2)長期存活對象,虛擬機給每個對象定義了一個對象年齡(Age)計數器。正常情況下對象會不斷的在 Survivor 的 From 區與 To 區之間移動,對象在 Survivor 區中每經歷一次 Minor GC,年齡就增加 1 歲。當年齡增加到 15 歲時,這時候就會被轉移到老年代。當然,這裡的 15,JVM 也支持進行特殊設置。
3)動態對象年齡,虛擬機並不重視要求對象年齡必須到 15 歲,才會放入老年區,如果 Survivor 空間中相同年齡所有對象大小的總合大於 Survivor 空間的一半,年齡大於等於該年齡的對象就可以直接進去老年區,無需等你“成年”。
這其實有點類似於負載均衡,輪詢是負載均衡的一種,保證每台機器都分得同樣的請求。看似很均衡,但每台機的硬體不通,健康狀況不同,我們還可以基於每台機接受的請求數,或每台機的響應時間等,來調整我們的負載均衡演算法。
3. String、StringBuffer、StringBuilder的區別
在Java中,String
、StringBuffer
和StringBuilder
都是用於處理字元串的類,但它們在性能、線程安全性和可變性方面存在一些區別。
String(字元串): String
是Java中最常用的字元串類,它是不可變的(immutable)。這意味著一旦創建了一個String
對象,它的值就不能被修改。每次對String
的操作(例如連接、替換等)都會創建一個新的String
對象。這種不可變性使得String
具有線程安全性,適合在多線程環境下使用。然而,頻繁的字元串操作可能會導致記憶體開銷較大,因為每次操作都會創建新的對象。
String str = "Hello"; str += " World"; // 創建了一個新的String對象 ```
StringBuffer(字元串緩衝區): StringBuffer
是可變的(mutable)字元串類,它可以進行多次修改而無需創建新的對象。StringBuffer
是線程安全的,適用於多線程環境下的字元串操作。它提供了多個方法用於對字元串進行修改、連接、插入和刪除等操作。
StringBuffer sb = new StringBuffer("Hello"); sb.append(" World"); // 在原對象上進行修改,無需創建新對象 ``` 由於`StringBuffer`是線程安全的,它的執行速度相對較慢。因此,如果在單線程環境下進行字元串操作,推薦使用`StringBuilder`,因為它的執行速度更快。
StringBuilder(字元串構建器): StringBuilder
也是可變的字元串類,類似於StringBuffer
,它可以進行多次修改而無需創建新的對象。StringBuilder
不是線程安全的,因此在多線程環境下使用時需要進行外部同步。由於不需要額外的線程安全檢查,StringBuilder
的執行速度相對較快。
StringBuilder sb = new StringBuilder("Hello"); sb.append(" World"); // 在原對象上進行修改,無需創建新對象 ```
總結:
- 如果需要頻繁操作字元串,並且在多線程環境下使用,應該使用
StringBuffer
。 - 如果需要頻繁操作字元串,但在單線程環境下使用,應該使用
StringBuilder
,因為它的執行速度更快。 - 如果不需要頻繁操作字元串,或者字元串是不可變的,可以使用
String
。
4. 操作字元串常見的類及方法
String
類:String
是Java中最常用的字元串類,它提供了許多方法來處理字元串。以下是一些示例:
String str1 = "Hello"; String str2 = "World"; // 連接字元串 String result1 = str1 + str2; // 結果為 "HelloWorld" // 獲取字元串長度 int length = str1.length(); // 結果為 5 // 檢查字元串是否為空 boolean isEmpty = str1.isEmpty(); // 結果為 false // 檢查字元串是否包含指定字元 boolean contains = str1.contains("H"); // 結果為 true // 提取子字元串 String subStr = str1.substring(1, 4); // 結果為 "ell" // 替換字元 String replacedStr = str1.replace("H", "J"); // 結果為 "Jello" // 拆分字元串 String[] parts = str1.split("l"); // 結果為 ["He", "", "o"] // 轉換為大寫或小寫 String upperCase = str1.toUpperCase(); // 結果為 "HELLO" String lowerCase = str1.toLowerCase(); // 結果為 "hello"
StringBuilder
類:StringBuilder
用於構建可變字元串,它提供了一系列方法來進行字元串的拼接和修改。以下是一些示例:
StringBuilder sb = new StringBuilder(); // 追加字元串 sb.append("Hello"); sb.append("World"); // 插入字元串 sb.insert(5, " "); // 替換字元串 sb.replace(6, 11, "Java"); // 刪除字元 sb.deleteCharAt(5); // 反轉字元串 sb.reverse(); String result2 = sb.toString(); // 結果為 "avaJdlroW"
StringBuffer
類:StringBuffer
與StringBuilder
類似,也用於構建可變字元串。不同的是,StringBuffer
是線程安全的,適用於多線程環境下的字元串操作。以下是一個示例:
StringBuffer buffer = new StringBuffer(); buffer.append("Hello"); buffer.append("World"); String result3 = buffer.toString(); // 結果為 "HelloWorld"
5. Static的用法和作用
static
是Java中的一個關鍵字,可以應用於變數、方法和代碼塊。它具有以下幾種用法和作用:
靜態變數(Static Variables):使用 static
關鍵字聲明的變數稱為靜態變數,也稱為類變數。靜態變數屬於類而不是實例,它在類載入時被初始化,並且在整個程式執行期間保持不變。靜態變數可以通過類名直接訪問,無需創建類的實例。靜態變數常用於表示在類的所有實例之間共用的數據。
public class MyClass { static int count = 0; // 靜態變數 public MyClass() { count++; // 每次創建實例時,靜態變數 count 自增 } }
靜態方法(Static Methods):使用 static
關鍵字聲明的方法稱為靜態方法。靜態方法屬於類而不是實例,它可以在類載入時直接調用,無需創建類的實例。靜態方法只能訪問靜態變數和調用其他靜態方法,不能直接訪問實例變數或調用實例方法。
public class MathUtils { public static int add(int a, int b) { // 靜態方法 return a + b; } }
靜態代碼塊(Static Initialization Blocks):靜態代碼塊用於在類載入時執行一些初始化操作。它使用 static
關鍵字定義,並用花括弧括起來的代碼塊。靜態代碼塊只執行一次,且在類的第一次使用時執行。
public class MyClass { static { // 靜態代碼塊 // 執行一些初始化操作 } }
靜態代碼塊通常用於初始化靜態變數或執行其他與類相關的初始化操作。
靜態導入(Static Import):靜態導入用於在代碼中直接使用靜態成員(變數或方法),而無需使用類名限定符。通過使用 import static
語法,可以導入靜態成員,使其在代碼中可直接訪問。
import static java.lang.Math.PI; public class MyClass { public double calculateArea(double radius) { return PI * radius * radius; // 直接使用靜態變數 PI,無需使用 Math.PI } }