Java面試考題集錦之Java基礎

這篇文章記錄在準備Java後端面試複習過程中網上常見的考題,同時也會標明題目出現頻率,方便大家參考。有缺少、錯誤的部分歡迎大家補充糾正。–持續更新
推薦https://download.csdn.net/download/qq_41011723/12255299,基本涵蓋了面試題的原理

也歡迎大家來我自己搭建的博客看看,之後博客也會同步更新
hofe’s blog

文章目錄

圖片來源:牛客網面經
在這裏插入圖片描述

數據類型

Java中的八大類型及其包裝類型(佔用字節數)

在這裏插入圖片描述

String類爲什麼是final的?

https://www.jianshu.com/p/9c7f5daac283

string、stringbuilder、stringbuffer區別?

https://www.cnblogs.com/liyy7520/p/11899260.html

String 類的常用方法?

equals:字符串是否相同
compareTo:根據字符串中每個字符的Unicode編碼進行比較
indexOf:目標字符或字符串在源字符串中位置下標
valueOf:其他類型轉字符串
concat:追加字符串到當前字符串
isEmpty:字符串長度是否爲0
contains:是否包含目標字符串

Java面向對象

Java 接口\抽象類區別?

https://www.cnblogs.com/zzfpz/p/10990210.html

重載和重寫?

https://blog.csdn.net/qunqunstyle99/article/details/81007712

Java對象的生命週期?

創建階段 、 應用階段 、不可見階段 、不可達階段 、收集階段 、終結階段、 對象空間重新分配階段。

創建階段系統通過下面的幾個步驟來完成對象的創建過程

  1. 爲對象分配存儲空間
  2. 開始構造對象
  3. 從超類到子類對static成員進行初始化
  4. 超類成員變量按順序初始化,遞歸調用超類的構造方法
  5. 子類成員變量按順序初始化,子類構造方法調用
    一旦對象被創建,並被分派給某些變量賦值,這個對象的狀態就切換到了應用階段

應用階段
對象至少被一個強引用持有着

當一個對象處於不可見階段時,說明程序本身不再持有該對象的任何強引用,雖然該這些引用仍然是存在着的。

簡單說就是程序的執行已經超出了該對象的作用域了

不可達階段(Unreachable)
對象處於不可達階段是指該對象不再被任何強引用所持有。
與“不可見階段”相比,“不可見階段”是指程序不再持有該對象的任何強引用,這種情況下,該對象仍可能被JVM等系統下的某些已裝載的靜態變量或線程或JNI等強引用持有着,這些特殊的強引用被稱爲”GC root”。存在着這些GC root會導致對象的內存泄露情況,無法被回收。

收集階段(Collected)
當垃圾回收器發現該對象已經處於“不可達階段”並且垃圾回收器已經對該對象的內存空間重新分配做好準備時,則對象進入了“收集階段”。則對象進入了“收集階段”。如果該對象已經重寫了finalize()方法,則會去執行該方法的終端操作。

這裏要特別說明一下:不要重載finazlie()方法!原因有兩點:

  1. 會影響JVM的對象分配與回收速度
    在分配該對象時,JVM需要在垃圾回收器上註冊該對象,以便在回收時能夠執行該重載方法;在該方法的執行時需要消耗CPU時間且在執行完該方法後纔會重新執行回收操作,即至少需要垃圾回收器對該對象執行兩次GC。

  2. 可能造成該對象的再次“復活”
    在finalize()方法中,如果有其它的強引用再次持有該對象,則會導致對象的狀態由“收集階段”又重新變爲“應用階段”。這個已經破壞了Java對象的生命週期進程,且“復活”的對象不利用後續的代碼管理。

終結階段
當對象執行完finalize()方法後仍然處於不可達狀態時,則該對象進入終結階段。在該階段是等待垃圾回收器對該對象空間進行回收

對象空間重新分配階段
垃圾回收器對該對象的所佔用的內存空間進行回收或者再分配了,則該對象徹底消失了,稱之爲“對象空間重新分配階段”



List、Set、Map

在這裏插入圖片描述

高頻

List

ArrayList和LinkedList、Vector的區別?

https://blog.csdn.net/renfufei/article/details/17077425
在這裏插入圖片描述

ArrayList和LinkedList的區別?分別用在什麼場景?

①ArrayList和LinkedList可想從名字分析,它們一個是Array(動態數組)的數據結構,一個是Link(鏈表)的數據結構,此外,它們兩個都是對List接口的實現。
前者是數組隊列,相當於動態數組;後者爲雙向鏈表結構,也可當作堆棧、隊列、雙端隊列
②當隨機訪問List時(get和set操作),ArrayList比LinkedList的效率更高,因爲LinkedList是線性的數據存儲方式,所以需要移動指針從前往後依次查找。
③當對數據進行增加和刪除的操作時(add和remove操作),LinkedList比ArrayList的效率更高,因爲ArrayList是數組,所以在其中進行增刪操作時,會對操作點之後所有數據的下標索引造成影響,需要進行數據的移動。
④從利用效率來看,ArrayList自由性較低,因爲它需要手動的設置固定大小的容量,但是它的使用比較方便,只需要創建,然後添加數據,通過調用下標進行使用;而LinkedList自由性較高,能夠動態的隨數據量的變化而變化,但是它不便於使用。
⑤ArrayList主要控件開銷在於需要在lList列表預留一定空間;而LinkList主要控件開銷在於需要存儲結點信息以及結點指針信息。

場景:
鏈表,插入刪除快,查找修改慢。 適用於頻繁增刪的場景。
數組,查找快,插入刪除慢。 適用於頻繁查找和修改的場景。

Set

在這裏插入圖片描述
Set注重獨一無二的性質,該體系集合用於存儲無序(存入和取出的順序不一定相同)元素,值不能重 復。對象的相等性本質是對象hashCode值(java是依據對象的內存地址計算出的此序號)判斷 的,如果想要讓兩個不同的對象視爲相等的,就必須覆蓋Object的hashCode方法和equals方 法

HashSet

重寫equals爲何要重寫hashCode?

答:HashSet首先判斷兩個元素的哈希值,如果哈希值一樣,接着會比較 equals方法 如果 equls結果爲true ,HashSet就視爲同一個元素。如果equals 爲false就不是 同一個元素。

判斷兩個對象是否相等,比較的就是其hashCode, 如果你重載了equals,比如說是基於對象的內容實現的,而保留hashCode的實現不變,那麼很可能某兩個對象明明是“相等”,而hashCode卻不一樣。 hashcode不一樣,就無法認定兩個對象相等了

哈希值相同equals爲false的元素是怎麼存儲呢,就是在同樣的哈希值下順延(可以認爲哈希值相 同的元素放在一個哈希桶中)。也就是哈希一樣的存一列。如圖1表示hashCode值不相同的情 況;圖2表示hashCode值相同,但equals不相同的情況。
在這裏插入圖片描述

TreeSet

  1. TreeSet()是使用二叉樹的原理對新add()的對象按照指定的順序排序(升序、降序),每增 加一個對象都會進行排序,將對象插入的二叉樹指定的位置。
  2. Integer和String對象都可以進行默認的TreeSet排序,而自定義類的對象是不可以的,自 己定義的類必須實現Comparable接口,並且覆寫相應的compareTo()函數,纔可以正常使 用。
  3. 在覆寫compare()函數時,要返回相應的值才能使TreeSet按照一定的規則來排序
  4. 比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整 數、零或正整數

LinkHashSet(HashSet+LinkHashMap)

