1. 談談你對Java平臺的理解?java
Java 自己是一種面向對象的語言,最顯著的特性有兩個方面,一是所謂的「書寫一次,處處運行」(Write once, run anywhere),可以很是容易地得到跨平臺能力;另外就是垃圾收集(GC, Garbage Collection),Java 經過垃圾收集器(Garbage Collector)回收分配內存,大部分狀況下,程序員不須要本身操心內存的分配和回收。程序員
咱們平常會接觸到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。JRE,也就是 Java 運行環境,包含了 JVM 和 Java 類庫,以及一些模塊等。而 JDK 能夠看做是JRE 的一個超集,提供了更多工具,好比編譯器、各類診斷工具等。算法
對於「Java 是解釋執行」這句話,這個說法不太準確。咱們開發的 Java 的源代碼,首先經過Javac 編譯成爲字節碼(bytecode),而後,在運行時,經過 Java 虛擬機(JVM)內嵌的解釋器將字節碼轉換成爲最終的機器碼。可是常見的 JVM,好比咱們大多數狀況使用的 Oracle JDK提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)編譯器,也就是一般所說的動態編譯器,JIT 可以在運行時將熱點代碼編譯成機器碼,這種狀況下部分熱點代碼就屬於編譯執行,而不是解釋執行了。sql
2. Exception和Error有什麼區別?數據庫
Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例才能夠被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。Exception 和 Error 體現了 Java 平臺設計者對不一樣異常狀況的分類。Exception 是程序正常運行中,能夠預料的意外狀況,可能而且應該被捕獲,進行相應處理。編程
Error 是指在正常狀況下,不大可能出現的狀況,絕大部分的 Error 都會致使程序(好比 JVM自身)處於非正常的、不可恢復狀態。既然是非正常狀況,因此不便於也不須要捕獲,常見的好比 OutOfMemoryError 之類,都是 Error 的子類。後端
Exception 又分爲可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼裏必須顯式地進行捕獲處理,這是編譯期檢查的一部分。前面我介紹的不可查的 Error,是Throwable 不是 Exception。不檢查異常就是所謂的運行時異常,相似 NullPointerException、ArrayIndexOutOfBoundsException 之類,一般是能夠編碼避免的邏輯錯誤,具體根據須要來判斷是否須要捕獲,並不會在編譯期強制要求。設計模式
3. 談談final、finally、 finalize有什麼不一樣?數組
final 能夠用來修飾類、方法、變量,分別有不一樣的意義,final 修飾的 class 表明不能夠繼承擴展,final 的變量是不能夠修改的,而 final 的方法也是不能夠重寫的(override)。緩存
finally 則是 Java 保證重點代碼必定要被執行的一種機制。咱們可使用 try-finally 或者 try-catch-finally 來進行相似關閉 JDBC 鏈接、保證 unlock 鎖等動做。
finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9 開始被標記爲deprecated。
4. 強引用、軟引用、弱引用、幻象引用有什麼區別?
不一樣的引用類型,主要體現的是對象不一樣的可達性(reachable)狀態和對垃圾收集的影響。所謂強引用("Strong" Reference),就是咱們最多見的普通對象引用,只要還有強引用指向一個對象,就能代表對象還「活着」,垃圾收集器不會碰這種對象。對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲 null,就是能夠被垃圾收集的了,固然具體回收時機仍是要看垃圾收集策略。
軟引用(SoftReference),是一種相對強引用弱化一些的引用,可讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出OutOfMemoryError 以前,清理軟引用指向的對象。軟引用一般用來實現內存敏感的緩存,若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
弱引用(WeakReference)並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就能夠用來構建一種沒有特定約束的關係,好比,維護一種非強制性的映射關係,若是試圖獲取時對象還在,就使用它,不然重現實例化。它一樣是不少緩存實現的選擇。
對於幻象引用,有時候也翻譯成虛引用,你不能經過它訪問對象。幻象引用僅僅是提供了一種確保對象被 finalize 之後,作某些事情的機制,好比,一般用來作所謂的 Post-Mortem 清理機制,我在專欄上一講中介紹的 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的建立和銷燬。
5. String、StringBuffer、StringBuilder有什麼區別?
String 是 Java 語言很是基礎和重要的類,提供了構造和管理字符串的各類基本邏輯。它是典型的 Immutable 類,被聲明成爲 final class,全部屬性也都是 final 的。也因爲它的不可變性,相似拼接、裁剪字符串等動做,都會產生新的 String 對象。因爲字符串操做的廣泛性,因此相關操做的效率每每對應用性能有明顯影響。
StringBuffer 是爲解決上面提到拼接產生太多中間對象的問題而提供的一個類,它是 Java 1.5中新增的,咱們能夠用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,因此除非有線程安全的須要,否則仍是推薦使用它的後繼者,也就是StringBuilder。
StringBuilder 在能力上和 StringBuffer 沒有本質區別,可是它去掉了線程安全的部分,有效減少了開銷,是絕大部分狀況下進行字符串拼接的首選。
6. 動態代理是基於什麼原理?
反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。經過反射咱們能夠直接操做類或者對象,好比獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至能夠運行時修改類定義。
動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,不少場景都是利用相似機制作到的,好比用來包裝 RPC 調用、面向切面的編程(AOP)。實現動態代理的方式不少,好比 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其餘的實現方式,好比利用傳說中更高性能的字節碼操做機制,相似 ASM、cglib(基於 ASM)、Javassist 等。
7. int和Integer有什麼區別?
int 是咱們常說的整形數字,是 Java 的 8 個原始數據類型(Primitive Types,boolean、byte、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數據類型是例外。
Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數據,而且提供了基本操做,好比數學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 能夠根據上下文,自動進行轉換,極大地簡化了相關編程。
關於 Integer 的值緩存,這涉及 Java 5 中另外一個改進。構建 Integer 對象的傳統方式是直接調用構造器,直接 new 一個對象。可是根據實踐,咱們發現大部分數據操做都是集中在有限的、較小的數值範圍,於是,在 Java 5 中新增了靜態工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。
8. 對比Vector、ArrayList、LinkedList有何區別?
這三者都是實現集合框架中的 List,也就是所謂的有序集合,所以具體功能也比較近似,好比都提供按照位置進行定位、添加或者刪除的操做,都提供迭代器以遍歷其內容等。但由於具體的設計區別,在行爲、性能、線程安全等方面,表現又有很大不一樣。
Verctor 是 Java 早期提供的線程安全的動態數組,若是不須要線程安全,並不建議選擇,畢竟同步是有額外開銷的。Vector 內部是使用對象數組來保存數據,能夠根據須要自動的增長容量,當數組已滿時,會建立新的數組,並拷貝原有數組數據。
ArrayList 是應用更加普遍的動態數組實現,它自己不是線程安全的,因此性能要好不少。與Vector 近似,ArrayList 也是能夠根據須要調整容量,不過二者的調整邏輯有所區別,Vector在擴容時會提升 1 倍,而 ArrayList 則是增長 50%。
LinkedList 顧名思義是 Java 提供的雙向鏈表,因此它不須要像上面兩種那樣調整容量,它也不是線程安全的。
9.對比Hashtable、HashMap、TreeMap有什麼不一樣?
Hashtable、HashMap、TreeMap 都是最多見的一些 Map 實現,是以鍵值對的形式存儲和操做數據的容器類型。
Hashtable 是早期 Java 類庫提供的一個哈希表實現,自己是同步的,不支持 null 鍵和值,因爲同步致使的性能開銷,因此已經不多被推薦使用。
HashMap 是應用更加普遍的哈希表實現,行爲上大體上與 HashTable 一致,主要區別在於HashMap 不是同步的,支持 null 鍵和值等。一般狀況下,HashMap 進行 put 或者 get 操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選,好比,實現一個用戶 ID 和用戶信息對應的運行時存儲結構。
TreeMap 則是基於紅黑樹的一種提供順序訪問的 Map,和 HashMap 不一樣,它的 get、put、remove 之類操做都是 O(log(n))的時間複雜度,具體順序能夠由指定的 Comparator 來決定,或者根據鍵的天然順序來判斷。
10. 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
Java 提供了不一樣層面的線程安全支持。在傳統集合框架內部,除了 Hashtable 等同步容器,還提供了所謂的同步包裝器(Synchronized Wrapper),咱們能夠調用 Collections 工具類提供的包裝方法,來獲取一個同步的包裝容器(如 Collections.synchronizedMap),可是它們都是利用很是粗粒度的同步方式,在高併發狀況下,性能比較低下。另外,更加廣泛的選擇是利用併發包提供的線程安全容器類,它提供了:
具體保證線程安全的方式,包括有從簡單的 synchronize 方式,到基於更加精細化的,好比基於分離鎖實現的 ConcurrentHashMap 等併發實現等。具體選擇要看開發的場景需求,整體來講,併發包內提供的容器通用場景,遠優於早期的簡單同步實現。
11. Java提供了哪些IO方式? NIO如何實現多路複用?
首先,傳統的 java.io 包,它基於流模型實現,提供了咱們最熟知的一些 IO 功能,好比 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動做完成以前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。java.io 包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。
12. Java有幾種文件拷貝方式?哪種最高效?
Java 有多種比較典型的文件拷貝實現方式,好比:利用 java.io 類庫,直接爲源文件構建一個FileInputStream 讀取,而後再爲目標文件構建一個FileOutputStream,完成寫入工做。或者,利用 java.nio 類庫提供的 transferTo 或 transferFrom 方法實現。固然,Java 標準類庫自己已經提供了幾種 Files.copy 的實現。對於 Copy 的效率,這個其實與操做系統和配置等狀況相關,整體上來講,NIO transferTo/From 的方式可能更快,由於它更能利用現代操做系統底層機制,避免沒必要要拷貝和上下文切換。
13. 談談接口和抽象類有什麼區別?
接口是對行爲的抽象,它是抽象方法的集合,利用接口能夠達到 API 定義和實現分離的目的。
接口,不能實例化;不能包含任何很是量成員,任何 field 都是隱含着 public static final 的意義;同時,沒有非靜態方法實現,也就是說要麼是抽象方法,要麼是靜態方法。Java 標準類庫中,定義了很是多的接口,好比 java.util.List。
抽象類是不能實例化的類,用 abstract 關鍵字修飾 class,其目的主要是代碼重用。除了不能實例化,形式上和通常的 Java 類並無太大區別,能夠有一個或者多個抽象方法,也能夠沒有抽象方法。抽象類大多用於抽取相關 Java 類的共用方法實現或者是共同成員變量,而後經過繼承的方式達到代碼複用的目的。Java 標準庫中,好比 collection 框架,不少通用部分就被抽取成爲抽象類,例如 java.util.AbstractList。
Java 類實現 interface 使用 implements 關鍵詞,繼承 abstract class 則是使用 extends 關鍵詞,咱們能夠參考 Java 標準庫中的 ArrayList。
14. 談談你知道的設計模式?
大體按照模式的應用目標分類,設計模式能夠分爲建立型模式、結構型模式和行爲型模式。
15. synchronized和ReentrantLock有什麼區別呢?
synchronized 是 Java 內建的同步機制,因此也有人稱其爲 Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其餘試圖獲取的線程只能等待或者阻塞在那裏。
在 Java 5 之前,synchronized 是僅有的同步手段,在代碼中, synchronized 能夠用來修飾方法,也可使用在特定的代碼塊兒上,本質上 synchronized 方法等同於把方法所有語句用synchronized 塊包起來。
ReentrantLock,一般翻譯爲再入鎖,是 Java 5 提供的鎖實現,它的語義和 synchronized 基本相同。再入鎖經過代碼直接調用 lock() 方法獲取,代碼書寫也更加靈活。與此同時,ReentrantLock 提供了不少實用的方法,可以實現不少 synchronized 沒法作到的細節控制,好比能夠控制 fairness,也就是公平性,或者利用定義條件等。可是,編碼中也須要注意,必需要明確調用 unlock() 方法釋放,否則就會一直持有該鎖。
synchronized 和 ReentrantLock 的性能不能一律而論,早期版本 synchronized 在不少場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優於 ReentrantLock。
16. synchronized底層如何實現?什麼是鎖的升級、降級?
在回答這個問題前,先簡單複習一下上一講的知識點。synchronized 代碼塊是由一對兒monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。
在 Java 6 以前,Monitor 的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做。現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操做(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖。這樣作的假設是基於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。
若是有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就須要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。
我注意到有的觀點認爲 Java 不會進行鎖降級。實際上據我所知,鎖降級確實是會發生的,當JVM 進入安全點(SafePoint)的時候,會檢查是否有閒置的 Monitor,而後試圖進行降級。
17. 一個線程兩次調用start()方法會出現什麼狀況?
Java 的線程是不容許啓動兩次的,第二次調用必然會拋出 IllegalThreadStateException,這是一種運行時異常,屢次調用 start 被認爲是編程錯誤。關於線程生命週期的不一樣狀態,在 Java 5 之後,線程狀態被明肯定義在其公共內部枚舉類型java.lang.Thread.State 中,分別是:
public final native void wait(long timeout) throws InterruptedException;
在第二次調用 start() 方法的時候,線程可能處於終止或者其餘(非 NEW)狀態,可是不論如何,都是不能夠再次啓動的。
18. 什麼狀況下Java程序會產生死鎖?如何定位、修復?
死鎖是一種特定的程序狀態,在實體之間,因爲循環依賴致使彼此一直處於等待之中,沒有任何個體能夠繼續前進。死鎖不只僅是在線程之間會發生,存在資源獨佔的進程之間一樣也可能出現死鎖。一般來講,咱們大可能是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,因爲互相持有對方須要的鎖,而永久處於阻塞的狀態。定位死鎖最多見的方式就是利用 jstack 等工具獲取線程棧,而後定位互相之間的依賴關係,進而找到死鎖。若是是比較明顯的死鎖,每每 jstack 等就能直接定位,相似 JConsole 甚至能夠在圖形界面進行有限的死鎖檢測。若是程序運行時發生了死鎖,絕大多數狀況下都是沒法在線解決的,只能重啓、修正程序自己問題。因此,代碼開發階段互相審查,或者利用工具進行預防性排查,每每也是很重要的。
19. Java併發包提供了哪些併發工具類?
咱們一般所說的併發包也就是 java.util.concurrent 及其子包,集中了 Java 併發的各類基礎工具類,具體主要包括幾個方面:
20. 併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?
有時候咱們把併發包下面的全部容器都習慣叫做併發容器,可是嚴格來說,相似ConcurrentLinkedQueue 這種「Concurrent*」容器,纔是真正表明併發。關於問題中它們的區別:
不知道你有沒有注意到,java.util.concurrent 包提供的容器(Queue、List、Set)、Map,從命名上能夠大概區分爲 Concurrent、CopyOnWrite和 Blocking* 等三類,一樣是線程安全容器,能夠簡單認爲:
21. Java併發類庫提供的線程池有哪幾種? 分別有什麼特色?
一般開發者都是利用 Executors 提供的通用線程池建立方法,去建立不一樣配置的線程池,主要區別在於不一樣的 ExecutorService 類型或者不一樣的初始參數。Executors 目前提供了 5 種不一樣的線程池建立配置:
22. AtomicInteger底層實現原理是什麼?如何在本身的產品代碼中應用CAS操做?
AtomicIntger 是對 int 類型的一個封裝,提供原子性的訪問和更新操做,其原子性操做的實現是基於 CAS(compare-and-swap)技術。
所謂 CAS,表徵的是一些列操做的集合,獲取當前數值,進行一些運算,利用 CAS 指令試圖進行更新。若是當前數值未變,表明沒有其餘線程進行併發修改,則成功更新。不然,可能出現不一樣的選擇,要麼進行重試,要麼就返回一個成功或者失敗的結果。
從 AtomicInteger 的內部屬性能夠看出,它依賴於 Unsafe 提供的一些底層能力,進行底層操做;以 volatile 的 value 字段,記錄數值,以保證可見性。
具體的原子操做細節,能夠參考任意一個原子更新方法,好比下面的 getAndIncrement。Unsafe 會利用 value 字段的內存地址偏移,直接完成操做。
由於 getAndIncrement 須要返歸數值,因此須要添加失敗重試邏輯。
而相似 compareAndSet 這種返回 boolean 類型的函數,由於其返回值表現的就是成功與否,因此不須要重試。
CAS 是 Java 併發中所謂 lock-free 機制的基礎。
23. 請介紹類加載過程,什麼是雙親委派模型?
通常來講,咱們把 Java 的類加載過程分爲三個主要步驟:加載、連接、初始化,具體行爲在Java 虛擬機規範裏有很是詳細的定義。
首先是加載階段(Loading),它是 Java 將字節碼數據從不一樣的數據源讀取到 JVM 中,並映射爲 JVM 承認的數據結構(Class 對象),這裏的數據源多是各類各樣的形態,如 jar 文件、class 文件,甚至是網絡數據源等;若是輸入數據不是 ClassFile 的結構,則會拋出ClassFormatError。
加載階段是用戶參與的階段,咱們能夠自定義類加載器,去實現本身的類加載過程。
第二階段是連接(Linking),這是核心的步驟,簡單說是把原始的類定義信息平滑地轉化入JVM 運行的過程當中。這裏可進一步細分爲三個步驟:
再來談談雙親委派模型,簡單說就是當類加載器(Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應類型,不然儘可能將這個任務代理給當前加載器的父加載器去作。使用委派模型的目的是避免重複加載 Java 類型。
24. 有哪些方法能夠在運行時動態生成一個Java類?
咱們能夠從常見的 Java 類來源分析,一般的開發過程是,開發者編寫 Java 代碼,調用 javac編譯成 class 文件,而後經過類加載機制載入 JVM,就成爲應用運行時可使用的 Java 類了。從上面過程獲得啓發,其中一個直接的方式是從源碼入手,能夠利用 Java 程序生成一段源碼,而後保存到文件等,下面就只須要解決編譯問題了。
有一種笨辦法,直接用 ProcessBuilder 之類啓動 javac 進程,並指定上面生成的文件做爲輸入,進行編譯。最後,再利用類加載器,在運行時加載便可。
前面的方法,本質上仍是在當前程序進程以外編譯的,那麼還有沒有不這麼 low 的辦法呢?你能夠考慮使用 Java Compiler API,這是 JDK 提供的標準 API,裏面提供了與 javac 對等的編譯器功能,具體請參考java.compiler相關文檔。進一步思考,咱們一直圍繞 Java 源碼編譯成爲 JVM 能夠理解的字節碼,換句話說,只要是符合 JVM 規範的字節碼,無論它是如何生成的,是否是均可以被 JVM 加載呢?咱們能不能直接生成相應的字節碼,而後交給類加載器去加載呢?
固然也能夠,不過直接去寫字節碼難度太大,一般咱們能夠利用 Java 字節碼操縱工具和類庫來實現,好比在專欄第 6 講中提到的ASM、Javassist、cglib 等。
25. 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
一般能夠把 JVM 內存區域分爲下面幾個方面,其中,有的區域是以線程爲單位,而有的區域則是整個 JVM 進程惟一的。
首先,程序計數器(PC,Program Counter Register)。在 JVM 規範中,每一個線程都有它本身的程序計數器,而且任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的 Java 方法的 JVM 指令地址;或者,若是是在執行本地方法,則是未指定值(undefined)。
第二,Java 虛擬機棧(Java Virtual Machine Stack),早期也叫 Java 棧。每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。前面談程序計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,一般叫做當前幀,方法所在的類叫做當前類。若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,成爲新的當前幀,一直到它返回結果或者執行結束。JVM 直接對 Java 棧的操做只有兩個,就是對棧幀的壓棧和出棧。棧幀中存儲着局部變量表、操做數(operand)棧、動態連接、方法正常退出或者異常退出的定義等。
第三,堆(Heap),它是 Java 內存管理的核心區域,用來放置 Java 對象實例,幾乎全部建立的 Java 對象實例都是被直接分配在堆上。堆被全部的線程共享,在虛擬機啓動時,咱們指定的「Xmx」之類參數就是用來指定最大堆空間等指標。理所固然,堆也是垃圾收集器重點照顧的區域,因此堆內空間還會被不一樣的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。
第四,方法區(Method Area)。這也是全部線程共享的一塊內存區域,用於存儲所謂的元(Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。因爲早期的 Hotspot JVM 實現,不少人習慣於將方法區稱爲永久代(PermanentGeneration)。Oracle JDK 8 中將永久代移除,同時增長了元數據區(Metaspace)。
第五,運行時常量池(Run-Time Constant Pool),這是方法區的一部分。若是仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各類信息,還有一項信息就是常量池。Java 的常量池能夠存放各類常量信息,無論是編譯期生成的各類字面量,仍是須要在運行時決定的符號引用,因此它比通常語言的符號表存儲的信息更加寬泛。
第六,本地方法棧(Native Method Stack)。它和 Java 虛擬機棧是很是類似的,支持對本地方法的調用,也是每一個線程都會建立一個。在 Oracle Hotspot JVM 中,本地方法棧和 Java 虛擬機棧是在同一起區域,這徹底取決於技術實現的決定,並未在規範中強制。
26. 如何監控和診斷JVM堆內和堆外內存使用?
瞭解 JVM 內存的方法有不少,具體能力範圍也有區別,簡單總結以下:
可使用綜合性的圖形化工具,如 JConsole、VisualVM(注意,從 Oracle JDK 9 開始,VisualVM 已經再也不包含在 JDK 安裝包中)等。這些工具具體使用起來相對比較直觀,直接鏈接到 Java 進程,而後就能夠在圖形化界面裏掌握內存使用狀況。
以 JConsole 爲例,其內存頁面能夠顯示常見的堆內存和各類堆外部分使用狀態。
這裏有一個相對特殊的部分,就是是堆外內存中的直接內存,前面的工具基本不適用,可使用JDK 自帶的 Native Memory Tracking(NMT)特性,它會從 JVM 本地內存分配的角度進行解讀。
27. Java常見的垃圾收集器有哪些?
實際上,垃圾收集器(GC,Garbage Collector)是和具體 JVM 實現緊密相關的,不一樣廠商(IBM、Oracle),不一樣版本的 JVM,提供的選擇也不一樣。接下來,我來談談最主流的 OracleJDK。
Serial GC,它是最古老的垃圾收集器,「Serial」體如今其收集工做是單線程的,而且在進行垃圾收集過程當中,會進入臭名昭著的「Stop-The-World」狀態。固然,其單線程設計也意味着精簡的 GC 實現,無需維護複雜的數據結構,初始化也簡單,因此一直是 Client 模式下 JVM 的默認選項。
從年代的角度,一般將其老年代實現單獨稱做 Serial Old,它採用了標記 - 整理(Mark- Compact)算法,區別於新生代的複製算法。
Serial GC 的對應 JVM 參數是:-XX:+UseSerialGC
28. 談談你的GC調優思路?
談到調優,這必定是針對特定場景、特定目的的事情, 對於 GC 調優來講,首先就須要清楚調優的目標是什麼?從性能的角度看,一般關注三個方面,內存佔用(footprint)、延時(latency)和吞吐量(throughput),大多數狀況下調優會側重於其中一個或者兩個方面的目標,不多有狀況能夠兼顧三個不一樣的角度。固然,除了上面一般的三個方面,也可能須要考慮其餘 GC 相關的場景,例如,OOM 也可能與不合理的 GC 相關參數有關;或者,應用啓動速度方面的需求,GC 也會是個考慮的方面。基本的調優思路能夠總結爲:
理解應用需求和問題,肯定調優目標。假設,咱們開發了一個應用服務,但發現偶爾會出現性能抖動,出現較長的服務停頓。評估用戶可接受的響應時間和業務量,將目標簡化爲,但願 GC 暫停儘可能控制在 200ms 之內,而且保證必定標準的吞吐量。
29. Java內存模型中的happen-before是什麼?
Happen-before 關係,是 Java 內存模型中保證多線程操做可見性的機制,也是對早期語言規範中含糊的可見性概念的一個精肯定義。它的具體表現形式,包括但遠不止是咱們直覺中的 synchronized、volatile、lock 操做順序等方面,例如:
這些 happen-before 關係是存在着傳遞性的,若是知足 a happen-before b 和 b happen-before c,那麼 a happen-before c 也成立。
前面我一直用 happen-before,而不是簡單說先後,是由於它不只僅是對執行時間的保證,也包括對內存讀、寫操做順序的保證。僅僅是時鐘順序上的前後,並不能保證線程交互的可見性。
30. Java程序運行在Docker等容器環境有哪些新問題?
對於 Java 來講,Docker 畢竟是一個較新的環境,例如,其內存、CPU 等資源限制是經過CGroup(Control Group)實現的,早期的 JDK 版本(8u131 以前)並不能識別這些限制,進而會致使一些基礎問題:
從應用打包、發佈等角度出發,JDK 自身就比較大,生成的鏡像就更爲臃腫,當咱們的鏡像很是多的時候,鏡像的存儲等開銷就比較明顯了。若是考慮到微服務、Serverless 等新的架構和場景,Java 自身的大小、內存佔用、啓動速度,都存在必定侷限性,由於 Java 早期的優化大可能是針對長時間運行的大型服務器端應用。
31. 你瞭解Java應用開發中的注入攻擊嗎?
注入式(Inject)攻擊是一類很是常見的攻擊方式,其基本特徵是程序容許攻擊者將不可信的動態內容注入到程序中,並將其執行,這就可能徹底改變最初預計的執行過程,產生惡意效果。
下面是幾種主要的注入式攻擊途徑,原則上提供動態執行能力的語言特性,都須要提防發生注入攻擊的可能。
首先,就是最多見的 SQL 注入攻擊。一個典型的場景就是 Web 系統的用戶登陸功能,根據用戶輸入的用戶名和密碼,咱們須要去後端數據庫覈實信息。假設應用邏輯是,後端程序利用界面輸入動態生成相似下面的 SQL,而後讓 JDBC 執行。
Select * from use_info where username = 「input_usr_name」 and password = 「input_pwd」
可是,若是我輸入的 input_pwd 是相似下面的文本,「 or 「」=」那麼,拼接出的 SQL 字符串就變成了下面的條件,OR 的存在致使輸入什麼名字都是複合條件的。
Select * from use_info where username = 「input_usr_name」 and password = 「」 or 「」 = 「」
這裏只是舉個簡單的例子,它是利用了指望輸入和可能輸入之間的誤差。上面例子中,指望用戶輸入一個數值,但實際輸入的則是 SQL 語句片斷。相似場景能夠利用注入的不一樣 SQL 語句,進行各類不一樣目的的攻擊,甚至還能夠加上「;delete xxx」之類語句,若是數據庫權限控制不合理,攻擊效果就多是災難性的。
第二,操做系統命令注入。Java 語言提供了相似 Runtime.exec(…) 的 API,能夠用來執行特定命令,假設咱們構建了一個應用,以輸入文本做爲參數,執行下面的命令:ls –la input_file_name,可是若是用戶輸入是 「input_file_name;rm –rf /*」,這就有可能出現問題了。固然,這只是個舉例,Java 標準類庫自己進行了很是多的改進,因此相似這種編程錯誤,未必能夠真的完成攻擊,但其反映的一類場景是真實存在的。
第三,XML 注入攻擊。Java 核心類庫提供了全面的 XML 處理、轉換等各類 API,而 XML 自身是能夠包含動態內容的,例如 XPATH,若是使用不當,可能致使訪問惡意內容。
還有相似 LDAP 等容許動態內容的協議,都是可能利用特定命令,構造注入式攻擊的,包括XSS(Cross-site Scripting)攻擊,雖然並不和 Java 直接相關,但也可能在 JSP 等動態頁面中發生。
32. 如何寫出安全的Java代碼?
這個問題可能有點寬泛,咱們能夠用特定類型的安全風險爲例,如拒絕服務(DoS)攻擊,分析Java 開發者須要重點考慮的點。
DoS 是一種常見的網絡攻擊,有人也稱其爲「洪水攻擊」。最多見的表現是,利用大量機器發送請求,將目標網站的帶寬或者其餘資源耗盡,致使其沒法響應正經常使用戶的請求。
我認爲,從 Java 語言的角度,更加須要重視的是程序級別的攻擊,也就是利用 Java、JVM 或應用程序的瑕疵,進行低成本的 DoS 攻擊,這也是想要寫出安全的 Java 代碼所必須考慮的。
例如:
因此能夠看出,實現安全的 Java 代碼,須要從功能設計到實現細節,都充分考慮可能的安全影響。
33. 後臺服務出現明顯「變慢」,談談你的診斷思路?
首先,須要對這個問題進行更加清晰的定義:
第二,理清問題的症狀,這更便於定位具體的緣由,有如下一些思路:
34. 有人說「Lambda能讓Java程序慢30倍」,你怎麼看?
我認爲,「Lambda 能讓 Java 程序慢 30 倍」這個爭論實際反映了幾個方面:
第一,基準測試是一個很是有效的通用手段,讓咱們以直觀、量化的方式,判斷程序在特定條件下的性能表現。
第二,基準測試必須明肯定義自身的範圍和目標,不然頗有可能產生誤導的結果。前面代碼片斷自己的邏輯就有瑕疵,更多的開銷是源於自動裝箱、拆箱(auto-boxing/unboxing),而不是源自 Lambda 和 Stream,因此得出的初始結論是沒有說服力的。
第三,雖然 Lambda/Stream 爲 Java 提供了強大的函數式編程能力,可是也須要正視其侷限性:
35. JVM優化Java代碼時都作了什麼?
JVM 在對代碼執行的優化可分爲運行時(runtime)優化和即時編譯器(JIT)優化。運行時優化主要是解釋執行和動態編譯通用的一些機制,好比說鎖機制(如偏斜鎖)、內存分配機制(如TLAB)等。除此以外,還有一些專門用於優化解釋執行效率的,好比說模版解釋器、內聯緩存(inline cache,用於優化虛方法調用的動態綁定)。
JVM 的即時編譯器優化是指將熱點代碼以方法爲單位轉換成機器碼,直接運行在底層硬件之上。它採用了多種優化方式,包括靜態編譯器可使用的如方法內聯、逃逸分析,也包括基於程序運行 profile 的投機性優化(speculative/optimistic optimization)。這個怎麼理解呢?好比我有一條 instanceof 指令,在編譯以前的執行過程當中,測試對象的類一直是同一個,那麼即時編譯器能夠假設編譯以後的執行過程當中還會是這一個類,而且根據這個類直接返回instanceof 的結果。若是出現了其餘類,那麼就拋棄這段編譯後的機器碼,而且切換回解釋執行。
固然,JVM 的優化方式僅僅做用在運行應用代碼的時候。若是應用代碼自己阻塞了,好比說併發時等待另外一線程的結果,這就不在 JVM 的優化範疇啦。
36. 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
所謂隔離級別(Isolation Level),就是在數據庫事務中,爲保證併發數據讀寫的正確性而提出的定義,它並非MySQL專有的概念,而是源於ANSI/ISO制定的SQL-92標準。
每種關係型數據庫都提供了各自特點的隔離級別實現,雖然在一般的 定義 中是以鎖爲實現單元,但實際的實現千差萬別。以最多見的MySQL InnoDB引擎爲例,它是基於MVCC(Multi-Versioning Concurrency Control)和鎖的複合實現,按照隔離程度從低到高,MySQL事務隔離級別分爲四個不一樣層次:
串行化(Serializable),併發事務之間是串行化的,一般意味着讀取須要獲取共享讀鎖,更新須要獲取排他寫鎖,若是SQL使用WHERE語句,還會獲取區間鎖( MySQL 以 GAP 鎖形式實現,可重複讀級別中默認也會使用),這是最高的隔離級別。
至於悲觀鎖和樂觀鎖,也並非 MySQL 或者數據庫中獨有的概念,而是併發編程的基本概念。主要區別在於,操做共享數據時, 「 悲觀鎖 」 即認爲數據出現衝突的可能性更大,而 「 樂觀鎖 」 則是認爲大部分狀況不會出現衝突,進而決定是否採起排他性措施。反映到 MySQL 數據庫應用開發中,悲觀鎖通常就是利用相似 SELECT … FOR UPDATE 這樣的語句,對數據加鎖,避免其餘事務意外修改數據。樂觀鎖則與 Java 併發包中的 AtomicFieldUpdater 相似,也是利用 CAS 機制,並不會對數據加鎖,而是經過對比數據的時間戳或者版本號,來實現樂觀鎖須要的版本判斷。