蘇州做網(wǎng)站優(yōu)化網(wǎng)站關(guān)鍵詞怎么快速上排名
文章目錄
- 基礎(chǔ)
- 基礎(chǔ)數(shù)據(jù)類型
- 內(nèi)部類
- Java IO
- IO多路復(fù)用
- 重要概念 Channel **通道**
- 重要概念 Buffer **數(shù)據(jù)緩存區(qū)**
- 重要概念 Selector **選擇器**
- 關(guān)鍵字
- final
- 元注解
- 常用接口
- 異常處理
- Error
- Exception
- JVM與虛擬機
- JVM內(nèi)存模型
- 本地方法棧
- 虛擬機棧 Stack
- 堆 Heap
- 方法區(qū) Method Area (`JDK8以后被元空間Metaspace替代`)
- 程序計數(shù)器
- TLAB(*Thread Local Allocation Buffer*)
- 內(nèi)存間操作
- JVM的GC
- 類加載機制
- 類加載器
- 引用類型
- Java類加載器
- Tomcat類加載器
- FAQ
- 集合
- Collections
- (快速失敗)fail-fast VS (安全失敗)fail-safe
- FAQ
- HashSet
- Enumeration VS Iterator
- CopyOnWriteArrayList
- ArrayList
- ArrayList VS Vector 區(qū)別
- ArrayList VS Array*(即數(shù)組,非類)* 區(qū)別
- Map
- Hashtable
- Hashtable VS HashMap 區(qū)別
- Hashtable VS ConcurrentHashMap 區(qū)別
- ConcurrentMap
- JDK 7
- JDK 8
- HashMap工作原理
- 重要參數(shù)
- 重要方法
- 與JDK7對比
- 細節(jié)總結(jié)
- 結(jié)構(gòu)體系
- 序列化
- 序列化允許重構(gòu)
- 序列化并不安全
- 序列化的數(shù)據(jù)可以被簽名和密封
- 序列化允許將代理放在流中
- 信任,但要驗證
- 參考鏈接
- Java特性列表
- Java5
- Java6
- Java7
- `Java8 (LTS)`
- Java9
- Java10
- `Java11 (LTS)`
- Java12
- Java13
- Java14
- Java16
- Java12
- Java13
- Java14
- Java16
基礎(chǔ)
基礎(chǔ)數(shù)據(jù)類型
類型 | 數(shù)據(jù)大小(位) | 數(shù)據(jù)范圍 |
---|---|---|
int | 32 | -2^31 - 2^31-1 |
long | 64 | -2^63 - 2^63-1 |
float | 32 | ^ |
double | 64 | |
boolean | ||
short | 16 | -2^15 - 2^15-1 |
byte | 8 | -128 - 127 |
char | 16 | 存儲unicode碼 |
內(nèi)部類
-
成員內(nèi)部類
- 定義在一個類的內(nèi)部,可以認為是外部類的一個成員變量,它可以
無條件訪問成員內(nèi)部類的所有成員屬性和方法(包括私有和靜態(tài))
- HashMap的EntrySet, KeySet等
- 需要注意的是,當成員內(nèi)部類擁有和外部類同名的成員變量或者方法時,會發(fā)生隱藏現(xiàn)象。即默認情況下,訪問的是成員內(nèi)部類的成員。
- 定義在一個類的內(nèi)部,可以認為是外部類的一個成員變量,它可以
-
靜態(tài)內(nèi)部類
- HashMap的Node類
-
局部內(nèi)部類
+ -
匿名內(nèi)部類
- 使用匿名內(nèi)部類實現(xiàn)的多線程實例
public class ThreadTest extends Thread {public static void main(String[] args) {new ThreadTest() {@Overridepublic void run() {// ...}}.start();}
}
Java IO
IO多路復(fù)用
目前流行的多路復(fù)用IO實現(xiàn)主要包括四種:select, poll, epoll, kqueue
IO模型 | 相對性能 | 關(guān)鍵思路 | 操作系統(tǒng) | JAVA支持情況 |
---|---|---|---|---|
select | 較高 | Reactor | windows/Linux | 支持,Reactor模式(反應(yīng)器設(shè)計模式)。Linux操作系統(tǒng)的 kernels 2.4內(nèi)核版本之前,默認使用select;而目前windows下對同步IO的支持,都是select模型 |
poll | 較高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6內(nèi)核版本之前使用poll進行支持。也是使用的Reactor模式 |
epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6內(nèi)核版本及以后使用epoll進行支持;Linux kernels 2.6內(nèi)核版本之前使用poll進行支持;另外一定注意,由于Linux下沒有Windows下的IOCP技術(shù)提供真正的 異步IO 支持,所以Linux下使用epoll模擬異步IO |
kqueue | 高 | Proactor | Linux | 目前JAVA的版本不支持 |
重要概念 Channel 通道
被建立的一個應(yīng)用程序和操作系統(tǒng)交互事件、傳遞內(nèi)容的渠道 (注意是連接到操作系統(tǒng)
)。一個通道會有一個專屬的文件狀態(tài)描述符。那么既然是和操作系統(tǒng)進行內(nèi)容的傳遞,那么說明應(yīng)用程序可以通過通道讀取數(shù)據(jù),也可以通過通道向操作系統(tǒng)寫數(shù)據(jù)。
重要概念 Buffer 數(shù)據(jù)緩存區(qū)
為了保證每個通道的數(shù)據(jù)讀寫速度,Java NIO框架為每一種需要支持數(shù)據(jù)讀寫的通道集成了Buffer的支持
重要概念 Selector 選擇器
關(guān)鍵字
final
使用final修飾的方法無法被重寫,類無法被繼承。
元注解
- @Retention:保留的范圍
- SOURCE:注解將被編譯器丟棄(該類型的注解信息只會保留在源碼里,源碼經(jīng)過編譯后,注解信息會被丟棄,不會保留在編譯好的class文件里),如*@Override*
- CLASS:注解在class文件中可用,但會被VM丟棄(該類型的注解信息會保留在源碼里和class文件里,在執(zhí)行的時候,不會加載到虛擬機中),請注意,當注解未定義Retention值時,默認是CLASS
- RUNTIME:注解信息將在運行期(JVM)也保留,因此可以通過反射機制讀取注解的信息(源碼、class文件和執(zhí)行的時候都有注解的信息),如*@Deprecated*
- @Target:可以用來修飾哪些程序元素,如TYPE、METHOD、CONSTRUCTOR、FIELD、PARAMETER等,未標注則表示可修飾所有
- @Inherited:是否可以被繼承,默認為false
- @Documented:是否會保存到Javadoc文檔中
常用接口
- Comparable和Comparator接口
- Comparable
- 只包含一個compareTo方法,可以給兩個對象排序。具體來說,它返回負數(shù)、0、正數(shù)來表示輸入對象對于、等于、大于已經(jīng)存在的對象
- Comparator
- 包含equals、compare兩個方法。compare方法用來給兩個輸入?yún)?shù)排序,返回負數(shù)、0、正數(shù)表示第一個參數(shù)是小于、等于、大于第二個參數(shù)。equals方法需要一個對象作為參數(shù),它用來決定輸入?yún)?shù)是否和comparator相等。只有當輸入?yún)?shù)也是一個comparator并且輸入?yún)?shù)和當前comparator的排序結(jié)果是相同的時候,這個方法才返回true
- Comparable
異常處理
- Error類對象由Java虛擬機生成并拋出,不可捕捉
- 不管有沒有異常,finally中的代碼都會執(zhí)行
- 當try、catch中有return時,finally中的代碼依然會繼續(xù)執(zhí)行
Error
- OutOfMemoryError
- StackOverflowError
- NoClassDeffoundError
Exception
- 非檢查性異常
- ArithmeticException
- ArrayIndexOutOfBoundsException
- ClassCastException
- IllegalArgumentException
- IndexOutOfBoundsException
- NullPointerException
- 檢查性異常
- IOException
- CloneNotSupportedException
- IllegalAccessException
- NoSuchFieldException
- NoSuchMethodException
JVM與虛擬機
JVM內(nèi)存模型
本地方法棧
虛擬機棧 Stack
線程私有的內(nèi)存區(qū)域
,每個方法執(zhí)行的同時都會創(chuàng)建一個棧幀 (Stack Frame)
,用于儲存局部變量表
(基本數(shù)據(jù)類型
、對象引用
)、操作數(shù)棧
、動態(tài)鏈接
、方法出口
等信息。
堆 Heap
-
線程共享
。儲存幾乎所有的實例對象
,由垃圾收集器
自動回收,堆區(qū)由各子線程共享使用
-
Java中的堆是用來存儲對象本身的以及數(shù)組(當然,數(shù)組引用是存放在Java棧中的),堆是被所有線程共享的,在JVM中只有一個堆。所有對象實例以及數(shù)組都要在堆上分配內(nèi)存。
- 隨著JIT發(fā)展,棧上分配,標量替換優(yōu)化技術(shù),在堆上分配變得不那么絕對,只能在server模式下才能啟用逃逸分析
- 棧上分配
- 逃逸分析
- 目的是判斷對象的作用域是否有可能逃逸出函數(shù)體
- 標量替換
- 允許將對象打散分配在棧上
- 比如,若一個對象擁有兩個字段,會將這兩個字段視作局部變量進行分配
- 允許將對象打散分配在棧上
- 逃逸分析
-
新生代
- Eden
minorGC后,未被回收的,到from區(qū),每一次到from,age+1
- FromSurvivor
- ToSurvivor
- Eden
-
老年代
方法區(qū) Method Area (JDK8以后被元空間Metaspace替代
)
- 被
所有線程
共享的內(nèi)存區(qū)域,用來儲存已被虛擬機加載的類信息
、常量
、靜態(tài)變量
、JIT編譯后的代碼
。運行時常量池
是方法區(qū)的一部分,用于存放編譯期間
生成的各種字面常量
和符號引用
- 運行時常量池,是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應(yīng)的運行時常量池就會被創(chuàng)建出來。當然并非Class文件常量池中的內(nèi)容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池,比如String的intern方法
程序計數(shù)器
指向當前線程所執(zhí)行的字節(jié)碼指令的(地址)行號
TLAB(Thread Local Allocation Buffer)
- 線程專用的內(nèi)存分配區(qū)域
- 由于對象一般會分配在堆上,而堆是全局共享的。因此在同一時間,可能會有多個線程在堆上申請空間。因此,每次對象分配都必須要進行同步(虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性),而在競爭激烈的場合,分配的效率又會進一步下降。JVM使用TLAM來避免多線程沖突,在給對象分配內(nèi)存時,每個線程使用自己的TLAB,這樣可以避免線程同步,提高了對象分配的效率
內(nèi)存間操作
- lock和unlock
- 把一個變量標識為一條線程獨占的狀態(tài)
- 把一個處于鎖定狀態(tài)的變量釋放出來,釋放之后的變量才能被其它線程鎖定
- read和write
- 把一個變量值從主內(nèi)存傳輸?shù)骄€程的工作內(nèi)存,以便load
- 把store操作從工作內(nèi)存得到的變量的值,放入主內(nèi)存的變量中
- load和store
- 把read操作從主內(nèi)存得到的變量值放入工作內(nèi)存的變量副本中
- 把工作內(nèi)存的變量值傳送到主內(nèi)存,以便write
- use和assign
- 把工作內(nèi)存變量值傳遞給執(zhí)行引擎
- 將執(zhí)行引擎值傳遞給工作內(nèi)存變量值
- volatile的實現(xiàn)基于這8種內(nèi)存間操作,保證了一個線程對某個volatile變量的修改,一定會被另一個線程看見,即保證了可見性
JVM的GC
-
分代垃圾回收算法
- 試想,在不進行對象存活時間區(qū)分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活對象,但實際上,對于生命周期長的對象而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是它們依舊存在。因此,分代垃圾回收采用分治的思想,進行代的劃分,把不同生命周期的對象放在不同代上,不同代上采用最適合它的垃圾回收方式進行回收
- 年輕代(Young Generation)
- 所有新生成的對象首先都是放在年輕代的
- 目標就是盡快收集生命周期短的對象
- 分三個區(qū)
- Eden區(qū)
- 大部分對象在這里生成,當Eden區(qū)滿時,還存活的對象將被復(fù)制到Servivor區(qū),當這個Survivor區(qū)滿時,此區(qū)的存活對象將被復(fù)制到另外一個Survivor區(qū),當所有Survivor區(qū)都滿時,從第一個Survivor區(qū)復(fù)制過來的,且此時還存活的對象,將被復(fù)制到年老區(qū)(Tenured)
- 兩個Survivor區(qū)是對稱的,沒先后關(guān)系
- FromSurvivor區(qū)
- ToSurvivor區(qū)
- Eden區(qū)
- 年老代(Old Generation)
- 在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象
- 持久代(Permanent Generation)
- 主要存放Java類的類信息、方法,與垃圾收集要收集的Java對象關(guān)系不大
-
新生代Eden內(nèi)存快超出時,觸發(fā)
minor gc
,對象內(nèi)存從Eden到from,對象回收次數(shù)age+1 -
老年代內(nèi)存快超出時,觸發(fā)
full gc
-
Minor GC和Major GC
- Java內(nèi)存分配與回收策略
- 對象優(yōu)先在堆的Eden區(qū)分配
- 大對象直接進入老年代
- 長期存活的對象將直接進入老年代
- 當Eden區(qū)沒有足夠的空間進行分配時,虛擬機會執(zhí)行一次Minor GC。Minor GC通常發(fā)生在新生代的Eden區(qū),在這個區(qū)的對象生存期短,往往發(fā)生GC的頻率較高,回收速度比較快。Full GC/Major GC發(fā)生在老年代,一般情況下,觸發(fā)老年代GC的時候不會觸發(fā)Minor GC,但是通過配置,可以在Full GC之前進行一次Minor GC這樣加快老年代的回收速度
- 垃圾回收不會發(fā)生在永久代,如果永久代滿了或者是超過了臨界值,會觸發(fā)完全垃圾回收(Full GC)
- Java 8已經(jīng)移除了永久代,新加了一個叫元數(shù)據(jù)區(qū)的native內(nèi)存區(qū)
- Java內(nèi)存分配與回收策略
-
垃圾分析算法
判斷一個對象是否可被回收引用計數(shù)算法
為對象添加一個引用計數(shù)器
,當對象增加一個引用時
,計數(shù)器加1
,引用失效
時計數(shù)器減1
。引用計數(shù)為0的對象可被回收
。在兩個對象出現(xiàn)循環(huán)引用
的情況下,此時引用計數(shù)器永遠不為0,導致無法對他們進行回收。正是因為循環(huán)引用的存在,因此Java虛擬機不再使用計數(shù)算法可達性算法
主流
以GC Roots為起始點進行搜索,可達的對象都是存活的,不可達的對象可被回收。Java虛擬機使用該算法來判斷對象是否可回收,GC Roots一般包含以下內(nèi)容- 虛擬機棧中局部變量表中引用的對象
- 本地方法棧中JNI引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 雖然可達性算法可以判定一個對象是否能夠被回收,但是當滿足上述條件時,一個對象不一定會被回收。當一個對象不可達GC Root時,這個對象并不會馬上被回收,而是處于一個死緩的階段,若要被真正的回收,需要經(jīng)歷兩次標記
- 如果對象在可達性分析中沒有與GC Root的引用鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法或者已被虛擬機調(diào)用過,那么就認為是沒必要的。如果對象有必要執(zhí)行finalize()方法,那么這個對象將會放在一個稱為F-Queue的隊列中,虛擬機會觸發(fā)一個Finalize()線程去執(zhí)行,此線程是低優(yōu)先級的,并且虛擬機不會承諾一直等待它運行完,這是因為如果finalize()執(zhí)行緩慢或者發(fā)生了死鎖,那么就會造成F-Queue隊列一直等待,造成了內(nèi)存回收系統(tǒng)的崩潰。GC對處于F-Queue中的對象進行第二次標記,這時,該對象將被移除即將回收集合,等待回收
-
垃圾收集算法
標記-清除
算法:標記哪些要被回收的對象,然后統(tǒng)一回收- 優(yōu)點:簡單
- 缺點:垃圾回收后,內(nèi)存變得不連續(xù),造成很大零散的內(nèi)存區(qū)域
復(fù)制
算法 (大多數(shù)GC使用):將可用內(nèi)存按容量劃分為相等的兩部分,然后每次只使用其中的一塊,當一塊內(nèi)存用完時,就將還存活的對象復(fù)制到第二塊內(nèi)存上,然后一次性清除完第一塊內(nèi)存,再將第二塊上的對象復(fù)制到第一塊- 這種方式,內(nèi)存的代價太高,每次都要浪費一半的內(nèi)存。于是將算法進行了改進,內(nèi)存區(qū)域不再是按照1:1去劃分,而是將內(nèi)存劃分為8:1:1三部分,較大的那份內(nèi)存交Eden區(qū),其余是兩塊較小的內(nèi)存叫Survivor區(qū)。每次都會優(yōu)先使用Eden區(qū),若Eden區(qū)滿,就將對象復(fù)制到第二塊內(nèi)存區(qū)上,然后清除Eden區(qū),如果此時存活的對象太多,以至于Survivor不夠時,會將這些對象通過分配擔保機制復(fù)制到老年代中
- 優(yōu)點:GC后的內(nèi)存空間是連續(xù)的
- 缺點:真正存放新對象的內(nèi)存空間會變少
標記-整理
算法:為了解決標記-清除算法產(chǎn)生大量內(nèi)存碎片的問題;當對象存活率較高時,也解決了復(fù)制算法的效率問題。它的不同之處就是在清除對象的時候先將可回收對象移動到一端,然后清除掉端邊界以外的對象,這樣就不會產(chǎn)生內(nèi)存碎片- 缺點:效率低下
-
finalize()
- 用于關(guān)閉外部資源,但是try-finally等方式可以做的更好,并且該方法運行代價很高,不確定性大,無法保證各個對象的調(diào)用順序,因此最好不要使用
- 如果內(nèi)存總是充足的,那么垃圾回收可能永遠不會進行,也就是說finalize()可能永遠不會被執(zhí)行
- 有一種JNI(Java Native Interface )調(diào)用non-java程序(C或C++),finalize()的工作就是回收這部分內(nèi)存
- 用于關(guān)閉外部資源,但是try-finally等方式可以做的更好,并且該方法運行代價很高,不確定性大,無法保證各個對象的調(diào)用順序,因此最好不要使用
當一個對象可被回收時,如果需要執(zhí)行該對象的finalize方法,那么就有可能在該方法中讓對象重新被引用,從而實現(xiàn)自救。自救只能進行一次,如果回收的對象之前調(diào)用了finalize方法自救,后面回收時不會再調(diào)用該方法。
類加載機制
-
加載(Loading)
- 通過類的完全限定名稱獲取定義該類的二進制字節(jié)流
- 將該字節(jié)流表示的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運行時儲存結(jié)構(gòu)
- 在內(nèi)存中生成一個代表該類的Class對象,作為方法區(qū)中該類各種數(shù)據(jù)的訪問入口
-
連接(Linking)
-
驗證(Verification)
- 確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全
- 從整體上看,大致上會完成下面4個階段的檢驗動作
- 文件格式驗證:驗證字節(jié)流是否符合Class文件的規(guī)范,如主次版本號是否在當前虛擬機范圍內(nèi)、常量池中的常量是否有不被支持的類型
- 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析,如這個類是否有父類、是否集成了不被繼承的類等
- 字節(jié)碼驗證:是整個驗證過程中最復(fù)雜的一個階段,通過驗證數(shù)據(jù)流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如方法中的類型轉(zhuǎn)換是否正確,跳轉(zhuǎn)指令是否正確等
- 符號引用驗證:這個動作在后面的解析過程中發(fā)生,主要是為了確保解析動作能正確執(zhí)行
-
準備(Preparation)
- 類變量是被static修飾的變量,準備階段為類變量分配內(nèi)存并設(shè)置初始值,使用的是方法區(qū)的內(nèi)存
- 實例變量不會在這階段分配內(nèi)存
實例化不是類加載的一個過程,類加載發(fā)生在所有實例化操作之前,并且類加載只進行一次,實例化可以進行多次
-
解析(Resolution)
- 將常量池的符號引用替換為直接引用的過程
解析過程在某些情況下可以在初始化階段之后再開始,這是為了支持Java的動態(tài)綁定
-
-
初始化(Initialization)
- 初始化階段才真正開始執(zhí)行類中定義的Java程序代碼
- 虛擬機規(guī)范嚴格規(guī)定了有且只有5種情況必須立即對類進行初始化(而加載、驗證、準備自然需要在此之前開始)
- 遇到new、getstatic、putstatic、invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化
- 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main方法的那個類),虛擬機會先初始化這個主類
- 當使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化
- 對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化
-
使用(Using)
-
卸載(Unloading)
類加載器
- 啟動類加載器(Bootstrap ClassLoader)
- 加載Java核心類庫,無法被Java程序直接引用
- 擴展類加載器(Extensions ClassLoader)
- 加載Java的擴展庫。Java虛擬機的實現(xiàn)會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載Java類
- 系統(tǒng)類加載器(System ClassLoader)
- 根據(jù)Java應(yīng)用的類路徑(CLASSPATH)來加載Java類。一般來說,Java應(yīng)用的類都是由它來完成加載的。可以通過*ClassLoader.getSystemClassLoader()*來獲取它
- 用戶自定義類加載器
- 通過繼承java.lang.ClassLoader類的方式實現(xiàn)
引用類型
-
強引用
- 被強引用關(guān)聯(lián)的對象不會被回收。使用new一個新對象的方式來創(chuàng)建強引用
- 非靜態(tài)內(nèi)部類會在其整個生命周期中持有對它外部類的強引用
- 被強引用關(guān)聯(lián)的對象不會被回收。使用new一個新對象的方式來創(chuàng)建強引用
-
軟引用
- 被軟引用關(guān)聯(lián)的對象只有在內(nèi)存不夠的情況下才會被回收。使用SoftReference類來創(chuàng)建軟引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使對象只被軟引用關(guān)聯(lián)
- 弱引用
- 被弱引用關(guān)聯(lián)的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。使用WeakReference類來創(chuàng)建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虛引用
- 一個對象是否有虛引用的存在,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。為一個對象設(shè)置虛引用的唯一目的是能在這個對象被回收時收到系統(tǒng)的一個通知。使用PhantomReference來創(chuàng)建虛引用
- 只能通過是否被加入到ReferenceQueue來判斷是否被GC,這也是唯一判斷對象是否被GC的途徑
Object obj = new Object();
PhantomReference pf = new PhantomReference<Object>(obj, null);
obj = null;
Java類加載器
- 對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間
- 比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等
- 這里的相等,包括代表類的Class對象的equals、isAssignableFrom、isInstance方法返回的結(jié)果,也包括使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等情況
- 在自定義ClassLoader的子類的時候,我們常見的會有兩種做法,一種是重寫loadClass方法,另一種是重寫findClass。其實這兩種方法本質(zhì)上差不多,畢竟loadClass也會調(diào)用findClass,但是從邏輯上講我們最好不要直接修改loadClass的內(nèi)部邏輯,最好是只在findClass里重寫自定義類的加載方法
- loadClass這個方法是實現(xiàn)雙親委托模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題
- 比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等
雙親委派模型,該機制可以避免重復(fù)加載,當父親已經(jīng)加載了該類的時候,就沒有必要子
ClassLoader
再加載一次。JVM根據(jù)類名+包名+ClassLoader實例ID
來判定兩個類是否相同,是否已經(jīng)加載過。(可以通過創(chuàng)建不同的ClassLoader
實例來實現(xiàn)類的熱部署)
BootStrapClassLoader
最頂層的類加載器,由C++編寫而成,已經(jīng)內(nèi)嵌到JVM
中。在JVM啟動時會初始化該ClassLoader
,它主要用來讀取Java
的核心類庫JRE/lib/rt.jar
中所有的class
文件,這個jar
文件中包含了java
規(guī)范定義的所有接口及實現(xiàn)。
ExtensionClassLoader
用來讀取Java
的一些擴展類庫,如讀取JRE/lib/ext/*.jar
中的包
AppClassLoader
用來讀取classpath
下指定的所有jar
包或目錄的類文件,一般情況下,這個就是程序默認的類加載器
CustomClassLoader
用戶自定義編寫的,用來讀取指定類文件。基于自定義的ClassLoader
可用于加載非classpath
中(如從網(wǎng)絡(luò)上下載的jar
或二進制)的jar
及目錄、還可以在加載前對class
文件優(yōu)化一些動作,如解密、編碼等
ExtensionClassLoader
的父類加載器是BootStrapClassLoader
,其實這里省掉了一句話,容易造成很多新手的迷惑。嚴格說,ExtClassLoader
的父類加載器是null
,只不過是在默認的ClassLoader
的loadClass
方法中,當parent
為null
時,是交給BootStrapClassLoader
來處理的,而且ExtClassLoader
沒有重寫默認的loadClass
方法,所有,ExtClassLoader
也會調(diào)用BootStrapClassLoader
類加載器來加載。
Tomcat類加載器
- Common ClassLoader作為Catalina ClassLoader和Shared ClassLoader的parent,而Shared ClassLoader又可能存在多個children類加載器WebApp ClassLoader,一個WebApp ClassLoader實際上就對應(yīng)一個Web應(yīng)用,那Web應(yīng)用可能存在Jsp頁面,最終會轉(zhuǎn)成class類被加載,因此也需要一個Jsp的類加載器
- 在代碼層面,Catalina ClassLoader、Shared ClassLoader、Common ClassLoader對應(yīng)的實體類實際上都是URLClassLoader或者SecureClassLoader,一般我們只是根據(jù)加載內(nèi)容的不同和加載父子順序的關(guān)系,在邏輯上劃分為這三個類加載器
- 當tomcat啟動時,會創(chuàng)建幾種類加載器
- Bootstrap 引導類加載器
- 加載JVM啟動所需的類,以及標準擴展類(位于 jre/lib/ext 下)
- System 系統(tǒng)類加載器
- 加載tocmat啟動的類,比如bootstrap.jar,通常在 Catalina.bat 中指定。位于 CATALINA_HOME/bin 下
- Common 通用類加載器
- 加載tomcat使用以及應(yīng)用通用的一些類,位于 CATALINA_HOME/lib 下,比如 servlet-api.jar
- webapp 應(yīng)用類加載器
- 每個應(yīng)用在部署后,都會創(chuàng)建唯一的類加載器。該類加載器會加載位于 WEB-INF/lib 下的jar文件中的class和 WEB-INF/classes 下的class文件
- Bootstrap 引導類加載器
FAQ
-
Java中的內(nèi)存泄漏情況
- 長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對象已經(jīng)不再需要,但因為長生命周期持有它的引用而導致不能被回收
- 如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象,這個內(nèi)部類
- 長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對象已經(jīng)不再需要,但因為長生命周期持有它的引用而導致不能被回收
-
虛擬機棧和本地方法棧區(qū)別?
- 虛擬機棧為
虛擬機執(zhí)行Java方法
服務(wù) - 本地方法棧為
虛擬機使用到的Native方法服務(wù)
服務(wù)
- 虛擬機棧為
-
JVM內(nèi)存區(qū)域哪些會發(fā)生OOM
- 堆
- 拋出
OutOfMemoryError: Java heap space
- 通過控制
-Xmx
和-Xms
解決
- 拋出
Java虛擬機棧
和本地方法棧
- 如果線程請求的棧大于所分配的棧大小,則拋出
StackOverFlowError
錯誤,比如不會停止的遞歸調(diào)用 - 如果虛擬機棧是可以動態(tài)拓展的,拓展時無法申請到足夠的內(nèi)存,則拋出
OutOfMemoryError
錯誤
- 如果線程請求的棧大于所分配的棧大小,則拋出
- 方法區(qū)
- 拋出
OutOfMemoryError: Metaspace
- 拋出
- 永久代
- 舊版本的JDK,因JVM對永久代的垃圾回收并不積極。拋出
OutOfMemoryError: PermGen space
- 舊版本的JDK,因JVM對永久代的垃圾回收并不積極。拋出
- 堆
內(nèi)存區(qū)域 | 是否線程私有 | 是否會發(fā)生OOM |
---|---|---|
程序計數(shù)器 | 是 | 否 |
虛擬機棧 | 是 | 是 |
本地方法棧 | 是 | 是 |
方法區(qū) | 否 | 是 |
直接內(nèi)存 | 否 | 是 |
堆 | 否 | 是 |
-
*System.gc()*會做什么事情
- 提示JVM要進行垃圾回收,但實際是否進行回收取決于JVM
-
如果對象的引用被置為null,垃圾收集器是否會立即釋放對象占用的內(nèi)存
- 不會,在下一個垃圾回收周期中,這個對象將是可被回收的
-
GC為什么要分代?
- 減少stw次數(shù),即減少
full gc
次數(shù)
- 減少stw次數(shù),即減少
-
新生代為什么要替換to和from區(qū)?
-
重載和重寫的區(qū)別?
- 重載,發(fā)生在同一個類中,方法名必須相同,返回值和訪問修飾符可以不同,參數(shù)類型和個數(shù),順序不同
- 重寫**@Override**,發(fā)生在父子類中,方法名、參數(shù)列表必須相同,返回值范圍小于等于父類,拋出的異常范圍小于等于父類,訪問修飾符范圍大于等于父類;
如果父類方法訪問修飾符為private,則子類不能重寫
-
Java面向?qū)ο缶幊倘筇匦?#xff1a;封裝、繼承、多態(tài)
-
封裝
把一個對象的屬性私有化,同時提供一些可以被外界訪問的屬性的方法 -
繼承
使用已存在的類的定義作為基礎(chǔ)簡歷新類 -
多態(tài)
變量指向的具體類型和該變量發(fā)出的方法調(diào)用在編程時并不確定,而是在程序運行期間才確定。
Java中,通過兩種形式實現(xiàn)多態(tài)- 繼承 多個子類對同一方法的重寫
- 接口 實現(xiàn)接口并覆蓋接口中同一方法
-
集合
包含Map接口
和Collection接口
及其所有實現(xiàn)類
- Collection接口
- Set接口
- HashSet類
- TreeSet類
- LinkedHashSet類
- List接口
- ArrayList類
- LinkedList類
- Stack類
- Vector類
- Set接口
- Map接口
- HashMap類
- TreeMap類
- Hashtable類
- ConcurrentHashMap類
- Properties類
Collections
(快速失敗)fail-fast VS (安全失敗)fail-safe
- Iterator的安全失敗是基于對底層集合做拷貝,因此,它不受源集合上修改的影響。java.util包下面的所有的集合類都是快速失敗的,而java.util.concurrent包下面的所有的類都是安全失敗的。快速失敗的迭代器會拋出ConcurrentModificationException異常,而安全失敗的迭代器永遠不會拋出這樣的異常。
fail-fast: Java集合中的一種錯誤機制,在用迭代器遍歷
一個集合對象時,如果遍歷過程中對集合對象的結(jié)構(gòu)進行了修改(增加
,刪除
),則會拋出ConcurrentModificationException 并發(fā)修改異常
,防止繼續(xù)遍歷,這就是快速失敗機制。
JDK官方注釋:當Iterator這個迭代器被創(chuàng)建后,除了迭代器本身的方法(remove)可以改變集合的結(jié)構(gòu)外,其它因素如若
改變了集合的結(jié)構(gòu)
,都被拋出ConcurrentModificationException
異常
JDK官方注釋:迭代器的快速失敗行為是不一定能夠得到保證的,一般來說,存在非同步的并發(fā)修改時,不可能做出任何堅決的保證的。但是快速失敗迭代器會做出最大的努力來拋出
ConcurrentModificationException
。因此,編寫依賴于此異常的程序的做法是不正確的。正確的做法應(yīng)該是:迭代器的快速失敗行為應(yīng)該僅用于檢測程序中的bug
。
fail-safe:
不會拋出異常
- 復(fù)制時需要額外的空間和時間上的開銷
- 不能保證遍歷的是最新內(nèi)容
FAQ
- 如何避免fail-fast拋異常?
- 在遍歷時修改集合,使用迭代器的remove等方法,不用集合的remove等方法
- 并發(fā)的環(huán)境,需要對
Iterator對象加鎖
;也可以直接使用Collections.synchronizedList - 使用
CopyOnWriteArrayList
(采用fail-safe
)
HashSet
-
HashSet如何保證數(shù)據(jù)不可重復(fù)?
底層是
HashMap
,HashSet是實現(xiàn)了Set接口,并且把數(shù)據(jù)作為K值,而V值一直使用一個相同的虛值來保存
private static final Object PERSENT = new Object();public boolean add(E e) {return map.put(e, PERSENT) == null;
}
Enumeration VS Iterator
- 兩者都是
接口
- Iterator
- boolean hasNext()
- E next()
- default void remove()
- default void forEachRemaining(Consumer<? super E> action)
- Enumeration
- boolean hasMoreElements()
- E nextElement()
- Iterator
- Iterator支持
fail-fast機制
, Enumeration不支持 - Enumeration遍歷效率較高
CopyOnWriteArrayList
寫時復(fù)制,往一個容器添加元素的時候,先將當前容器復(fù)制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是,可以對CopyOnWriteArrayList進行并發(fā)的讀,而無需加鎖。所以CopyOnWriteArrayList也是一種讀寫分離的思想,讀和寫不同的容器。
- 添加元素的時候需要加鎖,否則多線程會復(fù)制N個副本
- 多個線程在添加數(shù)據(jù)時,讀到的是舊數(shù)據(jù)。因為寫的時候,不會鎖住舊的ArrayList
應(yīng)用場景: 用于讀多寫少的并發(fā)場景。比如白名單、黑名單、商品類目的訪問和更新場景
ArrayList
- 第一次添加元素時,數(shù)組大小將變化為DEFAULT_CAPACITY(10),不斷添加元素后,會進行擴容。刪除元素時,會按照位置關(guān)系把數(shù)組元素整體復(fù)制移動一遍
ArrayList VS Vector 區(qū)別
- Vector線程安全,ArrayList線程不安全
- 數(shù)據(jù)增長。Vector在數(shù)據(jù)滿時(
加載因子為1
),增長為原來的兩倍
。ArrayList在數(shù)據(jù)量達到容量的一半時(加載因子為0.5
),增長為原容量的0.5倍 + 1
個空間。
ArrayList VS Array*(即數(shù)組,非類)* 區(qū)別
- 數(shù)據(jù)可以包含
基本類型
和對象類型
,ArrayList只能包含對象類型
- 數(shù)據(jù)
大小固定
,ArrayList的大小不固定 - ArrayList提供更多方法
Map
- 永遠不要將可變對象類型用作HashMap中的鍵,因為hashCode()可能會隨著變動,導致get出來為null
Hashtable
Hashtable VS HashMap 區(qū)別
- HashMap沒有考慮同步,
線程不安全
;Hashtable使用了synchronized關(guān)鍵字(直接鎖方法
),線程安全
- HashMap允許
K/V都為null
;后者K/V都不允許為null
(HashMap存null時,hash值為0
) - HashMap繼承自
AbstractMap
類;而Hashtable繼承自Dictionary
類 - HashMap的迭代器(
Iterator
)是快速失敗的迭代器,Hashtable的迭代器(Enumeration
)不是。如果有其它線程對HashMap進行增刪操作,將會拋出ConcurrentModificationException
,但迭代器本身的remove
方法移除元素不會拋出異常。這也是Enumeration
和Iterator
的區(qū)別。
Hashtable VS ConcurrentHashMap 區(qū)別
- 都是線程安全
- Hashtable鎖住整個方法,ConcurrentHashMap鎖級別更細粒度
ConcurrentMap
JDK 7
當一個Map被多個線程訪問時,通常使用containsKey()
或者get()
來查看給定鍵是否在存儲鍵值對之前出現(xiàn)。但是即使有一個同步的Map,線程還是可以在這個過程中潛入,然后奪取對Map的控制權(quán)。問題是,在對put()的調(diào)用中,鎖在get()開始時獲取,然后在可以再次獲取鎖之前釋放。它的結(jié)果是個競爭條件:這是兩個線程之間的競爭,結(jié)果也會因誰先運行而不同
-
ConcurrentHashMap 最外層不是一個大的數(shù)組,而是一個Segment的數(shù)組。每個Segment包含一個與HashMap數(shù)據(jù)結(jié)構(gòu)差不多的鏈表數(shù)組
-
在讀寫某個key時,先取該key的哈希值,并將哈希值的高N位對Segment個數(shù)取模從而得到該key應(yīng)該屬于哪個Segment,接著如同操作HashMap一樣操作這個Segment。Segment繼承自ReentrantLock,可以很方便的對每一個Segment上鎖
-
讀操作
-
獲取key所在的Segment時,需要保證可見性。具體實現(xiàn)上,可以使用volatile關(guān)鍵字,也可使用鎖。但使用鎖開銷太大,而使用volatile時每次寫操作都會讓所有CPU內(nèi)緩存無效,也有一定開銷
-
ConcurrentHashMap使用如下方法保證可見性,取得最新的Segment
Segment<K,V> s = (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u)
獲取Segment中的HashEntry時也使用了類似方法
HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)
-
-
寫操作
- 不要求同時獲取所有Segment的鎖,因為那樣相當于鎖住了整個Map。它會先獲取該Key-Value對所在的Segment的鎖,獲取成功后就可以像操作一個普通的HashMap一樣操作該Segment,并保證該Segment的安全性,同時由于其它Segment的鎖并未被獲取,因此理論上可支持concurrencyLevel(等于Segment的個數(shù))個線程安全的并發(fā)讀寫
- 獲取鎖時,并不直接使用lock來獲取,因為該方法獲取鎖在失敗時會掛起。事實上,它使用了自旋鎖,如果tryLock獲取鎖失敗,說明鎖被其它線程占用,此時通過循環(huán)再次以tryLock的方式申請鎖。如果在循環(huán)過程中,該Key所對應(yīng)的鏈表頭被修改,則重置retry次數(shù)。如果retry次數(shù)超過一定值,則使用lock方法申請鎖
- 這里使用自旋鎖是因為自旋鎖的效率比較高,但是它消耗CPU資源比較多,因此在自旋次數(shù)超過閾值時切換為互斥鎖
-
JDK 8
- JDK 7已經(jīng)解決了并發(fā)問題,并且能支持N個Segment這么多次的并發(fā),但依然存在HashMap在JDK 7中的問題:查詢遍歷鏈表效率太低,因此JDK 8做了一些數(shù)據(jù)結(jié)構(gòu)上的調(diào)整
- 拋棄了原有的Segment分段鎖,采用了CAS + synchronized來保證并發(fā)安全性
HashMap工作原理
重要參數(shù)
-
loadFactor
負載因子,會影響到HashMap的擴容- 負載因子越大,HashMap的碰撞越劇烈,但是resize越少
- 負載因子越少,HashMap碰撞概率越小,但是resize越多。
-
threshold
要擴容的閾值。當容量大小等于這個閾值的時候,HashMap就會擴容
重要方法
put
- 取Key的hash值
- 判斷HashMap里面的槽是不是空的,如果是空的就需要初始化HashMap里面槽的數(shù)量
- 判斷這個Kay所對應(yīng)的槽是否被占用,如果沒有被占用,就將Key-Value生成一個新的節(jié)點,放到對應(yīng)的槽中
- 如果這個槽被占用了(
調(diào)用hash方法返回值一致,hash方法里面會調(diào)用hashCode方法
),分成三步判斷:- 判斷Key是否相同(
調(diào)用equals方法返回一致
),如果相同則返回原有的Node,如果不相同則進行下面的判斷 - 判斷節(jié)點是否屬于樹節(jié)點(TreeNode),如果屬于,則添加到紅黑樹中
- 如果不是樹節(jié)點,則一定是鏈表的Node,遍歷鏈表,如果鏈表長度大于等于8了,則將鏈表轉(zhuǎn)換成為紅黑樹 (
轉(zhuǎn)紅黑樹條件有二
),否則添加到鏈表中- 鏈表長度達到8
- 數(shù)組長度大于等于64
- 判斷Key是否相同(
- 最后判斷HashMap的大小是否大于閾值,如果大于,則進行擴容
resize
- 判斷當前的table里面的size是否大于0,如果大于0的話,就會判斷當前的table里面的size是否超過了最大值,如果超過最大值,就不會再擴容,如果沒有超過的話,就會將原有的size擴大到原來的兩倍,并且判斷擴容之后的size是否大于最大值,如果超過最大值就按照最大值來擴容。
- 如果當前table里面的size等于0的話,并且當前table里面的閾值大于0的時候,就會將原有的閾值設(shè)置為新的table的size大小
- 如果兩個條件都不滿足的話,則會對新的table設(shè)置默認的size(16),和默認的閾值(16 * 0.75)
- 如果這個時候新的table的閾值為空,則會重新計算新的閾值
HashMap的容量始終為2的冪次。在擴容的時候,元素的位置要么是在原位置,要么是否原位置再移動2次冪的位置
get
- 判斷現(xiàn)在HashMap里面的
table
是否為空,以及要獲取的key對應(yīng)的槽是否為空,如果為空,就直接返回null - 如果都不為空,就判斷槽里面的第一個node是不是想要找的key,如果是直接返回
- 如果第一個不是,就判斷node節(jié)點是不是樹節(jié)點,如果是,就直接去紅黑樹里面查找
- 如果也不是樹節(jié)點,那就在鏈表里面循環(huán)查找
- 判斷現(xiàn)在HashMap里面的
與JDK7對比
- 插入鏈表的時候,在JDK7的時候,HashMap插入鏈表是采用頭插法,而在JDK8使用的是尾插法,之所以這么改變的原因是因為,頭插法的鏈表在HashMap的resize()過程中可能因為多線程導致的逆序,讓鏈表形成死循環(huán)。
- 在JDK7的HashMap中,HashMap的數(shù)據(jù)結(jié)構(gòu)是
數(shù)組+單向鏈表
,在JDK8的HashMap中,采用的是數(shù)組+單鏈表+紅黑樹
的數(shù)據(jù)結(jié)構(gòu)。JDK7中可能存在鏈表過長,時間復(fù)雜度為O(n)
,導致查詢時間變長的情況。 - 在resiez過程中,JDK7和JDK8的差別主要是在遷移時計算新的索引的位置。JDK7是重新計算Key的hash值,然后用(size-1) & hash得到新的索引位置,而JDK8時,是采用判斷高一個bit位的位值,如果高一位的位值是0,那么索引位置就不變,如果是1那么就用原來的HashMap的size大小加上原有的索引位置(原索引+oldCap),這么改變是為了降低rehash帶來的開銷
細節(jié)總結(jié)
-
為什么JDK8的閾值要選擇8?
鏈表特點是插入快,查詢慢
。紅黑樹是插入慢,查詢快
。理想情況下,使用隨機的哈希碼導致沖突的概率符合泊松分布,按照泊松分布的計算公式計算出了桶中元素個數(shù)和hash沖突概率的對照表,可以看到鏈表中元素個數(shù)為8時,概率已經(jīng)非常小,再多的就更少了。紅黑樹本身就有維護成本,避免頻繁維護紅黑樹,紅黑樹變?yōu)殒湵?/code>。
-
紅黑樹的根節(jié)點不一定是
索引位置的頭節(jié)點
HashMap通過moveRootToFront
方法來維持紅黑樹的根結(jié)點就是索引位置的頭結(jié)點,但是在removeTreeNode
方法中,當movable
為false時,不會調(diào)用moveRootToFront
方法,此時紅黑樹的根節(jié)點不一定是索引位置的頭節(jié)點,該場景發(fā)生在HashIterator
的remove
方法中。 -
轉(zhuǎn)為紅黑樹節(jié)點后,鏈表的結(jié)構(gòu)還存在,通過next屬性維持,紅黑樹節(jié)點在進行操作時都會維護鏈表的結(jié)構(gòu)
-
為鏈表結(jié)構(gòu)時,是單向鏈表。為紅黑樹結(jié)構(gòu)時,成為雙向鏈表。雙向鏈表主要是為了紅黑樹相關(guān)鏈表操作方便
-
在HashMap中鏈表什么時候會轉(zhuǎn)換成紅黑樹?
- 鏈表在大于等于8個的時候 (
源碼驗證
) - 數(shù)組長度大于等于64
- 鏈表在大于等于8個的時候 (
// 鏈表長度大于等于8
static final int TREEIFY_THREESHOLD = 8;
if (binCount >= TREEIFY_THRESHOLD - 1) {treeifyBin(tab, hash)
}// 數(shù)組長度大于等于64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {...
}
-
HashMap什么時候擴容?
- 當前容量大于等于閾值
- 在樹化之前,當前數(shù)組的長度小于64,鏈表長度大于等于8
- 每次擴容,閾值=舊閾值<<1,容量=舊容量<<1
-
HashMap為什么選擇紅黑樹?
- AVL(自平衡二叉搜索樹),插入節(jié)點/刪除節(jié)點,整體性能不如紅黑樹。AVL每個左右節(jié)點的高度差不能大于1,維持這種結(jié)構(gòu)比較消耗性能。紅黑樹好在改變節(jié)點顏色即可
- 二分查找樹,左右節(jié)點不平衡,一開始就固定root,極端情況下會成為鏈表結(jié)構(gòu)
-
怎么解決哈希沖突?
- 鏈地址法,鏈接擁有相同hash值的數(shù)據(jù)
- 使用2次擾動函數(shù)(hash函數(shù)),降低哈希沖突概率,使數(shù)據(jù)分布更平均
- 引入紅黑樹,降低遍歷的時間復(fù)雜度
-
HashMap為什么不直接使用hashCode方法值用作table的下標?
hashCode返回值是int
數(shù)據(jù)類型,范圍是-(2^31) ~ 2^31 - 1
,而HashMap的容量范圍是16 ~ 2^30
,導致通過hashCode
計算出的哈希值可能不在數(shù)組大小范圍內(nèi),進而無法匹配存儲位置。 -
HashMap中怎么計算key的hash值?
static final int(Object key) {int h;return (key == null) ? 0 : (h == key.hashCode()) ^ (h >>> 16);
}
-
為什么數(shù)組長度要保證為2的冪次方?
只有當數(shù)組長度為2的冪次方時,h & (length-1)
才等價h % length
。 -
為什么String、Integer這樣的類適合作為K?
- 這些類能夠保證Hash值的不可更改性和計算準確性
- 都是final類型,即不可變性,保證key的不可更改性,不會存在獲取hash值不同的情況
- 內(nèi)部已重寫
equals
、hashCode
等方法 (想讓自己的類作為Key,同理要重寫這兩個方法)
結(jié)構(gòu)體系
- JDK8以后,HashMap結(jié)構(gòu)實際上是由
數(shù)組
+鏈表
+紅黑樹
組成
序列化
java對象序列化是JDK1.1中引入的一組開創(chuàng)性特性之一,用于作為一種將Java對象的狀態(tài)轉(zhuǎn)換為字節(jié)數(shù)組,以便存儲或傳輸?shù)臋C制,以后,仍可以將字節(jié)數(shù)組轉(zhuǎn)換回Java對象原有的狀態(tài)。
序列化允許重構(gòu)
序列化允許一定數(shù)量的變種淚,甚至重構(gòu)之后也是如此,(新舊類保持同一個序列化版本hash
,即private static final serialVersionUID
字段)。ObjectInputStream
仍可以很好的將其讀出來。Java Object Serialization規(guī)范可以自動管理的關(guān)鍵任務(wù)是
- 將
新字段
添加到類 - 將字段從
static
改為非static
- 將字段從
transient
改為非transient
序列化并不安全
序列化二進制格式完全編寫在文檔中,并且完全可逆。實際上,只需將二進制序列化流的內(nèi)容轉(zhuǎn)儲到控制臺,就足以看清類是什么樣子。好在序列化允許hook
序列化過程??梢酝ㄟ^在Serializable對象
上提供一個writeObject
方法來做到這一點。
public class Person implements java.io.Serializable {private int age;// 這兩個方法不是override,要求方法和參數(shù)與下面要求一致private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException {age = age << 2;stream.defaultWriteObject();}private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException {stream.defaultReadObject();age = age << 2;}
}
序列化的數(shù)據(jù)可以被簽名和密封
可以通過writeObject
和readObject
實現(xiàn)密碼加密和簽名管理,但其實還有更好的方式。如果需要對整個對象進行加密和簽名,最簡單的是將它放在一個javax.crypto.SealedObject
和java.security.SignedObject
包裝器中。兩者都是可序列化的,所以將對象包裝在SealedObject
中可以圍繞原對象創(chuàng)建一種“包裝盒”。必須有對稱密鑰才能解密,而且密鑰必須單獨管理。同樣,也可以將SignedObject
用于數(shù)據(jù)驗證,并且對稱密鑰也必須單獨管理。
序列化允許將代理放在流中
很多情況下,類中包含一個核心數(shù)據(jù)元素,通過它可以派生或找到類中的其他字段。在此情況下,沒有必要序列化整個對象??梢詫⒆侄螛擞洖閠ransient,但是每當有方法訪問一個字段時,類仍然必須顯式地產(chǎn)生代碼來檢查它是否被初始化。
如果首要問題是序列化,那么最好指定一個flyweight或代理放在流中。為原始person提供一個writeReplace方法,可以序列化不同類型的對象來代替它。類似地,如果反序列化期間發(fā)現(xiàn)一個readResolve方法,那么將調(diào)用方法,將替代對象提供給調(diào)用者。
class PersonProxy implements java.io.Serializable {public String data;private Object readResolve() throws java.io.ObjectStreamException {String[] pieces = data.split(",");Person result = new Person(...);// do somethingreturn result;}
}public class Person implements java.io.Serializable {//
}
注意,PersonProxy必須跟蹤Person的所有數(shù)據(jù)。這通常意味著代理需要是Person的一個內(nèi)部類,以便能訪問private字段。有時候,代理還需要追蹤其他對象引用并手動序列化他們。
這種技巧是少數(shù)幾種不需要讀/寫平衡的技巧之一。例如,一個類被重構(gòu)成另一種類型后的版本可以提供一個readResolve方法,以便靜默地將序列化的對象轉(zhuǎn)換成新類型。類似地,它可以采用writeReplace方法將舊類序列化成新版本。
信任,但要驗證
對于序列化的對象,這意味著驗證字段,以確保在反序列化之后,它們?nèi)跃哂姓_的值。為此,可以實現(xiàn)ObjectInputValidation
接口,并覆蓋validateObject
()方法,如果調(diào)用該方法時發(fā)現(xiàn)某處有錯誤,則拋出一個InvalidObjectException
。
參考鏈接
Java 對象序列化
Java特性列表
Java5
- 自動裝箱拆箱
- 泛型
- 枚舉
- 變長參數(shù)
- 注解
- foreach循環(huán)
- 靜態(tài)導入
- 新的線程模型和并發(fā)庫
java.util.concurrent
Java6
- 腳本引擎
- Java Compiler API
- JDBC 4.0規(guī)范
Java7
- 字面常量數(shù)字的下劃線
- switch支持String
- 泛型實例化類型自動推斷
// java7之前
Map<String,List<String>> map = new HashMap<String,List<String>>();
// java7之后
Map<String,List<String>> map = new HashMap<>();
try-with-resources
- 單個catch中捕獲多個異常類型
- NIO 2.0 (
AIO
)
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.put(new byte[16]);
buffer.position();
Java8 (LTS)
Lambda
表達式- 新
Date Time API
- 接口的
默認方法
和靜態(tài)方法
public interface Test {default String defaultFunction() {return "default";}static String staticFunction() {return "static";}
}
- 方法引用
- 集合的
stream
操作 Optional
處理空指針異常
Java9
- REPL工具
jShell
- 模塊系統(tǒng)
- 增強Stream
Java10
- 局部變量類型推斷
var url = new URL("http://www.oracle.com/")
- Optional新增
orElseThrow
方法
Java11 (LTS)
- 字符串、集合、Stream、Optional、InputStream增強
- java直接編譯運行
# before
javac Javastack.java; java Javastack# after
java Javastack.java
Java12
- switch增強
// 之前
switch (day) {case MONDAY:// XXX
}// 之后
switch (day) {case MONDAY -> "monday";
}
- 其他增強
Java13
Java14
- 增強instanceof
// 無需自己進行類型轉(zhuǎn)換
public boolean isBadRequestError(Exception ex) {return (ex instanceof HttpClientErrorException rce) && HttpStatus.BAD_REQUEST == rce.getStatusCode();
}
- JDK12引入的switch在JDK14變?yōu)檎桨姹?。增強switch,提供
yield
在塊中返回值,但不同于return
String quantityString = switch (n) {case 1 : yield "one";case 2 -> "two";
}
- 長文本字符串
String name = """what's your name""";
##Java15
- 關(guān)鍵字
sealed
、permits
、non-sealed
限定實現(xiàn)類,限定父類的使用。
Java16
##Java17 (LTS)
m/")
+ Optional新增`orElseThrow`方法## `Java11 (LTS)`+ 字符串、集合、Stream、Optional、InputStream增強
+ java直接編譯運行```shell
# before
javac Javastack.java; java Javastack# after
java Javastack.java
Java12
- switch增強
// 之前
switch (day) {case MONDAY:// XXX
}// 之后
switch (day) {case MONDAY -> "monday";
}
- 其他增強
Java13
Java14
- 增強instanceof
// 無需自己進行類型轉(zhuǎn)換
public boolean isBadRequestError(Exception ex) {return (ex instanceof HttpClientErrorException rce) && HttpStatus.BAD_REQUEST == rce.getStatusCode();
}
- JDK12引入的switch在JDK14變?yōu)檎桨姹?。增強switch,提供
yield
在塊中返回值,但不同于return
String quantityString = switch (n) {case 1 : yield "one";case 2 -> "two";
}
- 長文本字符串
String name = """what's your name""";
##Java15
- 關(guān)鍵字
sealed
、permits
、non-sealed
限定實現(xiàn)類,限定父類的使用。
Java16
##Java17 (LTS)