Android
Activity生命周期
onStart()與onResume()有什么區(qū)別?
onStart()是activity界面被顯示出來的時候執(zhí)行的,但不能與它交互;
onResume()是當(dāng)該activity與用戶能進(jìn)行交互時被執(zhí)行,用戶可以獲得activity的焦點(diǎn),能夠與用戶交互。
Activity啟動流程
startActivity最終都會調(diào)用startActivityForResult,通過ActivityManagerProxy調(diào)用system_server進(jìn)程中ActivityManagerService的startActvity方法,如果需要啟動的Activity所在進(jìn)程未啟動,則調(diào)用Zygote孵化應(yīng)用進(jìn)程,進(jìn)程創(chuàng)建后會調(diào)用應(yīng)用的ActivityThread的main方法,main方法調(diào)用attach方法將應(yīng)用進(jìn)程綁定到ActivityManagerService(保存應(yīng)用的ApplicationThread的代理對象)并開啟loop循環(huán)接收消息。ActivityManagerService通過ApplicationThread的代理發(fā)送Message通知啟動Activity,ActivityThread內(nèi)部Handler處理handleLaunchActivity,依次調(diào)用performLaunchActivity,handleResumeActivity(即activity的onCreate,onStart,onResume)。
深入理解Activity啟動流程
Android類加載器
Android平臺上虛擬機(jī)運(yùn)行的是Dex字節(jié)碼,一種對class文件優(yōu)化的產(chǎn)物,傳統(tǒng)Class文件是一個Java源碼文件會生成一個.class文件,而Android是把所有Class文件進(jìn)行合并,優(yōu)化,然后生成一個最終的class.dex,目的是把不同class文件重復(fù)的東西只需保留一份,如果我們的Android應(yīng)用不進(jìn)行分dex處理,最后一個應(yīng)用的apk只會有一個dex文件。
Android中常用的有兩種類加載器,DexClassLoader和PathClassLoader,它們都繼承于BaseDexClassLoader。區(qū)別在于調(diào)用父類構(gòu)造器時,DexClassLoader多傳了一個optimizedDirectory參數(shù),這個目錄必須是內(nèi)部存儲路徑,用來緩存系統(tǒng)創(chuàng)建的Dex文件。而PathClassLoader該參數(shù)為null,只能加載內(nèi)部存儲目錄的Dex文件。所以我們可以用DexClassLoader去加載外部的apk。
Android消息機(jī)制
- 應(yīng)用啟動是從ActivityThread的main開始的,先是執(zhí)行了Looper.prepare(),該方法先是new了一個Looper對象,在私有的構(gòu)造方法中又創(chuàng)建了MessageQueue作為此Looper對象的成員變量,Looper對象通過ThreadLocal綁定MainThread中;
- 當(dāng)我們創(chuàng)建Handler子類對象時,在構(gòu)造方法中通過ThreadLocal獲取綁定的Looper對象,并獲取此Looper對象的成員變量MessageQueue作為該Handler對象的成員變量;
- 在子線程中調(diào)用上一步創(chuàng)建的Handler子類對象的sendMesage(msg)方法時,在該方法中將msg的target屬性設(shè)置為自己本身,同時調(diào)用成員變量MessageQueue對象的enqueueMessag()方法將msg放入MessageQueue中;
- 主線程創(chuàng)建好之后,會執(zhí)行Looper.loop()方法,該方法中獲取與線程綁定的Looper對象,繼而獲取該Looper對象的成員變量MessageQueue對象,并開啟一個會阻塞(不占用資源)的死循環(huán),只要MessageQueue中有msg,就會獲取該msg,并執(zhí)行msg.target.dispatchMessage(msg)方法(msg.target即上一步引用的handler對象),此方法中調(diào)用了我們第二步創(chuàng)建handler子類對象時覆寫的handleMessage()方法,之后將該msg對象存入回收池;
Looper.loop()為什么不會阻塞主線程
Android是基于事件驅(qū)動的,即所有Activity的生命周期都是通過Handler事件驅(qū)動的。loop方法中會調(diào)用MessageQueue的next方法獲取下一個message,當(dāng)沒有消息時,基于Linux pipe/epoll機(jī)制會阻塞在loop的queue.next()中的nativePollOnce()方法里,并不會消耗CPU。
IdleHandler (閑時機(jī)制)
IdleHandler是一個回調(diào)接口,可以通過MessageQueue的addIdleHandler添加實(shí)現(xiàn)類。當(dāng)MessageQueue中的任務(wù)暫時處理完了(沒有新任務(wù)或者下一個任務(wù)延時在之后),這個時候會回調(diào)這個接口,返回false,那么就會移除它,返回true就會在下次message處理完了的時候繼續(xù)回調(diào)。
同步屏障機(jī)制(sync barrier)
同步屏障可以通過MessageQueue.postSyncBarrier函數(shù)來設(shè)置。該方法發(fā)送了一個沒有target的Message到Queue中,在next方法中獲取消息時,如果發(fā)現(xiàn)沒有target的Message,則在一定的時間內(nèi)跳過同步消息,優(yōu)先執(zhí)行異步消息。再換句話說,同步屏障為Handler消息機(jī)制增加了一種簡單的優(yōu)先級機(jī)制,異步消息的優(yōu)先級要高于同步消息。在創(chuàng)建Handler時有一個async參數(shù),傳true表示此handler發(fā)送的時異步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪制優(yōu)先執(zhí)行。
View的繪制原理
View的繪制從ActivityThread類中Handler的處理RESUME_ACTIVITY事件開始,在執(zhí)行performResumeActivity之后,創(chuàng)建Window以及DecorView并調(diào)用WindowManager的addView方法添加到屏幕上,addView又調(diào)用ViewRootImpl的setView方法,最終執(zhí)行performTraversals方法,依次執(zhí)行performMeasure,performLayout,performDraw。也就是view繪制的三大過程。
measure過程測量view的視圖大小,最終需要調(diào)用setMeasuredDimension方法設(shè)置測量的結(jié)果,如果是ViewGroup需要調(diào)用measureChildren或者measureChild方法進(jìn)而計算自己的大小。
layout過程是擺放view的過程,View不需要實(shí)現(xiàn),通常由ViewGroup實(shí)現(xiàn),在實(shí)現(xiàn)onLayout時可以通過getMeasuredWidth等方法獲取measure過程測量的結(jié)果進(jìn)行擺放。
draw過程先是繪制背景,其次調(diào)用onDraw()方法繪制view的內(nèi)容,再然后調(diào)用dispatchDraw()調(diào)用子view的draw方法,最后繪制滾動條。ViewGroup默認(rèn)不會執(zhí)行onDraw方法,如果復(fù)寫了onDraw(Canvas)方法,需要調(diào)用 setWillNotDraw(false);清楚不需要繪制的標(biāo)記。
Android視圖繪制流程完全解析,帶你一步步深入了解View(二)
什么是MeasureSpec
MeasureSpec代表一個32位int值,高兩位代表SpecMode(測量模式),低30位代表SpecSize(具體大?。?。
SpecMode有三類:
- UNSPECIFIED 表示父容器不對View有任何限制,一般用于系統(tǒng)內(nèi)部,表示一種測量狀態(tài);
- EXACTLY 父容器已經(jīng)檢測出view所需的精確大小,這時候view的最終大小SpecSize所指定的值,相當(dāng)于match_parent或指定具體數(shù)值。
- AT_MOST 父容器指定一個可用大小即SpecSize,view的大小不能大于這個值,具體多大要看view的具體實(shí)現(xiàn),相當(dāng)于wrap_content。
getWidth()方法和getMeasureWidth()區(qū)別呢?
首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進(jìn)行設(shè)置的,而getWidth()方法中的值則是通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的。
事件分發(fā)機(jī)制
requestLayout,invalidate,postInvalidate區(qū)別與聯(lián)系
相同點(diǎn):三個方法都有刷新界面的效果。
不同點(diǎn):invalidate和postInvalidate只會調(diào)用onDraw()方法;requestLayout則會重新調(diào)用onMeasure、onLayout、onDraw。
調(diào)用了invalidate方法后,會為該View添加一個標(biāo)記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區(qū)域,直到傳遞到ViewRootImpl中,最終觸發(fā)performTraversals方法,進(jìn)行開始View樹重繪流程(只繪制需要重繪的視圖)。
調(diào)用requestLayout方法,會標(biāo)記當(dāng)前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調(diào)用三大流程,從measure開始,對于每一個含有標(biāo)記位的view及其子View都會進(jìn)行測量onMeasure、布局onLayout、繪制onDraw。
Android View 深度分析requestLayout、invalidate與postInvalidate
Binder機(jī)制,共享內(nèi)存實(shí)現(xiàn)原理
為什么使用Binder?

概念
進(jìn)程隔離
進(jìn)程空間劃分:用戶空間(User Space)/內(nèi)核空間(Kernel Space)
系統(tǒng)調(diào)用:用戶態(tài)與內(nèi)核態(tài)
原理
跨進(jìn)程通信是需要內(nèi)核空間做支持的。傳統(tǒng)的 IPC 機(jī)制如管道、Socket 都是內(nèi)核的一部分,因此通過內(nèi)核支持來實(shí)現(xiàn)進(jìn)程間通信自然是沒問題的。但是 Binder 并不是 Linux 系統(tǒng)內(nèi)核的一部分,那怎么辦呢?這就得益于 Linux 的動態(tài)內(nèi)核可加載模塊(Loadable Kernel Module,LKM)的機(jī)制;模塊是具有獨(dú)立功能的程序,它可以被單獨(dú)編譯,但是不能獨(dú)立運(yùn)行。它在運(yùn)行時被鏈接到內(nèi)核作為內(nèi)核的一部分運(yùn)行。這樣,Android 系統(tǒng)就可以通過動態(tài)添加一個內(nèi)核模塊運(yùn)行在內(nèi)核空間,用戶進(jìn)程之間通過這個內(nèi)核模塊作為橋梁來實(shí)現(xiàn)通信。
在 Android 系統(tǒng)中,這個運(yùn)行在內(nèi)核空間,負(fù)責(zé)各個用戶進(jìn)程通過 Binder 實(shí)現(xiàn)通信的內(nèi)核模塊就叫 Binder 驅(qū)動(Binder Dirver)。
那么在 Android 系統(tǒng)中用戶進(jìn)程之間是如何通過這個內(nèi)核模塊(Binder 驅(qū)動)來實(shí)現(xiàn)通信的呢?難道是和前面說的傳統(tǒng) IPC 機(jī)制一樣,先將數(shù)據(jù)從發(fā)送方進(jìn)程拷貝到內(nèi)核緩存區(qū),然后再將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到接收方進(jìn)程,通過兩次拷貝來實(shí)現(xiàn)嗎?顯然不是,否則也不會有開篇所說的 Binder 在性能方面的優(yōu)勢了。
這就不得不通道 Linux 下的另一個概念:內(nèi)存映射。
Binder IPC 機(jī)制中涉及到的內(nèi)存映射通過 mmap() 來實(shí)現(xiàn),mmap() 是操作系統(tǒng)中一種內(nèi)存映射的方法。內(nèi)存映射簡單的講就是將用戶空間的一塊內(nèi)存區(qū)域映射到內(nèi)核空間。映射關(guān)系建立后,用戶對這塊內(nèi)存區(qū)域的修改可以直接反應(yīng)到內(nèi)核空間;反之內(nèi)核空間對這段區(qū)域的修改也能直接反應(yīng)到用戶空間。
一次完整的 Binder IPC 通信過程通常是這樣:
- 首先 Binder 驅(qū)動在內(nèi)核空間創(chuàng)建一個數(shù)據(jù)接收緩存區(qū);
- 接著在內(nèi)核空間開辟一塊內(nèi)核緩存區(qū),建立內(nèi)核緩存區(qū)和內(nèi)核中數(shù)據(jù)接收緩存區(qū)之間的映射關(guān)系,以及內(nèi)核中數(shù)據(jù)接收緩存區(qū)和接收進(jìn)程用戶空間地址的映射關(guān)系;
- 發(fā)送方進(jìn)程通過系統(tǒng)調(diào)用 copyfromuser() 將數(shù)據(jù) copy 到內(nèi)核中的內(nèi)核緩存區(qū),由于內(nèi)核緩存區(qū)和接收進(jìn)程的用戶空間存在內(nèi)存映射,因此也就相當(dāng)于把數(shù)據(jù)發(fā)送到了接收進(jìn)程的用戶空間,這樣便完成了一次進(jìn)程間的通信。
Binder通訊模型
Binder是基于C/S架構(gòu)的,其中定義了4個角色:Client、Server、Binder驅(qū)動和ServiceManager。
- Binder驅(qū)動:類似網(wǎng)絡(luò)通信中的路由器,負(fù)責(zé)將Client的請求轉(zhuǎn)發(fā)到具體的Server中執(zhí)行,并將Server返回的數(shù)據(jù)傳回給Client。
- ServiceManager:類似網(wǎng)絡(luò)通信中的DNS服務(wù)器,負(fù)責(zé)將Client請求的Binder描述符轉(zhuǎn)化為具體的Server地址,以便Binder驅(qū)動能夠轉(zhuǎn)發(fā)給具體的Server。Server如需提供Binder服務(wù),需要向ServiceManager注冊。
具體的通訊過程
- Server向ServiceManager注冊。Server通過Binder驅(qū)動向ServiceManager注冊,聲明可以對外提供服務(wù)。ServiceManager中會保留一份映射表。
- Client向ServiceManager請求Server的Binder引用。Client想要請求Server的數(shù)據(jù)時,需要先通過Binder驅(qū)動向ServiceManager請求Server的Binder引用(代理對象)。
- 向具體的Server發(fā)送請求。Client拿到這個Binder代理對象后,就可以通過Binder驅(qū)動和Server進(jìn)行通信了。
- Server返回結(jié)果。Server響應(yīng)請求后,需要再次通過Binder驅(qū)動將結(jié)果返回給Client。
ServiceManager是一個單獨(dú)的進(jìn)程,那么Server與ServiceManager通訊是靠什么呢?
當(dāng)Android系統(tǒng)啟動后,會創(chuàng)建一個名稱為servicemanager的進(jìn)程,這個進(jìn)程通過一個約定的命令BINDERSETCONTEXT_MGR向Binder驅(qū)動注冊,申請成為為ServiceManager,Binder驅(qū)動會自動為ServiceManager創(chuàng)建一個Binder實(shí)體。并且這個Binder實(shí)體的引用在所有的Client中都為0,也就說各個Client通過這個0號引用就可以和ServiceManager進(jìn)行通信。Server通過0號引用向ServiceManager進(jìn)行注冊,Client通過0號引用就可以獲取到要通信的Server的Binder引用。
寫給 Android 應(yīng)用工程師的 Binder 原理剖析
一篇文章了解相見恨晚的 Android Binder 進(jìn)程間通訊機(jī)制
序列化的方式
Serializable是Java提供的一個序列化接口,是一個空接口,用于標(biāo)示對象是否可以支持序列化,通過ObjectOutputStrean及ObjectInputStream實(shí)現(xiàn)序列化和反序列化的過程。注意可以為需要序列化的對象設(shè)置一個serialVersionUID,在反序列化的時候系統(tǒng)會檢測文件中的serialVersionUID是否與當(dāng)前類的值一致,如果不一致則說明類發(fā)生了修改,反序列化失敗。因此對于可能會修改的類最好指定serialVersionUID的值。
Parcelable是Android特有的一個實(shí)現(xiàn)序列化的接口,在Parcel內(nèi)部包裝了可序列化的數(shù)據(jù),可以在Binder中自由傳輸。序列化的功能由writeToParcel方法來完成,最終通過Parcel的一系列write方法完成。反序列化功能由CREAOR來完成,其內(nèi)部標(biāo)明了如何創(chuàng)建序列化對象和數(shù)組,并通過Parcel的一系列read方法來完成反序列化的過程。
Fragment的懶加載實(shí)現(xiàn)
Fragment可見狀態(tài)改變時會被調(diào)用setUserVisibleHint()方法,可以通過復(fù)寫該方法實(shí)現(xiàn)Fragment的懶加載,但需要注意該方法可能在onVIewCreated之前調(diào)用,需要確保界面已經(jīng)初始化完成的情況下再去加載數(shù)據(jù),避免空指針。
Fragment的懶加載
RecyclerView與ListView(緩存原理,區(qū)別聯(lián)系,優(yōu)缺點(diǎn))
緩存區(qū)別:
- 層級不同:
ListView有兩級緩存,在屏幕與非屏幕內(nèi)。
RecyclerView比ListView多兩級緩存,支持多個離屏ItemView緩存(匹配pos獲取目標(biāo)位置的緩存,如果匹配則無需再次bindView),支持開發(fā)者自定義緩存處理邏輯,支持所有RecyclerView共用同一個RecyclerViewPool(緩存池)。 - 緩存不同:
ListView緩存View。
RecyclerView緩存RecyclerView.ViewHolder,抽象可理解為:
View + ViewHolder(避免每次createView時調(diào)用findViewById) + flag(標(biāo)識狀態(tài));
優(yōu)點(diǎn)
RecylerView提供了局部刷新的接口,通過局部刷新,就能避免調(diào)用許多無用的bindView。
RecyclerView的擴(kuò)展性更強(qiáng)大(LayoutManager、ItemDecoration等)。
Android兩種虛擬機(jī)區(qū)別與聯(lián)系
Android中的Dalvik虛擬機(jī)相較于Java虛擬機(jī)針對手機(jī)的特點(diǎn)做了很多優(yōu)化。
Dalvik基于寄存器,而JVM基于棧。在基于寄存器的虛擬機(jī)里,可以更為有效的減少冗余指令的分發(fā)和減少內(nèi)存的讀寫訪問。
Dalvik經(jīng)過優(yōu)化,允許在有限的內(nèi)存中同時運(yùn)行多個虛擬機(jī)的實(shí)例,并且每一個 Dalvik應(yīng)用作為一個獨(dú)立的Linux進(jìn)程執(zhí)行。
java虛擬機(jī)運(yùn)行的是java字節(jié)碼。(java類會被編譯成一個或多個字節(jié)碼.class文件,打包到.jar文件中,java虛擬機(jī)從相應(yīng)的.class文件和.jar文件中獲取相應(yīng)的字節(jié)碼)
Dalvik運(yùn)行的是自定義的.dex字節(jié)碼格式。(java類被編譯成.class文件后,會通過一個dx工具將所有的.class文件轉(zhuǎn)換成一個.dex文件,然后dalvik虛擬機(jī)會從其中讀取指令和數(shù)據(jù))
Android開發(fā)之淺談java虛擬機(jī)和Dalvik虛擬機(jī)的區(qū)別
adb常用命令行
查看當(dāng)前連接的設(shè)備:adb devices
安裝應(yīng)用:adb install -r <apk_path> -r表示覆蓋安裝
卸載apk:adb uninstall <packagename>
apk打包流程
- aapt工具打包資源文件,生成R.java文件
- aidl工具處理AIDL文件,生成對應(yīng)的.java文件
- javac工具編譯Java文件,生成對應(yīng)的.class文件
- 把.class文件轉(zhuǎn)化成Davik VM支持的.dex文件
- apkbuilder工具打包生成未簽名的.apk文件
- jarsigner對未簽名.apk文件進(jìn)行簽名
- zipalign工具對簽名后的.apk文件進(jìn)行對齊處理
apk安裝流程
- 復(fù)制APK到/data/app目錄下,解壓并掃描安裝包。
- 資源管理器解析APK里的資源文件。
- 解析AndroidManifest文件,并在/data/data/目錄下創(chuàng)建對應(yīng)的應(yīng)用數(shù)據(jù)目錄。
- 然后對dex文件進(jìn)行優(yōu)化,并保存在dalvik-cache目錄下。
- 將AndroidManifest文件解析出的四大組件信息注冊到PackageManagerService中。
- 安裝完成后,發(fā)送廣播。
apk瘦身
APK主要由以下幾部分組成:
- META-INF/ :包含了簽名文件CERT.SF、CERT.RSA,以及 manifest 文件MANIFEST.MF。
- assets/ : 存放資源文件,這些資源不會被編譯成二進(jìn)制。
- lib/ :包含了一些引用的第三方庫。
- resources.arsc :包含res/values/中所有資源,例如strings,styles,以及其他未被包含在resources.arsc中的資源路徑信息,例如layout 文件、圖片等。
- res/ :包含res中沒有被存放到resources.arsc的資源。
- classes.dex :經(jīng)過dx編譯能被android虛擬機(jī)理解的Java源碼文件。
- AndroidManifest.xml :清單文件
其中占據(jù)較大內(nèi)存的是res資源、lib、class.dex,因此我們可以從下面的幾個方面下手:
- 代碼方面可以通過代碼混淆,這個一般都會去做。平時也可以刪除一些沒有使用類。
- 去除無用資源。使用
lint工具來檢測沒有使用到的資源,或者在gradle中配置shrinkResources來刪除包括庫中所有的無用的資源,需要配合proguard壓縮代碼使用。這里需要注意項(xiàng)目中是否存在使用getIdentifier方式獲取資源,這種方式類似反射lint及shrinkResources無法檢測情況。如果存在這種方式,則需要配置一個keep.xml來記錄使用反射獲取的資源。壓縮代碼和資源 - 去除無用國際化支持。對于一些第三庫來說(如support),因?yàn)閲H化的問題,它們可能會支持了幾十種語言,但我們的應(yīng)用可能只需要支持幾種語言,可以通過配置resConfigs提出不要的語言支持。
- 不同尺寸的圖片支持。通常情況下只需要一套xxhpi的圖片就可以支持大部分分辨率的要求了,因此,我們只需要保留一套圖片。
- 圖片壓縮。 png壓縮或者使用webP圖片,完美支持需要Android版本4.2.1+
- 使用矢量圖形。簡單的圖標(biāo)可以使用矢量圖片。
HTTP緩存機(jī)制