對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。 LinkedHashSet 底層使用 LinkedHashMap 來保存所有元素,它繼承與 HashSet,其所有的方法 操作上又與HashSet相同,因此LinkedHashSet 的實現上非常簡單,只提供了四個構造方法,並 通過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操 作上與父類HashSet的操作相同,直接調用父類HashSet的方法即可

Map

HashMap

在這裏插入圖片描述
HashMap根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快 的訪問速度,但遍歷順序卻是不確定的。 HashMap多隻允許一條記錄的鍵爲null,允許多條記 錄的值爲 null。HashMap 非線程安全,即任一時刻可以有多個線程同時寫 HashMap,可能會導 致數據的不一致。如果需要滿足線程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有線程安全的能力,或者使用 ConcurrentHashMap。我們用下面這張圖來介紹 HashMap 的結構

Java7實現
數組+鏈表
在這裏插入圖片描述
大方向上,HashMap 裏面是一個數組,然後數組中每個元素是一個單向鏈表。上圖中,每個綠色 的實體是嵌套類 Entry 的實例,Entry 包含四個屬性:key, value, hash 值和用於單向鏈表的 next。

  1. capacity:當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍。
  2. loadFactor:負載因子,默認爲 0.75。
  3. threshold:擴容的閾值,等於 capacity * loadFactor

Java8實現
數組+鏈表+紅黑樹
Java8 對 HashMap 進行了一些修改,大的不同就是利用了紅黑樹,所以其由 數組+鏈表+紅黑 樹 組成。
根據 Java7 HashMap 的介紹,我們知道,查找的時候,根據 hash 值我們能夠快速定位到數組的 具體下標,但是之後的話,需要順着鏈表一個個比較下去才能找到我們需要的,時間複雜度取決 於鏈表的長度,爲 O(n)。爲了降低這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個以後, 會將鏈表轉換爲紅黑樹,在這些位置進行查找的時候可以降低時間複雜度爲 O(logN)。
在這裏插入圖片描述

Hashtable

Hashtable 是遺留類,很多映射的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類, 並且是線程安全的,任一時間只有一個線程能寫 Hashtable,併發性不如 ConcurrentHashMap, 因爲 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不需要線程安全 的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換

CurrentHashMap

支持併發操作。ConcurrentHashMap 由一個個 Segment 數組+數組+紅黑樹組成。Segment 通過繼承 ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每 個 Segment 是線程安全的,也就實現了全局的線程安全。

concurrencyLevel:並行級別、併發數、Segment 數,怎麼翻譯不重要,理解它。默認是 16, 也就是說 ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,多可以同時支 持 16 個線程併發寫,只要它們的操作分別分佈在不同的 Segment 上。這個值可以在初始化的時 候設置爲其他值,但是一旦初始化以後,它是不可以擴容的。再具體到每個 Segment 內部,其實 每個 Segment 很像之前介紹的 HashMap,不過它要保證線程安全,所以處理起來要麻煩些。

jdk1.7:segment數組+數組+鏈表
在這裏插入圖片描述
jdk1.8:segment數組+數組+紅黑樹
在這裏插入圖片描述

TreeMap

TreeMap 實現 SortedMap 接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序, 也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。
如果使用排序的映射,建議使用TreeMap。
在使用 TreeMap 時,key 必須實現 Comparable 接口或者在構造 TreeMap 傳入自定義的 Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。
參考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html

LinkedHashMap

LinkedHashMap 是 HashMap 的一個子類,保存了記錄的插入順序,在用 Iterator 遍歷 LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。 參考1:http://www.importnew.com/28263.html
參考2:http://www.importnew.com/20386.html#comment-648123

hashtable和hashmap的區別及實現原理,

hashmap會問到數組索引,hash碰撞怎麼解決?

Hashtable,HashMap,ConcurrentHashMap 底層實現原理與線程安全問題

建議熟悉 jdk 源碼,才能從容應答

請你說明HashMap和Hashtable的區別?

HashMap和Hashtable都實現了Map接口,因此很多特性非常相似。但是,他們有以下不同點:
HashMap允許鍵和值是null,而Hashtable不允許鍵或者值是null。
Hashtable是同步的,而HashMap不是。因此,HashMap更適合於單線程環境,而Hashtable適合於多線程環境。
HashMap提供了可供應用迭代的鍵的集合,因此,HashMap是快速失敗的。另一方面,Hashtable提供了對鍵的列舉(Enumeration)。
一般認爲Hashtable是一個遺留的類。

HashMap 和 ConcurrentHashMap區別?
  1. HashMap不支持併發操作,沒有同步方法,ConcurrentHashMap支持併發操作,通過繼承 ReentrantLock(JDK1.7重入鎖)/CAS和synchronized(JDK1.8內置鎖)來進行加鎖(分段鎖),每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實現了全局的線程安全。
  2. JDK1.8之前HashMap的結構爲數組+鏈表,JDK1.8之後HashMap的結構爲數組+鏈表+紅黑樹;JDK1.8之前ConcurrentHashMap的結構爲segment數組+數組+鏈表,JDK1.8之後ConcurrentHashMap的結構爲數組+鏈表+紅黑樹。
如何使HashMap變得安全?
ConcurrentHashMap如何保證線程安全的?

常見

請說明Collection 和 Collections的區別?

Collection是集合類的上級接口,繼承與他的接口主要有Set 和List.
Collections是針對集合類的一個幫助類,他提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作

HashMap底層實現?
hash衝突瞭解哪些?

少見

Collection集合接口和Map接口有什麼關係?

沒有直接關係,但是一些子類會有依賴,Collection是最基本的集合接口,聲明瞭適用於JAVA集合(只包括Set和List)的通用方法。Map接口並不是Collection接口的子接口,但是它仍然被看作是Collection框架的一部分。

Map的各種實現類,它們有什麼區別?
Hash衝突怎麼辦?哪些解決散列衝突的方法?
HashMap衝突很厲害,最差性能,你會怎麼解決?

從O(n)提升到log(n)咯,用二叉排序樹的思路說了一通

rehash
hashCode() 與 equals() 生成算法、方法怎麼重寫


JVM

高頻

JVM 的內存結構?

在這裏插入圖片描述
在這裏插入圖片描述

  1. 程序計數器:程序計數器(Program Counter Register)是一塊較小的內存空間,可以看作是當前線程所執行字節碼的行號指示器,指向下一個將要執行的指令代碼,由執行引擎來讀取下一條指令。更確切的說,一個線程的執行,是通過字節碼解釋器改變當前線程的計數器的值,來獲取下一條需要執行的字節碼指令,從而確保線程的正確執行。每個線程都有一個獨立的程序計數器,是線程私有的內存

  2. 虛擬機棧:描述java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame) 用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成 的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

  3. 本地方法棧:本地方法棧則爲 JVM 使用到的 Native 方法服務。Native 方法不是以 Java 語言實現的,而是以本地語言實現的(比如 C 或 C++)。個人理解Native 方法是與操作系統直接交互的。比如通知垃圾收集器進行垃圾回收的代碼 System.gc(),就是使用 native 修飾的

  4. 堆:堆是Java虛擬機所管理的內存中最大的一塊存儲區域。堆內存被所有線程共享。主要存放使用new關鍵字創建的對象。所有對象實例以及數組都要在堆上分配。垃圾收集器就是根據GC算法,收集堆上對象所佔用的內存空間(收集的是對象佔用的空間而不是對象本身)
    Java堆分爲年輕代(Young Generation)和老年代(Old Generation);年輕代又分爲伊甸園(Eden)和倖存區(Survivor區);倖存區又分爲From Survivor空間和 To Survivor空間。

年輕代存儲“新生對象”,我們新創建的對象存儲在年輕代中。當年輕內存佔滿後,會觸發Minor GC,清理年輕代內存空間。

老年代存儲長期存活的對象和大對象。年輕代中存儲的對象,經過多次GC後仍然存活的對象會移動到老年代中進行存儲。老年代空間佔滿後,會觸發Full GC。

注:Full GC是清理整個堆空間,包括年輕代和老年代。如果Full GC之後,堆中仍然無法存儲對象,就會拋出OutOfMemoryError異常。

  1. 方法區:方法區同 Java 堆一樣是被所有線程共享的區間,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼。更具體的說,靜態變量+常量+類信息(版本、方法、字段等)+運行時常量池存在方法區中。常量池是方法區的一部分
    注:JDK1.8 使用元空間 MetaSpace 替代方法區,元空間並不在 JVM中,而是使用本地內存。元空間兩個參數:
    MetaSpaceSize:初始化元空間大小,控制發生GC閾值
    MaxMetaspaceSize : 限制元空間大小上限,防止異常佔用過多物理內存

常量池中存儲編譯器生成的各種字面量和符號引用。字面量就是Java中常量的意思。比如文本字符串,final修飾的常量等。方法引用則包括類和接口的全限定名,方法名和描述符,字段名和描述符等。

分代?

Java堆從GC的角度還可以細分爲: 新生代( Eden 區 、 From Survivor 區 和To Survivor 區 )和老年代

新生代:是用來存放新生的對象。一般佔據堆的1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發 MinorGC進行垃圾回收。新生代又分爲 Eden區、ServivorFrom、ServivorTo三個區
在這裏插入圖片描述
MinorGC的過程
採用複製算法
在這裏插入圖片描述

老年代
主要存放應用程序中生命週期長的內存對象。
老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行 了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足 夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
MajorGC 採用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒 有標記的對象。MajorGC的耗時比較長,因爲要掃描再回收。MajorGC 會產生內存碎片,爲了減 少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的 時候,就會拋出OOM(Out of Memory)異常。

永久代
指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被 放入永久區域,它和存放實例的區域不同,GC 不會在主程序運行期對永久區域進行清理。所以這 也導致了永久代的區域會隨着加載的Class的增多而脹滿,終拋出OOM異常

垃圾清除算法?

在這裏插入圖片描述
確定垃圾算法

  • 引用計數法:在 Java 中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單 的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關 聯的引用,即他們的引用計數都不爲 0,則說明對象不太可能再被用到,那麼這個對象就是可回收 對象

  • 可達性分析:爲了解決引用計數法的循環引用問題,Java 使用了可達性分析的方法。通過一系列的“GC roots” 對象作爲起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象,不可達對象變爲可回收對象至少要經過兩次標記 過程。兩次標記後仍然是可回收對象,則將面臨回收。

標記清除算法(mark-sweep)
最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清 除階段回收被標記的對象所佔用的空間
缺陷:大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可 利用空間的問題
在這裏插入圖片描述

複製算法(coping)
爲了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小 的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用 的內存清掉。
缺陷:這種算法雖然實現簡單,內存效率高,不易產生碎片,但是大的問題是可用內存被壓縮到了原 本的一半。且存活對象增多的話,Copying算法的效率會大大降低
在這裏插入圖片描述

標記整理算法(mark-compact)
結合了以上兩個算法,爲了避免缺陷而提出。標記階段和Mark-Sweep算法相同,標記後不是清 理對象,而是將存活對象移向內存的一端。然後清除端邊界外的對象

在這裏插入圖片描述

分代收集算法
分代收集法是目前大部分JVM所採用的方法,其核心思想是根據對象存活的不同生命週期將內存 劃分爲不同的域,一般情況下將GC堆劃分爲老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃 圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法

  • 新生代-複製算法:目前大部分JVM的GC 對於新生代都採取Copying算法,因爲新生代中每次垃圾回收都要 回收大部分對象,即要複製的操作比較少,但通常並不是按照1:1來劃分新生代。一般將新生代 劃分爲一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用 Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象複製到另 一塊Survivor空間中
    在這裏插入圖片描述
  • 老年代-標記整理算法:因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標 記—整理”算法來進行回收, 不必進行內存複製, 且直接騰出空閒內存。
  1. JAVA虛擬機提到過的處於方法區的永生代(Permanet Generation),它用來存儲class類, 常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。
  2. 對象的內存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放對象的那一塊),少數情況會直接分配到老生代。
  3. 當新生代的Eden Space和From Space空間不足時就會發生一次GC,進行GC後,Eden Space和From Space區的存活對象會被挪到To Space,然後將Eden Space和From Space進行清理。
  4. 如果To Space無法足夠存儲某個對象,則將這個對象存儲到老生代。
  5. 在進行GC後,使用的便是Eden Space和To Space了,如此反覆循環。
  6. 當對象在Survivor區躲過一次GC 後,其年齡就會+1。默認情況下年齡到達15 的對象會被 移到老生代中

分區收集算法:分區算法則將整個堆空間劃分爲連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的 好處是可以控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是 整個堆), 從而減少一次GC 所產生的停頓。

垃圾回收器?

Java 堆內存被劃分爲新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收算法; 年老代主要使用標記-整理垃圾回收算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不 同的垃圾收集器,JDK1.6中Sun HotSpot虛擬機的垃圾收集器如下
在這裏插入圖片描述

  • 新生代垃圾收集器
  1. Serial(英文連續)是基本垃圾收集器,使用複製算法,曾經是JDK1.3.1之前新生代唯一的垃圾 收集器。Serial 是一個單線程的收集器,它不但只會使用一個 CPU 或一條線程去完成垃圾收集工 作,並且在進行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結束。 Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對於限 定單個 CPU 環境來說,沒有線程交互的開銷,可以獲得高的單線程垃圾收集效率,因此 Serial 垃圾收集器依然是java虛擬機運行在Client模式下默認的新生代垃圾收集器
  2. ParNew垃圾收集器其實是Serial收集器的多線程版本,也使用複製算法,除了使用多線程進行垃 圾收集之外,其餘的行爲和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也 要暫停所有其他的工作線程
    ParNew 收集器默認開啓和 CPU 數目相同的線程數,可以通過-XX:ParallelGCThreads 參數來限 制垃圾收集器的線程數。【Parallel:平行的】 ParNew雖然是除了多線程外和Serial收集器幾乎完全一樣,但是ParNew垃圾收集器是很多java 虛擬機運行在Server模式下新生代的默認垃圾收集器
  3. Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製算法,也是一個多線程的垃 圾收集器,它重點關注的是程序達到一個可控制的吞吐量(Thoughput,CPU 用於運行用戶代碼 的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)), 高吞吐量可以高效率地利用 CPU 時間,儘快地完成程序的運算任務,主要適用於在後臺運算而 不需要太多交互的任務。自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個 重要區別
  • 老年代
  1. Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單線程的收集器,使用標記-整理算法, 這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器
    在Server模式下,主要有兩個用途:
    (1)在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。
    (2)作爲年老代中使用CMS收集器的後備垃圾收集方案。 新生代Serial與年老代Serial Old搭配垃圾收集過程圖:
    在這裏插入圖片描述
    (3)新生代Parallel Scavenge收集器與ParNew收集器工作原理類似,都是多線程的收集器,都使 用的是複製算法,在垃圾收集過程中都需要暫停所有的工作線程。新生代Parallel Scavenge/ParNew與年老代Serial Old搭配垃圾收集過程圖:
    在這裏插入圖片描述

  2. Parallel Old收集器是Parallel Scavenge的年老代版本,使用多線程標記-整理算法,在JDK1.6 纔開始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只 能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是爲了在年老代同樣提供吞 吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge 和年老代Parallel Old收集器的搭配策略。 新生代Parallel Scavenge和年老代Parallel Old收集器搭配運行過程圖:
    在這裏插入圖片描述

  3. Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其主要目標是獲取短垃圾 回收停頓時間,和其他年老代使用標記-整理算法不同,它使用多線程的標記-清除算法。 短的垃圾收集停頓時間可以爲交互比較高的程序提高用戶體驗。 CMS工作機制相比其他的垃圾收集器來說更復雜,整個過程分爲以下4個階段:
    初始標記:只是標記一下GC Roots能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。
    併發標記:進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。
    重新標記:爲了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記 記錄,仍然需要暫停所有的工作線程。
    併發清除:清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。由於耗時長的並 發標記和併發清除過程中,垃圾收集線程可以和用戶現在一起併發工作,所以總體上來看 CMS收集器的內存回收和用戶線程是一起併發地執行。
    在這裏插入圖片描述
    7.G1收集器:Garbage first 垃圾收集器是目前垃圾收集器理論發展的前沿成果,相比與CMS 收集器,G1 收 集器兩個突出的改進是:
    1.基於標記-整理算法,不產生內存碎片
    2.可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。 G1 收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,並且跟蹤這些區域 的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所允許的收集時間優先回收垃圾 多的區域。區域劃分和優先級區域回收機制,確保 G1 收集器可以在有限時間獲得高的垃圾收 集效率。