緩存的響應(yīng)頭:

Cache-control:標(biāo)明緩存的最大存活時常;
Date:服務(wù)器告訴客戶端,該資源的發(fā)送時間;
Expires:表示過期時間(該字段是1.0的東西,當(dāng)cache-control和該字段同時存在的條件下,cache-control的優(yōu)先級更高);
Last-Modified:服務(wù)器告訴客戶端,資源的最后修改時間;
還有一個字段,這個圖沒給出,就是E-Tag:當(dāng)前資源在服務(wù)器的唯一標(biāo)識,可用于判斷資源的內(nèi)容是否被修改了。
除以上響應(yīng)頭字段以外,還需了解兩個相關(guān)的Request請求頭:If-Modified-since、If-none-Match。這兩個字段是和Last-Modified、E-Tag配合使用的。大致流程如下:
服務(wù)器收到請求時,會在200 OK中回送該資源的Last-Modified和ETag頭(服務(wù)器支持緩存的情況下才會有這兩個頭哦),客戶端將該資源保存在cache中,并記錄這兩個屬性。當(dāng)客戶端需要發(fā)送相同的請求時,根據(jù)Date + Cache-control來判斷是否緩存過期,如果過期了,會在請求中攜帶If-Modified-Since和If-None-Match兩個頭。兩個頭的值分別是響應(yīng)中Last-Modified和ETag頭的值。服務(wù)器通過這兩個頭判斷本地資源未發(fā)生變化,客戶端不需要重新下載,返回304響應(yīng)。
組件化
- 在gradle.properties聲明一個變量用于控制是否是調(diào)試模式,并在dependencies中根據(jù)是否是調(diào)試模式依賴必要組件。
- 通過resourcePrefix規(guī)范module中資源的命名前綴。
- 組件間通過ARouter完成界面跳轉(zhuǎn)和功能調(diào)用。
MVP
三方庫
okhttp原理
OkHttpClient通過newCall可以將一個Request構(gòu)建成一個Call,Call表示準(zhǔn)備被執(zhí)行的請求。Call調(diào)用executed或enqueue會調(diào)用Dispatcher對應(yīng)的方法在當(dāng)前線程或者一步開始執(zhí)行請求,經(jīng)過RealInterceptorChain獲得最終結(jié)果,RealInterceptorChain是一個攔截器鏈,其中依次包含以下攔截器:
- 自定義的攔截器
- retryAndFollowUpInterceptor 請求失敗重試
- BridgeInterceptor 為請求添加請求頭,為響應(yīng)添加響應(yīng)頭
- CacheInterceptor 緩存get請求
- ConnectInterceptor 連接相關(guān)的攔截器,分配一個Connection和HttpCodec為最終的請求做準(zhǔn)備
- CallServerInterceptor 該攔截器就是利用HttpCodec完成最終請求的發(fā)送
Retrofit的實(shí)現(xiàn)與原理
Retrofit采用動態(tài)代理,創(chuàng)建聲明service接口的實(shí)現(xiàn)對象。當(dāng)我們調(diào)用service的方法時候會執(zhí)行InvocationHandler的invoke方法。在這方法中:首先,通過method把它轉(zhuǎn)換成ServiceMethod,該類是對聲明方法的解析,可以進(jìn)一步將設(shè)定參數(shù)變成Request ;然后,通過serviceMethod, args獲取到okHttpCall 對象,實(shí)際調(diào)用okhttp的網(wǎng)絡(luò)請求方法就在該類中,并且會使用serviceMethod中的responseConverter對ResponseBody轉(zhuǎn)化;最后,再把okHttpCall進(jìn)一步封裝成聲明的返回對象(默認(rèn)是ExecutorCallbackCall,將原本call的回調(diào)轉(zhuǎn)發(fā)至UI線程)。
Retrofit2使用詳解及從源碼中解析原理
Retrofit2 完全解析 探索與okhttp之間的關(guān)系
ARouter原理
RxLifecycle原理
在Activity中,定義一個Observable(Subject),在不同的生命周期發(fā)射不同的事件;
通過compose操作符(內(nèi)部實(shí)際上還是依賴takeUntil操作符),定義了上游數(shù)據(jù),當(dāng)其接收到Subject的特定事件時,取消訂閱;
Subject的特定事件并非是ActivityEvent,而是簡單的boolean,它已經(jīng)內(nèi)部通過combineLast操作符進(jìn)行了對應(yīng)的轉(zhuǎn)化。
RxJava
Java
類的加載機(jī)制
程序在啟動的時候,并不會一次性加載程序所要用的所有class文件,而是根據(jù)程序的需要,通過Java的類加載機(jī)制(ClassLoader)來動態(tài)加載某個class文件到內(nèi)存當(dāng)中的,從而只有class文件被載入到了內(nèi)存之后,才能被其它c(diǎn)lass所引用。所以ClassLoader就是用來動態(tài)加載class文件到內(nèi)存當(dāng)中用的。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準(zhǔn)備、驗(yàn)證、解析3個部分統(tǒng)稱為連接(Linking)。
- 加載:查找和導(dǎo)入Class文件;
- 鏈接:把類的二進(jìn)制數(shù)據(jù)合并到JRE中;
(a) 驗(yàn)證:檢查載入Class文件數(shù)據(jù)的正確性;
(b) 準(zhǔn)備:給類的靜態(tài)變量分配存儲空間;
(c) 解析:將符號引用轉(zhuǎn)成直接引用; - 初始化:對類的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作
什么時候發(fā)生類初始化
- 遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實(shí)例化對象的時候,讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機(jī)會先初始化這個主類。
- 當(dāng)使用JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實(shí)例左后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄鎖對應(yīng)的類沒有進(jìn)行過初始化時。
雙親委派模型
Java中存在3種類加載器:
(1) Bootstrap ClassLoader : 將存放于<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動類加載器無法被Java程序直接引用 。
(2) Extension ClassLoader : 將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫加載。開發(fā)者可以直接使用擴(kuò)展類加載器。
(3) Application ClassLoader : 負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可直接使用。
每個ClassLoader實(shí)例都有一個父類加載器的引用(不是繼承關(guān)系,是一個包含的關(guān)系),虛擬機(jī)內(nèi)置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但是可以用做其他ClassLoader實(shí)例的父類加載器。
當(dāng)一個ClassLoader 實(shí)例需要加載某個類時,它會試圖在親自搜索這個類之前先把這個任務(wù)委托給它的父類加載器,這個過程是由上而下依次檢查的,首先由頂層的類加載器Bootstrap ClassLoader進(jìn)行加載,如果沒有加載到,則把任務(wù)轉(zhuǎn)交給Extension ClassLoader加載,如果也沒有找到,則轉(zhuǎn)交給AppClassLoader進(jìn)行加載,還是沒有的話,則交給委托的發(fā)起者,由它到指定的文件系統(tǒng)或者網(wǎng)絡(luò)等URL中進(jìn)行加載類。還沒有找到的話,則會拋出CLassNotFoundException異常。否則將這個類生成一個類的定義,并將它加載到內(nèi)存中,最后返回這個類在內(nèi)存中的Class實(shí)例對象。
為什么使用雙親委托模型
JVM在判斷兩個class是否相同時,不僅要判斷兩個類名是否相同,還要判斷是否是同一個類加載器加載的。
- 避免重復(fù)加載,父類已經(jīng)加載了,則子CLassLoader沒有必要再次加載。
- 考慮安全因素,假設(shè)自定義一個String類,除非改變JDK中CLassLoader的搜索類的默認(rèn)算法,否則用戶自定義的CLassLoader如法加載一個自己寫的String類,因?yàn)镾tring類在啟動時就被引導(dǎo)類加載器Bootstrap CLassLoader加載了。
HashMap原理,Hash沖突
在JDK1.6,JDK1.7中,HashMap采用數(shù)組+鏈表實(shí)現(xiàn),即使用鏈表處理沖突,同一hash值的鏈表都存儲在一個鏈表里。但是當(dāng)位于一個鏈表中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。而JDK1.8中,HashMap采用位數(shù)組+鏈表+紅黑樹實(shí)現(xiàn),當(dāng)鏈表長度超過閾值(8)時,將鏈表轉(zhuǎn)換為紅黑樹,這樣大大減少了查找時間。
當(dāng)鏈表數(shù)組的容量超過初始容量*加載因子(默認(rèn)0.75)時,再散列將鏈表數(shù)組擴(kuò)大2倍,把原鏈表數(shù)組的搬移到新的數(shù)組中。為什么需要使用加載因子?為什么需要擴(kuò)容呢?因?yàn)槿绻畛浔群艽螅f明利用的空間很多,如果一直不進(jìn)行擴(kuò)容的話,鏈表就會越來越長,這樣查找的效率很低,擴(kuò)容之后,將原來鏈表數(shù)組的每一個鏈表分成奇偶兩個子鏈表分別掛在新鏈表數(shù)組的散列位置,這樣就減少了每個鏈表的長度,增加查找效率。
HashMap是非線程安全的,HashTable、ConcurrentHashMap是線程安全的。
HashMap的鍵和值都允許有null存在,而HashTable、ConcurrentHashMap則都不行。
因?yàn)榫€程安全、哈希效率的問題,HashMap效率比HashTable、ConcurrentHashMap的都要高。
HashTable里使用的是synchronized關(guān)鍵字,這其實(shí)是對對象加鎖,鎖住的都是對象整體,當(dāng)Hashtable的大小增加到一定的時候,性能會急劇下降,因?yàn)榈鷷r需要被鎖定很長的時間。
ConcurrentHashMap引入了分割(Segment),可以理解為把一個大的Map拆分成N個小的HashTable,在put方法中,會根據(jù)hash(paramK.hashCode())來決定具體存放進(jìn)哪個Segment,如果查看Segment的put操作,我們會發(fā)現(xiàn)內(nèi)部使用的同步機(jī)制是基于lock操作的,這樣就可以對Map的一部分(Segment)進(jìn)行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(HashTable就是這么做的),相對于HashTable提高了多線程環(huán)境下的性能,因此HashTable已經(jīng)被淘汰了。
Java中HashMap底層實(shí)現(xiàn)原理(JDK1.8)源碼分析
什么是Fail-Fast機(jī)制
Fail-Fast是Java集合的一種錯誤檢測機(jī)制。當(dāng)遍歷集合的同時修改集合或者多個線程對集合進(jìn)行結(jié)構(gòu)上的改變的操作時,有可能會產(chǎn)生fail-fast機(jī)制,記住是有可能,而不是一定。其實(shí)就是拋出ConcurrentModificationException 異常。
集合的迭代器在調(diào)用next()、remove()方法時都會調(diào)用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,從而產(chǎn)生fail-fast機(jī)制。modCount是在每次改變集合數(shù)量時會改變的值。
Java提高篇(三四)-----fail-fast機(jī)制
Java泛型
Java多線程中調(diào)用wait() 和 sleep()方法有什么不同?
Java程序中wait 和 sleep都會造成某種形式的暫停,它們可以滿足不同的需要。wait()方法用于線程間通信,如果等待條件為真且其它線程被喚醒時它會釋放鎖,而 sleep()方法僅僅釋放CPU資源或者讓當(dāng)前線程停止執(zhí)行一段時間,但不會釋放鎖。
volatile的作用和原理
Java代碼在編譯后會變成Java字節(jié)碼,字節(jié)碼被類加載器加載到JVM里,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行。
volatile是輕量級的synchronized(volatile不會引起線程上下文的切換和調(diào)度),它在多處理器開發(fā)中保證了共享變量的“可見性”??梢娦缘囊馑际钱?dāng)一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。
由于內(nèi)存訪問速度遠(yuǎn)不及CPU處理速度,為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存后在進(jìn)行操作,但操作完不知道何時會寫到內(nèi)存。普通共享變量被修改之后,什么時候被寫入主存是不確定的,當(dāng)其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性。如果對聲明了volatile的變量進(jìn)行寫操作,JVM就會想處理器發(fā)送一條Lock前綴的指令,表示將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
一個int變量,用volatile修飾,多線程去操作++,線程安全嗎?
不安全。volatile只能保證可見性,并不能保證原子性。i++實(shí)際上會被分成多步完成:1)獲取i的值;2)執(zhí)行i+1;3)將結(jié)果賦值給i。volatile只能保證這3步不被重排序,多線程情況下,可能兩個線程同時獲取i,執(zhí)行i+1,然后都賦值結(jié)果2,實(shí)際上應(yīng)該進(jìn)行兩次+1操作。
那如何才能保證i++線程安全?
可以使用java.util.concurrent.atomic包下的原子類,如AtomicInteger。
其實(shí)現(xiàn)原理是采用CAS自旋操作更新值。CAS即compare and swap的縮寫,中文翻譯成比較并交換。CAS有3個操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。自旋就是不斷嘗試CAS操作直到成功為止。
CAS實(shí)現(xiàn)原子操作會出現(xiàn)什么問題?
- ABA問題。因?yàn)镃AS需要在操作之的時候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成,有變成A,那么使用CAS進(jìn)行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但實(shí)際上發(fā)生了變化。ABA問題可以通過添加版本號來解決。Java 1.5開始,JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。
- 循環(huán)時間長開銷大。pause指令優(yōu)化。
- 只能保證一個共享變量的原子操作??梢院喜⒊梢粋€對象進(jìn)行CAS操作。
synchronized
Java中每個對象都可以作為鎖:
- 對于普通同步方法,鎖是當(dāng)前實(shí)例對象;
- 對于靜態(tài)同步方法,鎖是當(dāng)前類的Class對象;
- 對于同步方法塊,鎖是括號中配置的對象;
當(dāng)一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。synchronized用的鎖是存在Java對象頭里的MarkWord,通常是32bit或者64bit,其中最后2bit表示鎖標(biāo)志位
Java SE1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,在1.6中鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾種狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級。
偏向鎖
偏向鎖獲取過程:
- 訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1,鎖標(biāo)志位是否為01,確認(rèn)為可偏向狀態(tài)。
- 如果為可偏向狀態(tài),則測試線程ID是否指向當(dāng)前線程,如果是,進(jìn)入步驟5,否則進(jìn)入步驟3。
- 如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行5;如果競爭失敗,執(zhí)行4。
- 如果CAS獲取偏向鎖失敗,則表示有競爭。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼。(撤銷偏向鎖的時候會導(dǎo)致stop the word)
- 執(zhí)行同步代碼。
輕量級鎖
- 在代碼進(jìn)入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。
- 拷貝對象頭中的Mark Word復(fù)制到鎖記錄中;
- 拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟4,否則執(zhí)行步驟5。
- 如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài)。
- 如果這個更新操作失敗了,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。 而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程。
自旋
如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài),它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗。
但是線程自旋是需要消耗cup的,說白了就是讓cup在做無用功,如果一直獲取不到鎖,那線程也不能一直占用cup自旋做無用功,所以需要設(shè)定一個自旋等待的最大時間。
如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導(dǎo)致其它爭用鎖的線程在最大等待時間內(nèi)還是獲取不到鎖,這時爭用線程會停止自旋進(jìn)入阻塞狀態(tài)。
線程池
好處:1)降低資源消耗;2)提高相應(yīng)速度;3)提高線程的可管理性。
線程池的實(shí)現(xiàn)原理:
- 當(dāng)提交一個新任務(wù)到線程池時,判斷核心線程池里的線程是否都在執(zhí)行。如果不是,則創(chuàng)建一個新的線程執(zhí)行任務(wù)。如果核心線程池的線程都在執(zhí)行任務(wù),則進(jìn)入下個流程。
- 判斷工作隊列是否已滿。如果未滿,則將新提交的任務(wù)存儲在這個工作隊列里。如果工作隊列滿了,則進(jìn)入下個流程。
- 判斷線程池是否都處于工作狀態(tài)。如果沒有,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果滿了,則交給飽和策略來處理這個任務(wù)。
假如有n個網(wǎng)絡(luò)線程,你需要當(dāng)n個網(wǎng)絡(luò)線程完成之后,再去做數(shù)據(jù)處理,你會怎么解決?
這題考的其實(shí)是多線程同步的問題。這種情況可以可以使用thread.join();join方法會阻塞直到thread線程終止才返回。更復(fù)雜一點(diǎn)的情況也可以使用CountDownLatch,CountDownLatch的構(gòu)造接收一個int參數(shù)作為計數(shù)器,每次調(diào)用countDown方法計數(shù)器減一。做數(shù)據(jù)處理的線程調(diào)用await方法阻塞直到計數(shù)器為0時。
Java中interrupted 和 isInterruptedd方法的區(qū)別?
interrupted() 和 isInterrupted()的主要區(qū)別是前者會將中斷狀態(tài)清除而后者不會。Java多線程的中斷機(jī)制是用內(nèi)部標(biāo)識來實(shí)現(xiàn)的,調(diào)用Thread.interrupt()來中斷一個線程就會設(shè)置中斷標(biāo)識為true。當(dāng)中斷線程調(diào)用靜態(tài)方法Thread.interrupted()來 檢查中斷狀態(tài)時,中斷狀態(tài)會被清零。而非靜態(tài)方法isInterrupted()用來查詢其它線程的中斷狀態(tài)且不會改變中斷狀態(tài)標(biāo)識。簡單的說就是任何拋 出InterruptedException異常的方法都會將中斷狀態(tài)清零。無論如何,一個線程的中斷狀態(tài)有有可能被其它線程調(diào)用中斷來改變。
懶漢式單例的同步問題
同步的懶加載雖然是線程安全的,但是導(dǎo)致性能開銷。因此產(chǎn)生了雙重檢查鎖定。但雙重檢查鎖定存在隱藏的問題。instance = new Instance()實(shí)際上會分為三步操作:1)分配對象的內(nèi)存空間;2)初始化對象;3)設(shè)置instance指向剛分配的內(nèi)存地址;由于指令重排序,2和3的順序并不確定。在多線程的情況下,第一個線程執(zhí)行了1,3,此時第二個線程判斷instance不為null,但實(shí)際上操作2還沒有執(zhí)行,第二個線程就會獲得一個還未初始化的對象,直接使用就會造成空指針。
解決方案是用volatile修飾instance,在JDK 1.5加強(qiáng)了volatile的語意之后,用volatile修飾instance就阻止了2和3的重排序,進(jìn)而避免上述情況的發(fā)生。
另一種方式則是使用靜態(tài)內(nèi)部類:
public class Singleton {
private static class InstanceHolder {
public static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.instance;
}
}
其原理是利用類初始化時會加上初始化鎖確保類對象的唯一性。
什么是ThreadLocal
ThreadLocal即線程變量,它為每個使用該變量的線程提供獨(dú)立的變量副本,所以每一個線程都可以獨(dú)立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。從線程的角度看,目標(biāo)變量就象是線程的本地變量,這也是類名中“Local”所要表達(dá)的意思。ThreadLocal的實(shí)現(xiàn)是以ThreadLocal對象為鍵。任意對象為值得存儲結(jié)構(gòu)。這個結(jié)構(gòu)被附帶在線程上,也就是說一個線程可以根據(jù)一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
什么是數(shù)據(jù)競爭
數(shù)據(jù)競爭的定義:在一個線程寫一個變量,在另一個線程讀同一個變量,而且寫和讀沒有通過同步來排序。
Java內(nèi)存模型(Java Memory Model JMM)
JM屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。
線程之間的共享變量存儲在主內(nèi)存中,每個線程都有一個私有的本地內(nèi)存,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是一個抽象概念,它涵蓋了緩存、寫緩存區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。在多線程中重排序會對程序的執(zhí)行結(jié)果有影響。
JSR-133內(nèi)存模型采用happens-before的概念來闡述操作之間的內(nèi)存可見性。happens-before會限制重排序以滿足規(guī)則。
主要的happens-before規(guī)則有如下:
- 程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before與鎖隨后對這個鎖的加鎖。
- volatile變量規(guī)則:對一個volatile域的寫,happens-before與任意后續(xù)對這個volatile域的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
Java內(nèi)存區(qū)域
- 程序計數(shù)器:當(dāng)前線程鎖執(zhí)行的字節(jié)碼的行號指示器,用于線程切換恢復(fù),是線程私有的;
- Java虛擬機(jī)棧(棧):虛擬機(jī)棧也是線程私有的。每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。
- 本地方法棧:與虛擬機(jī)棧類似,服務(wù)于Native方法。
- Java堆:堆是被所有線程共享的一塊內(nèi)存,用于存放對象實(shí)例。是垃圾收集器管理的主要區(qū)域,也被稱作GC堆。
- 方法區(qū):與Java堆一樣,是線程共享的內(nèi)存區(qū)域,用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)常量、即時編譯器編譯后的代碼等數(shù)據(jù)。
- 運(yùn)行時常量池:是方法區(qū)的一部分,用于存放編譯器生成的各種字面量和符號引用。
判斷對象是否需要回收的方法
- 引用計數(shù)算法。實(shí)現(xiàn)簡單,判定效率高,但不能解決循環(huán)引用問題,同時計數(shù)器的增加和減少帶來額外開銷,JDK1.1以后廢棄了。
- 可達(dá)性分析算法/根搜索算法 。根搜索算法是通過一些“GC Roots”對象作為起點(diǎn),從這些節(jié)點(diǎn)開始往下搜索,搜索通過的路徑成為引用鏈(Reference Chain),當(dāng)一個對象沒有被GC Roots 的引用鏈連接的時候,說明這個對象是不可用的。 Java中可作為“GC Root”的對象包括:虛擬機(jī)棧(本地變量表)中引用的對象;方法區(qū)中類靜態(tài)屬性和常量引用的對象。本地方法棧中引用的對象。
引用類型
- 強(qiáng)引用:默認(rèn)的引用方式,不會被垃圾回收,JVM寧愿拋出OutOfMemory錯誤也不會回收這種對象。
- 軟引用(SoftReference):如果一個對象只被軟引用指向,只有內(nèi)存空間不足夠時,垃圾回收器才會回收它;
- 弱引用(WeakReference):如果一個對象只被弱引用指向,當(dāng)JVM進(jìn)行垃圾回收時,無論內(nèi)存是否充足,都會回收該對象。
- 虛引用(PhantomReference):虛引用和前面的軟引用、弱引用不同,它并不影響對象的生命周期。如果一個對象與虛引用關(guān)聯(lián),則跟沒有引用與之關(guān)聯(lián)一樣,在任何時候都可能被垃圾回收器回收。虛引用通常和ReferenceQueue配合使用。
ReferenceQueue
作為一個Java對象,Reference對象除了具有保存引用的特殊性之外,也具有Java對象的一般性。所以,當(dāng)對象被回收之后,雖然這個Reference對象的get()方法返回null,但這個SoftReference對象已經(jīng)不再具有存在的價值,需要一個適當(dāng)?shù)那宄龣C(jī)制,避免大量Reference對象帶來的內(nèi)存泄漏。
在java.lang.ref包里還提供了ReferenceQueue。我們創(chuàng)建Reference對象時使用兩個參數(shù)的構(gòu)造傳入ReferenceQueue,當(dāng)Reference所引用的對象被垃圾收集器回收的同時,Reference對象被列入ReferenceQueue。也就是說,ReferenceQueue中保存的對象是Reference對象,而且是已經(jīng)失去了它所軟引用的對象的Reference對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列,當(dāng)我們調(diào)用它的poll()方法的時候,如果這個隊列中不是空隊列,那么將返回隊列前面的那個Reference對象。于是我們可以在適當(dāng)?shù)臅r候把這些失去所軟引用的對象的SoftReference對象清除掉。
垃圾收集算法
- 標(biāo)記-清楚算法(Mark-Sweep)
在標(biāo)記階段,確定所有要回收的對象,并做標(biāo)記。清除階段緊隨標(biāo)記階段,將標(biāo)記階段確定不可用的對象清除。標(biāo)記—清除算法是基礎(chǔ)的收集算法,有兩個不足:1)標(biāo)記和清除階段的效率不高;2)清除后回產(chǎn)生大量的不連續(xù)空間,這樣當(dāng)程序需要分配大內(nèi)存對象時,可能無法找到足夠的連續(xù)空間。 - 復(fù)制算法(Copying)
復(fù)制算法是把內(nèi)存分成大小相等的兩塊,每次使用其中一塊,當(dāng)垃圾回收的時候,把存活的對象復(fù)制到另一塊上,然后把這塊內(nèi)存整個清理掉。復(fù)制算法實(shí)現(xiàn)簡單,運(yùn)行效率高,但是由于每次只能使用其中的一半,造成內(nèi)存的利用率不高?,F(xiàn)在的JVM 用復(fù)制方法收集新生代,由于新生代中大部分對象(98%)都是朝生夕死的,所以會分成1塊大內(nèi)存Eden和兩塊小內(nèi)存Survivor(大概是8:1:1),每次使用1塊大內(nèi)存和1塊小內(nèi)存,當(dāng)回收時將2塊內(nèi)存中存活的對象賦值到另一塊小內(nèi)存中,然后清理剩下的。 - 標(biāo)記—整理算法(Mark-Compact)
標(biāo)記—整理算法和復(fù)制算法一樣,但是標(biāo)記—整理算法不是把存活對象復(fù)制到另一塊內(nèi)存,而是把存活對象往內(nèi)存的一端移動,然后直接回收邊界以外的內(nèi)存。標(biāo)記—整理算法提高了內(nèi)存的利用率,并且它適合在收集對象存活時間較長的老年代。 - 分代收集(Generational Collection)
分代收集是根據(jù)對象的存活時間把內(nèi)存分為新生代和老年代,根據(jù)各代對象的存活特點(diǎn),每個代采用不同的垃圾回收算法。新生代采用復(fù)制算法,老年代采用標(biāo)記—整理算法。
內(nèi)存分配策略
- 對象優(yōu)先在Eden分配。
- 大對象直接進(jìn)入老年代。 大對象是指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的就是那種很長的字符串以及數(shù)組。
- 長期存活的對象進(jìn)入老年代。存活過一次新生代的GC,Age+1,當(dāng)達(dá)到一定程度(默認(rèn)15)進(jìn)入老年代。
- 動態(tài)對象年齡判定。如果在Survivor空間中相同Age所有對象大小的總和大于Survivor空間一半。那么Age大于等于該Age的對象就可以直接進(jìn)入老年代。
- 空間分配擔(dān)保。 在發(fā)生新生代GC之前,會檢查老年代的剩余空間是否大于新生代所有對象的總和。如果大于則是安全的,如果不大于有風(fēng)險。