類的實例化順序?

比如父類靜態數據,構造函數,字段,子類靜態數據,構造函數,字段,他們的執行順序?
父類靜態成員和靜態代碼塊->
子類靜態成員和靜態代碼塊->
父類非靜態成員和非靜態代碼塊->
父類構造方法->
子類非靜態成員和非靜態代碼塊->
子類構造方法

java的.class加載機制,java到class到二進制字節碼文件的轉化過程?
描述一下JVM加載class文件的原理機制?

這塊引用鏈接:https://www.jianshu.com/p/7423f01bf77f

JVM中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java中的類加載器是一個重要的Java運行時系統組件,它負責在運行時查找和裝入類文件中的類。
由於Java的跨平臺性,經過編譯的Java源程序並不是一個可執行程序,而是一個或多個類文件。
當Java程序需要使用某個類時,JVM會確保這個類已經被加載、連接(驗證、準備和解析)和初始化。
類的加載是指把類的.class文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class文件,然後產生與所加載類對應的Class對象。
加載完成後,Class對象還不完整,所以此時的類還不可用。當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。
最後JVM對類進行初始化,包括:
1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;
2)如果類中存在初始化語句,就依次執行這些初始化語句。

類的加載是由類加載器完成的,類加載器包括:
根加載器(BootStrap)、擴展加載器(Extension)、系統加載器(System)和用戶自定義類加載器(java.lang.ClassLoader的子類)。
從Java 2(JDK 1.2)開始,類加載過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根加載器,其他的加載器都有且僅有一個父類加載器。類的加載首先請求父類加載器加載,父類加載器無能爲力時才由其子類加載器自行加載。JVM不會向Java程序提供對Bootstrap的引用。下面是關於幾個類加載器的說明:

Bootstrap:一般用本地代碼實現,負責加載JVM基礎核心類庫(rt.jar);負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被 虛擬機認可(按文件名識別,如rt.jar)

Extension:從java.ext.dirs系統屬性所指定的目錄中加載類庫,它的父加載器是Bootstrap; 負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類 庫。

System:又叫應用類加載器,其父類是Extension。它是應用最廣泛的類加載器。它從環境變量classpath或者系統屬性java.class.path所指定的目錄中記載類,是用戶自定義加載器的默認父加載器。負責加載用戶路徑(classpath)上的類庫。
在這裏插入圖片描述

雙親委派?

當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父 類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載其中, 只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的 Class),子類加載器纔會嘗試自己去加載。
採用雙親委派的一個好處是比如加載位於 rt.jar 包中的類 java.lang.Object,不管是哪個加載 器加載這個類,終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載 器終得到的都是同樣一個Object對象。
在這裏插入圖片描述

Minor GC和Full GC的觸發時機

年輕代存儲“新生對象”,我們新創建的對象存儲在年輕代中。當年輕內存佔滿後,會觸發Minor GC,清理年輕代內存空間。

老年代存儲長期存活的對象和大對象。年輕代中存儲的對象,經過多次GC後仍然存活的對象會移動到老年代中進行存儲。老年代空間佔滿後,會觸發Full GC。

注:Full GC是清理整個堆空間,包括年輕代和老年代。如果Full GC之後,堆中仍然無法存儲對象,就會拋出OutOfMemoryError異常。

  • Minor GC觸發條件:當Eden區滿時,觸發Minor GC。
  • Full GC觸發條件:
    (1)調用System.gc時,系統建議執行Full GC,但是不必然執行
    (2)老年代空間不足
    (3)方法去空間不足
    (4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存
    (5)由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

常見

JVM 調優?

調整年輕代、年老代的內存空間大小及使用GC發生器的類型
工具:
Jconsole,jProfile,VisualVM
Jconsole : jdk自帶,功能簡單,但是可以在系統有一定負荷的情況下使用。對垃圾回收算法有很詳細的跟蹤。
JProfiler:商業軟件,需要付費。功能強大。
VisualVM:JDK自帶,功能強大,與JProfiler類似。推薦

垃圾回收器的基本原理是什麼?垃圾回收器可以馬上回收內存嗎?有什麼辦法主動通知虛擬機進行垃圾回收?
  1. 基本原理(對象引用遍歷方式):
    對於GC(垃圾收集)來說,當程序員創建對象時,GC就開始監控這個對象的地址、大小以及使用情況。
    通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有對象。
    通過這種方式確定哪些對象是"可達的",哪些對象是"不可達的"。
    當GC確定一些對象爲"不可達"時,GC就有責任回收這些內存空間。

  2. 垃圾回收器不可以馬上回收內存。
    垃圾收集器不可以被強制執行,但程序員可以通過調研System.gc方法來建議執行垃圾收集。
    記住,只是建議。一般不建議自己寫System.gc,因爲會加大垃圾收集工作量

  3. 程序員可以手動執行System.gc(),通知GC運行,但是Java語言規範並不保證GC一定會執行

java中會存在內存泄漏嗎,請簡單描述。

這部分引用https://blog.csdn.net/qq_41033290/article/details/85265449

論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的對象,會被GC自動回收掉,自動從內存中清除。

但是,即使這樣,Java也還是存在着內存泄漏的情況,
1、長生命週期的對象持有短生命週期對象的引用就很可能發生內存泄露。

儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收,這就是Java中內存泄露的發生場景,通俗地說,就是程序員可能創建了一個對象,以後一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是Java中可能出現內存泄露的情況,例如,緩存系統,我們加載了一個對象放在緩存中(例如放在一個全局map對象中),然後一直不再使用它,這個對象一直被緩存引用,但卻不再被使用。

如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由於內部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄露。

2、當一個對象被存儲進HashSet集合中以後,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改後的哈希值與最初存儲進HashSet集合中時的哈希值就不同了,在這種情況下,即使在contains方法使用該對象的當前引用作爲的參數去HashSet集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從HashSet集合中單獨刪除當前對象,造成內存泄露

內存泄漏會導致什麼?

沒有及時的把不用的內存釋放或根本沒有釋放該內存,造成資源的浪費,表現爲運行速度變慢,甚至系統的崩潰

內存泄漏檢查

參考https://www.cnblogs.com/andy-zhou/p/5327288.html#_caption_24

年老代堆空間被佔滿
異常: java.lang.OutOfMemoryError: Java heap space

持久代被佔滿
異常:java.lang.OutOfMemoryError: PermGen space
Perm空間被佔滿。無法爲新的class分配存儲空間而引發的異常。這個異常以前是沒有的,但是在Java反射大量使用的今天這個異常比較常見了。主要原因就是大量動態反射生成的類不斷被加載,最終導致Perm區被佔滿。

更可怕的是,不同的classLoader即便使用了相同的類,但是都會對其進行加載,相當於同一個東西,如果有N個classLoader那麼他將會被加載N次。因此,某些情況下,這個問題基本視爲無解。當然,存在大量classLoader和大量反射類的情況其實也不多。
解決:

-XX:MaxPermSize=16m
換用JDK。比如JRocket

堆棧溢出
異常:java.lang.StackOverflowError
說明:這個就不多說了,一般就是遞歸沒返回,或者循環調用造成

線程堆棧滿
異常:Fatal: Stack size too small
說明:java中一個線程的空間大小是有限制的。JDK5.0以後這個值是1M。與這個線程相關的數據將會保存在其中。但是當線程空間滿了以後,將會出現上面異常。
解決:增加線程棧大小。-Xss2m。但這個配置無法解決根本問題,還要看代碼部分是否有造成泄漏的部分。

系統內存被佔滿
異常:java.lang.OutOfMemoryError: unable to create new native thread
說明:
這個異常是由於操作系統沒有足夠的資源來產生這個線程造成的。系統創建線程時,除了要在Java堆中分配內存外,操作系統本身也需要分配資源來創建線程。因此,當線程數量大到一定程度以後,堆中或許還有空間,但是操作系統分配不出資源來了,就出現這個異常了。

分配給Java虛擬機的內存愈多,系統剩餘的資源就越少,因此,當系統內存固定時,分配給Java虛擬機的內存越多,那麼,系統總共能夠產生的線程也就越少,兩者成反比的關係。同時,可以通過修改-Xss來減少分配給單個線程的空間,也可以增加系統總共內生產的線程數。

解決:
重新設計系統減少線程數量。
線程數量不能減少的情況下,通過-Xss減小單個線程大小。以便能生產更多的線程

少見

CGLib
JVM的作用?

JVM是Java字節碼執行的引擎,爲Java程序的執行提供必要的支持,它還能優化Java字節碼,使之轉換成效率更高的機器指令。程序員編寫的程序最終都要在 JVM 上執行,JVM 中類的裝載是由類加載器(ClassLoader)和它的子類來實現的。ClassLoader是Java運行時一個重要的系統組件,負責在運行時查找和裝入類文件的類。
JVM屏蔽了與具體操作系統平臺相關的信息,從而實現了Java程序只需生成在JVM上運行的字節碼文件(class 文件),就可以在多種平臺上不加修改地運行。不同平臺對應着不同的JVM,在執行字節碼時,JVM負責將每一條要執行的字節碼送給解釋器,解釋器再將其翻譯成特定平臺環境的機器指令並執行。Java語言最重要的特點就是跨平臺運行,使用JVM就是爲了支持與操作系統無關,實現跨平臺運行



Java 併發編程

高頻

什麼是線程?

線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麼用十個線程完成一個任務只需10毫秒。Java在語言層面對多線程提供了卓越的支持,它也是一個很好的賣點。

線程和進程有什麼區別?

線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。

Java中sleep方法和wait方法的區別?

兩個方法區別在於:
sleep()是Thread類的,而wait()是Object類的;
sleep是睡眠,指定時間後線程會繼續執行,不放棄對cpu資源的佔用(即不放棄對象鎖),相當於暫停指定t;
wait()是等待,需要喚醒,它會釋放對cpu資源的佔用(即會放棄對象鎖),進入等待此對象的等待鎖定池,調用notify()和notifyAll()喚醒(本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態)

Thread 類中的start() 和 run() 方法有什麼區別?

這個問題經常被問到,但還是能從此區分出面試者對Java線程模型的理解程度。start()方法被用來啓動新創建的線程,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。當你調用run()方法的時候,只會是在原來的線程中調用執行,沒有新的線程啓動,start()方法纔會啓動新線程。

  1. start()方法來啓動線程,真正實現了多線程運行。這時無需等待 run 方法體代碼執行完畢, 可以直接繼續執行下面的代碼。
  2. 通過調用 Thread 類的 start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運 行。
  3. 方法 run()稱爲線程體,它包含了要執行的這個線程的內容,線程就進入了運行狀態,開始運 行run函數當中的代碼。 Run方法運行結束, 此線程終止。然後CPU再調度其它線程
終止線程4種方式?
  1. 使用標誌退出
    一般 run()方法執行完,線程就會正常結束,然而,常常有些線程是伺服線程。它們需要長時間的 運行,只有在外部某些條件滿足的情況下,才能關閉這些線程。使用一個變量來控制循環,例如: 直接的方法就是設一個boolean類型的標誌,並通過設置這個標誌爲true或false來控制while 循環是否退出,代碼示例:
    在這裏插入圖片描述
    定義了一個退出標誌exit,當exit爲true時,while循環退出,exit的默認值爲false.在定義exit 時,使用了一個 Java 關鍵字 volatile,這個關鍵字的目的是使 exit 同步,也就是說在同一時刻只 能由一個線程來修改exit的值。

2.使用interrupt()方法來中斷線程有兩種情況:

  • 線程處於阻塞狀態:如使用了 sleep,同步鎖的 wait,socket 中的 receiver,accept 等方法時, 會使線程處於阻塞狀態。當調用線程的 interrupt()方法時,會拋出 InterruptException 異常。 阻塞中的那個方法拋出這個異常,通過代碼捕獲該異常,然後 break 跳出循環狀態,從而讓 我們有機會結束這個線程的執行。通常很多人認爲只要調用 interrupt 方法線程就會結束,實 際上是錯的, 一定要先捕獲InterruptedException異常之後通過break來跳出循環,才能正 常結束run方法。
  • 線程未處於阻塞狀態:使用isInterrupted()判斷線程的中斷標誌來退出循環。當使用 interrupt()方法時,中斷標誌就會置true,和使用自定義的標誌來控制循環是一樣的道理。
    在這裏插入圖片描述
  1. stop方法
    程序中可以直接使用thread.stop()來強行終止線程,但是stop方法是很危險的,就象突然關 閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果,不安全主要是: thread.stop()調用之後,創建子線程的線程就會拋出 ThreadDeatherror 的錯誤,並且會釋放子 線程所持有的所有鎖。一般任何進行加鎖的代碼塊,都是爲了保護數據的一致性,如果在調用 thread.stop()後導致了該線程所持有的所有鎖的突然釋放(不可控制),那麼被保護數據就有可能呈 現不一致性,其他線程在使用這些被破壞的數據時,有可能導致一些很奇怪的應用程序錯誤。因 此,並不推薦使用stop方法來終止線程。
什麼是線程池? 爲什麼要使用它?

創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麼響應時間會變長,而且一個進程能創建的線程數有限。爲了避免這些問題,在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程。

種類:緩存線程池(可變尺寸的線程池)、固定大小的線程池、調度線程池、單例線程池、自定義線程池

  1. newCachedThreadPool
  2. newFixedThreadPool
  3. newScheduledThreadPool
  4. newSingleThreadExecutor
線程池原理?

線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後 啓動這些任務,如果線程數量超過了大數量超出數量的線程排隊等候,等其它線程執行完畢, 再從隊列中取出任務來執行。
主要特點爲:線程複用;控制大併發數;管理線程。

  • 線程複用:每一個 Thread 的類都有一個 start 方法。 當調用start啓動線程時Java虛擬機會調用該類的 run 方法。 那麼該類的 run() 方法中就是調用了 Runnable 對象的 run() 方法。 我們可以繼承重寫 Thread 類,在其 start 方法中添加不斷循環調用傳遞過來的 Runnable 對象。 這就是線程池的實 現原理。循環方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以 是阻塞的。
Java線程池工作過程 ?
  1. 線程池剛創建時,裏面沒有一個線程。任務隊列是作爲參數傳進來的。不過,就算隊列裏面 有任務,線程池也不會馬上執行它們。

  2. 當調用 execute() 方法添加一個任務時,線程池會做如下判斷:
    a) 如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務;
    b) 如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列;
    c) 如果這時候隊列滿了,而且正在運行的線程數量小於 maximumPoolSize,那麼還是要 創建非核心線程立刻運行這個任務;
    d) 如果隊列滿了,而且正在運行的線程數量大於或等於 maximumPoolSize,那麼線程池 會拋出異常RejectExecutionException。

  3. 當一個線程完成任務時,它會從隊列中取下一個任務來執行。

  4. 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運 行的線程數大於 corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後,它 終會收縮到 corePoolSize 的大小

什麼是ThreadLocal?

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,ThreadLocal 的作用 是提供線程內的局部變量,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或 者組件之間一些公共變量的傳遞的複雜度。
線程範圍內的共享變量,每個線程只能訪問他自己的,不能訪問別的線程。

Java中Runnable和Callable有什麼不同?

Runnable和Callable代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區別是Callable的 call() 方法可以返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計算結果的Future對象

提交任務時,線程池隊列已滿。會發生什麼?(工作過程、拒絕策略)

這個問題問得很狡猾,許多程序員會認爲該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那麼ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常

樂觀鎖與悲觀鎖?
  • 樂觀鎖:認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數 據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新), 如果失敗則要重複讀-比較-寫的操作。
    java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入 值是否一樣,一樣則更新,否則失敗。

  • 悲觀鎖:認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。 java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到, 纔會轉換爲悲觀鎖,如RetreenLock

同步鎖與死鎖?
  • 同步鎖:當多個線程同時訪問同一個數據時,很容易出現問題。爲了避免這種情況出現,我們要保證線程 同步互斥,就是指併發執行的多個線程,在同一時間內只允許一個線程訪問共享數據。 Java 中可 以使用synchronized關鍵字來取得一個對象的同步鎖。

  • 死鎖:就是多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放

共享鎖與獨佔鎖?
  • 共享鎖:共享鎖則允許多個線程同時獲取鎖,併發訪問 共享資源,如:ReadWriteLock。共享鎖則是一種 樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源。
  • 獨佔鎖:獨佔鎖模式下,每次只能有一個線程能持有鎖,ReentrantLock 就是以獨佔方式實現的互斥鎖。 獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,如果某個只讀線程獲取鎖,則其他讀線 程都只能等待,這種情況下就限制了不必要的併發性,因爲讀操作並不會影響數據的一致性

常見

Java中如何停止一個線程?
線程基本方法?

線程相關的基本方法有wait,notify,notifyAll,sleep,join,yield等。

wait():調用該方法的線程進入WAITING 狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意的 是調用wait()方法後,會釋放對象的鎖。因此,wait方法一般用在同步方法或同步代碼塊中。

sleep(): 導致當前線程休眠,與 wait 方法不同的是 sleep 不會釋放當前佔有的鎖,sleep(long)會導致 線程進入TIMED-WATING狀態,而wait()方法會導致當前線程進入WATING狀態

yield(): 會使當前線程讓出 CPU 執行時間片,與其他線程一起重新競爭 CPU 時間片。一般情況下, 優先級高的線程有更大的可能性成功競爭得到 CPU 時間片,但這又不是絕對的,有的操作系統對 線程優先級並不敏感

interrupt():中斷一個線程,其本意是給這個線程一個通知信號,會影響這個線程內部的一箇中斷標識位。這 個線程本身並不會因此而改變狀態(如阻塞,終止等)。

  1. 調用 interrupt()方法並不會中斷一個正在運行的線程。也就是說處於 Running 狀態的線 程並不會因爲被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。
  2. 若調用 sleep()而使線程處於 TIMED-WATING 狀態,這時調用 interrupt()方法,會拋出 InterruptedException,從而使線程提前結束TIMED-WATING狀態。
  3. 許多聲明拋出InterruptedException的方法(如Thread.sleep(long mills方法)),拋出異 常前,都會清除中斷標識位,所以拋出異常後,調用 isInterrupted()方法將會返回 false。
  4. 中斷狀態是線程固有的一個標識位,可以通過此標識位安全的終止線程。比如,你想終止 一個線程thread的時候,可以調用thread.interrupt()方法,在線程的run方法內部可以 根據thread.isInterrupted()的值來優雅的終止線程。

notify() :Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象 上等待,則會選擇喚醒其中一個線程,選擇是任意的,並在對實現做出決定時發生,線程通過調 用其中一個 wait() 方法,在對象的監視器上等待,直到當前的線程放棄此對象上的鎖定,才能繼 續執行被喚醒的線程,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競 爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程

synchronized?

底層實現:
進入時,執行 monitorenter,將計數器 +1,釋放鎖monitorexit 時,計數器-1;
當一個線程判斷到計數器爲 0 時,則當前鎖空閒,可以佔用;反之,當前線程進入等待狀態。

含義:(monitor 機制)
Synchronized 是在加鎖,加對象鎖。對象鎖是一種重量鎖(monitor),synchronized 的鎖機制會根據線程競爭情況在運行時會有偏向鎖(單一線程)、輕量鎖(多個線程訪問 synchronized 區域)、對象鎖(重量鎖,多個線程存在競爭的情況)、自旋鎖等。
該關鍵字是一個幾種鎖的封裝。

volatile?

概念:Java語言提供了一種稍弱的同步機制(比 sychronized更輕量級的同步鎖),即volatile變量,用來確保將變量的更新操作通知到其他 線程。volatile 變量具備兩種特性,volatile變量不會被緩存在寄存器或者對其他處理器不可見的 地方,因此在讀取volatile類型的變量時總會返回新寫入的值。

功能:

  1. 變量可見性:保證該變量對所有線程可見,這裏的可見性指的是當一個線程修改了變量的值,那麼新的 值對於其他線程是可以立即獲取的。
  2. 禁止 JVM 進行的指令重排序。
  3. 訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一 種比sychronized關鍵字更輕量級的同步機制。volatile適合這種場景:一個變量被多個線程共 享,線程直接給這個變量賦值。

原理:

  • 當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有 多個CPU,每個線程可能在不同的CPU上被處理,這意味着每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。

適用場景:
對volatile變量的單次讀/寫操作可以保證原子性的,如long和double類型變量, 但是並不能保證i++這種操作的原子性,因爲本質上i++是讀、寫兩次操作。在某些場景下可以代替Synchronized。但是, volatile的不能完全取代Synchronized的位置,只有在一些特殊的場景下,才能適用volatile。
總的來說,必須同時滿足下面兩個條件才能保證在併發環境的線程安 全:

  1. 對變量的寫操作不依賴於當前值(比如 i++),或者說是單純的變量賦值(boolean flag = true)。
  2. 該變量沒有包含在具有其他變量的不變式中,也就是說,不同的volatile變量之間,不 能互相依賴。只有在狀態真正獨立於程序內其他內容時才能使用 volatile。
如何在Java中實現線程?
  1. 繼承Thread類:Thread類本質上是實現了Runnable接口的一個實例,代表一個線程的實例。啓動線程的唯一方 法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啓動一個新線 程,並執行run()方法。
    在這裏插入圖片描述
  2. 實現Runnable:如果類已經extends另一個類,就無法直接extends Thread,此時,可以實現一個 Runnable接口。
    在這裏插入圖片描述

以下兩種可能不算,因爲本質也是實現接口

  1. ExecutorService、Callable、Future有返回值線程:有返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。執行 Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務 返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了
  2. 基於線程池的方式
    在這裏插入圖片描述
線程池的組成?
  1. 線程池管理器:用於創建並管理線程池
  2. 工作線程:線程池中的線程
  3. 任務接口:每個任務必須實現的接口,用於工作線程調度其運行
  4. 任務隊列:用於存放待處理的任務,提供一種緩衝機制
線程池,threadpool有哪些重要的參數?

Java 中的線程池是通過 Executor 框架實現的,該框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor ,Callable和Future、FutureTask這幾個類。
ThreadPoolExecutor的構造方法如下:
在這裏插入圖片描述

  1. corePoolSize:指定了線程池中的線程數量。
  2. maximumPoolSize:指定了線程池中的大線程數量。
  3. keepAliveTime:當前線程池數量超過corePoolSize時,多餘的空閒線程的存活時間,即多 次時間內會被銷燬。
  4. unit:keepAliveTime的單位。
  5. workQueue:任務隊列,被提交但尚未被執行的任務。
  6. threadFactory:線程工廠,用於創建線程,一般用默認的即可。
  7. handler:拒絕策略,當任務太多來不及處理,如何拒絕任務。
拒絕策略 ?

線程池中的線程已經用完了,無法繼續爲新任務服務,同時,等待隊列也已經排滿了,再也 塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。 JDK內置的拒絕策略如下:

  1. AbortPolicy : 直接拋出異常,阻止系統正常運行。
  2. CallerRunsPolicy : 只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的 任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。
  3. DiscardOldestPolicy : 丟棄老的一個請求,也就是即將被執行的一個任務,並嘗試再 次提交當前任務。
  4. DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟 失,這是好的一種方案。

以上內置拒絕策略均實現了RejectedExecutionHandler接口,若以上策略仍無法滿足實際 需要,完全可以自己擴展RejectedExecutionHandler接口。

JAVA阻塞隊列原理?

阻塞隊列,關鍵字是阻塞,先理解阻塞的含義,在阻塞隊列中,線程阻塞有這樣的兩種情況:
在這裏插入圖片描述
在這裏插入圖片描述

Java中的同步集合與併發集合有什麼區別?

同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程併發的時候會導致爭用,阻礙了系統的擴展性。Java5介紹了併發集合像HashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性

少見

Java中CyclicBarrier 和 CountDownLatch有什麼不同?

CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。與 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。

Java中如何停止一個線程?

Java提供了很豐富的API但沒有爲停止線程提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由於潛在的死鎖威脅因此在後續的JDK版本中他們被棄用了,之後Java API的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程

一個線程運行時發生異常會怎樣?

這是我在一次面試中遇到的一個很刁鑽的Java面試題, 簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler並將線程和異常作爲參數傳遞給handler的uncaughtException()方法進行處理

如何在兩個線程間共享數據?
Java中notify 和 notifyAll有什麼區別?

這又是一個刁鑽的問題,因爲多線程可以等待單監控鎖,Java API 的設計人員提供了一些方法當等待條件改變的時候通知它們,但是這些方法沒有完全實現。notify()方法不能喚醒某個具體的線程,所以只有一個線程在等待的時候它纔有用武之地。而notifyAll()喚醒所有線程並允許他們爭奪鎖確保了至少有一個線程能繼續運行。

Java中interrupted 和 isInterruptedd方法的區別?

interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識爲true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。

爲什麼wait和notify方法要在同步塊中調用?

主要是因爲Java API強制要求這樣做,如果你不這麼做,你的代碼會拋出IllegalMonitorStateException異常。還有一個原因是爲了避免wait和notify之間產生競態條件。
Java線程池中submit() 和 execute()方法有什麼區別?

兩個方法都可以向線程池提交任務,execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法可以返回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法

有哪些不同的線程生命週期?
你對線程優先級的理解是什麼?
Sleep()、suspend()和wait()之間有什麼區別?
什麼是線程餓死,什麼是活鎖?
什麼是Java Timer類?如何創建一個有特定時間間隔的任務?
同步方法和同步塊,哪個是更好的選擇?
Java中invokeAndWait 和 invokeLater有什麼區別?
多線程中的忙循環是什麼?


Java IO

在這裏插入圖片描述
在這裏插入圖片描述

講講IO裏面的常見類,字節流、字符流、接口、實現類、方法阻塞。
講講NIO

NIO主要有三大核心部分:Channel(通道),Buffer(緩衝區), Selector。傳統IO基於字節流和字 符流進行操作,而NIO基於 Channel和Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區 中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連接打開, 數據到達)。因此,單個線程可以監聽多個數據通道。
在這裏插入圖片描述
Channel和IO中的Stream(流)是差不多一個 等級的。只不過Stream是單向的,譬如:InputStream, OutputStream,而Channel是雙向 的,既可以用來進行讀操作,又可以用來進行寫操作。 NIO中的Channel的主要實現有:

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel 這裏看名字就可以猜出個所以然來:分別可以對應文件IO、UDP和TCP(Server和Client

Buffer,故名思意,緩衝區,實際上是一個容器,是一個連續數組。Channel提供從文件、 網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由Buffer

在這裏插入圖片描述
在NIO中,Buffer是一個頂層父類,它是一個抽象類,常用的Buffer的子類有: ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、 ShortBuffer

Selector類是NIO的核心類,Selector能夠檢測多個註冊的通道上是否有事件發生,如果有事 件發生,便獲取事件然後針對每個事件進行相應的響應處理。這樣一來,只是用一個單線程就可 以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,纔會調用 函數來進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護 多個線程,並且避免了多線程之間的上下文切換導致的開銷。

String 編碼UTF-8 和GBK的區別?

Unicode是國際通用的一種編碼標準,它包含着世界上各個國家的所有符號且每一個符號都對應着一個全球唯一的數字。每一個符號在計算機物理層存儲的都是二進制,但是有些字符像英文字母只需要用一個字節就能表示,有的符號可能要用很多字節表示,計算機無法知道哪個符號用幾個字節表示,要是全部統一用固定字節就會造成大量的浪費,如何解決這個問題?
西方人提出了一種編碼方式就是utf-8(utf16/utf32),是變長編碼,英文只用一個字節即可,漢字要用三個字節;
針對漢字編碼的問題,專門提出了gbk,每個符號用兩個字節表示,比utf8的方式佔用資源少。
因此,處理的文本主要爲中文時最好用gbk編碼,英文較多時用utf8編碼。
gbk和utf8兩種編碼之間轉換要通過unicode來間接實現。
一個符號可通過unicode碼錶查到二進制數表示,再將其轉化成utf8(gbk)編碼的形式存貯;解碼時將utf8(gbk)轉化成unicode,就可還原符號。

什麼時候使用字節流、什麼時候使用字符流?
遞歸讀取文件夾下的文件,代碼怎麼實現

通過file.listFiles()方法獲取目錄下的所有文件(包含子目錄下的所有文件),得到files[]數組,然後遍歷得到的所有文件,通過isFile(文件)和isDirectory(文件夾)方法來判斷讀取的是文件還是文件夾,如果得到的是文件夾,就遞歸調用test()方法,如果得到的是文件,就將其加入fileList中,最後測試的時候遍歷fileList下的所有文件,來驗證讀取數據的準確性



引用

強引用:在 Java 中常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引 用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即 使該對象以後永遠都不會被用到JVM也不會回收。因此強引用是造成Java內存泄漏的主要原因之 一

軟引用需要用 SoftReference 類來實現,對於只有軟引用的對象來說,當系統內存足夠時它 不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。

弱引用需要用WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的對象 來說,只要垃圾回收機制一運行,不管JVM的內存空間是否足夠,總會回收該對象佔用的內存

虛引用需要PhantomReference類來實現,它不能單獨使用,必須和引用隊列聯合使用。虛 引用的主要作用是跟蹤對象被垃圾回收的狀態。



異常

這部分常考的就這兩題,歡迎補充;這部分已全部更新完畢。
概念:如果某個方法不能按照正常的途徑完成任務,就可以通過另一種路徑退出方法。在這種情況下 會拋出一個封裝了錯誤信息的對象。此時,這個方法會立刻退出同時不返回任何值。另外,調用 這個方法的其他代碼也無法繼續執行,異常處理機制會將代碼執行交給異常處理器。
在這裏插入圖片描述

高頻

Exception 與 Error?

Error:指java運行時系統的內部錯誤和資源耗盡錯誤。應用程序不會拋出該類對象。如果 出現了這樣的錯誤,除了告知用戶,剩下的就是盡力使程序安全的終止
Exception: Exception 又有兩個分支,一個是 (受檢異常)CheckedException,一個是(非受檢)運行時異常 RuntimeException 。

  • 受檢異常:一般是外部錯誤,這種異常都發生在編譯階段,Java 編譯器會強 製程序去捕獲此類異常,即會出現要求你把這段可能出現異常的程序進行 try catch。如: IOException、SQLException
  • 非受檢異常:所謂的運行時異常,程序中可以選擇捕獲處理,也可以不處理。如:NullPointerException、ClassCastException,程序邏輯錯誤引起

常見

Throw和throws的區別:

在這裏插入圖片描述

  1. throws 用在函數上,後面跟的是異常類,可以跟多個;而 throw 用在函數內,後面跟的 是異常對象。
  2. throws 用來聲明異常,讓調用者只知道該功能可能出現的問題,可以給出預先的處理方 式;
    throw拋出具體的問題對象,執行到throw,功能就已經結束了,跳轉到調用者,並將具體的問題對象拋給調用者。也就是說 throw 語句獨立存在時,下面不要定義其他語 句,因爲執行不到。
  3. throws 表示出現異常的一種可能性,並不一定會發生這些異常;
    throw 則是拋出了異常, 執行throw則一定拋出了某種異常對象。
  4. 兩者都是消極處理異常的方式,只是拋出或者可能拋出異常,但是不會由函數去處理異 常,真正的處理異常由函數的上層調用處理。


反射

這部分其實都不太常考,但還是需要了解一下,這部分已全部更新完畢。

什麼是反射?

概念:Java 中的反射機制是指在運行狀態中,對於任意一個類都能夠知道這個類所有的屬性和方法; 並且對於任意一個對象,都能夠調用它的任意一個方法;這種動態獲取信息以及動態調用對象方 法的功能稱爲Java 語言的反射機制

Java反射機制的作用?

應用場合:在Java程序中許多對象在運行是都會出現兩種類型:編譯時類型和運行時類型。 編譯時的類型由 聲明對象時實用的類型來決定,運行時的類型由實際賦值給對象的類型決定 。如Person p=new Student();
其中編譯時類型爲Person,運行時類型爲Student

程序在運行時還可能接收到外部傳入的對象,該對象的編譯時類型爲 Object,但是程序有需要調用 該對象的運行時類型的方法。爲了解決這些問題,程序需要在運行時發現對象和類的真實信息。 然而,如果編譯時根本無法預知該對象和類屬於哪些類,程序只能依靠運行時信息來發現該對象 和類的真實信息,此時就必須使用到反射了 。
作用

  1. 在運行時能夠判斷任意一個對象所屬的類
  2. 在運行時構造任意一個類的對象
  3. 在運行時判斷任意一個類所具有的成員變量和方法
  4. 在運行時調用任一對象的方法
  5. 在運行時創建新類對象
哪裏用到反射機制?
  1. JDBC中,利用反射動態加載了數據庫驅動程序。
  2. Web服務器中利用反射調用了Sevlet的服務方法。
  3. Eclispe等開發工具利用反射動態刨析對象的類型與結構,動態提示對象的屬性和方法。
  4. 很多框架都用到反射機制,注入屬性,調用方法,如Spring。
  5. 讀取配置文件
如何使用Java的反射?

獲得Class對象的三種方法
在這裏插入圖片描述
創建對象的兩種方法
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

反射用到了哪些接口,哪些類?
反射機制中可以獲取private成員的值嗎?
反射機制的優缺點?
  • 優點:可以動態執行,在運行期間根據業務功能動態執行方法、訪問屬性,最大限度發揮了java的靈活性。
  • 缺點:對性能有影響,這類操作總是慢於直接執行java代碼。

註解

Java的註解沒什麼考點,考的都是Spring基於java的,這點詳細放在Spring中解釋

Java中有哪些註解?在SpringMVC中,requestmapping是自定義註解,問:如何實現自定義註解?
JSR-250註解

1 @Resource:自動裝配,默認根據類型裝配,如果指定name屬性將根據名字裝配,可以使用如下方式來指定
@Resource(name = “標識符”)
字段或setter方法

2 @PostConstruct和PreDestroy:通過註解指定初始化和銷燬方法定義

JSR-330註解

1 @Inject:等價於默認的@Autowired,只是沒有required屬性
2 @Named:指定Bean名字,對應於Spring自帶@Qualifier中的缺省的根據Bean名字注入情況
3 @Qualifier:只對應於Spring自帶@Qualifier中的擴展@Qualifier限定描述符註解,即只能擴展使用,沒有value屬性

JPA註解

@PersistenceContext
@PersistenceUnit用於注入EntityManagerFactory和EntityManager

Spring自帶依賴注入註解

1 @Required:依賴檢查;

2 @Autowired:自動裝配2 自動裝配,用於替代基於XML配置的自動裝配 基於@Autowired的自動裝配,默認是根據類型注入,可以用於構造器、字段、方法注入

3 @Value:注入SpEL表達式 用於注入SpEL表達式,可以放置在字段方法或參數上
@Value(value = “SpEL表達式”)
@Value(value = “#{message}”)
4 @Qualifier:限定描述符,用於細粒度選擇候選者
@Qualifier限定描述符除了能根據名字進行注入,但能進行更細粒度的控制如何選擇候選者
@Qualifier(value = “限定標識符”)

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