Android面試準備(中高階)

胡奚冰發表於2018-08-15

Android

Activity生命週期

這裡寫圖片描述

onStart()與onResume()有什麼區別?

onStart()是activity介面被顯示出來的時候執行的,但不能與它互動;  onResume()是當該activity與使用者能進行互動時被執行,使用者可以獲得activity的焦點,能夠與使用者互動。

Activity啟動流程

startActivity最終都會呼叫startActivityForResult,通過ActivityManagerProxy呼叫system_server程式中ActivityManagerService的startActvity方法,如果需要啟動的Activity所在程式未啟動,則呼叫Zygote孵化應用程式,程式建立後會呼叫應用的ActivityThread的main方法,main方法呼叫attach方法將應用程式繫結到ActivityManagerService(儲存應用的ApplicationThread的代理物件)並開啟loop迴圈接收訊息。ActivityManagerService通過ApplicationThread的代理髮送Message通知啟動Activity,ActivityThread內部Handler處理handleLaunchActivity,依次呼叫performLaunchActivity,handleResumeActivity(即activity的onCreate,onStart,onResume)。
深入理解Activity啟動流程

Android類載入器

Android平臺上虛擬機器執行的是Dex位元組碼,一種對class檔案優化的產物,傳統Class檔案是一個Java原始碼檔案會生成一個.class檔案,而Android是把所有Class檔案進行合併,優化,然後生成一個最終的class.dex,目的是把不同class檔案重複的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex檔案。 Android中常用的有兩種類載入器,DexClassLoader和PathClassLoader,它們都繼承於BaseDexClassLoader。區別在於呼叫父類構造器時,DexClassLoader多傳了一個optimizedDirectory引數,這個目錄必須是內部儲存路徑,用來快取系統建立的Dex檔案。而PathClassLoader該引數為null,只能載入內部儲存目錄的Dex檔案。所以我們可以用DexClassLoader去載入外部的apk。

Android訊息機制

  1. 應用啟動是從ActivityThread的main開始的,先是執行了Looper.prepare(),該方法先是new了一個Looper物件,在私有的構造方法中又建立了MessageQueue作為此Looper物件的成員變數,Looper物件通過ThreadLocal繫結MainThread中;
  2. 當我們建立Handler子類物件時,在構造方法中通過ThreadLocal獲取繫結的Looper物件,並獲取此Looper物件的成員變數MessageQueue作為該Handler物件的成員變數;
  3. 在子執行緒中呼叫上一步建立的Handler子類物件的sendMesage(msg)方法時,在該方法中將msg的target屬性設定為自己本身,同時呼叫成員變數MessageQueue物件的enqueueMessag()方法將msg放入MessageQueue中;
  4. 主執行緒建立好之後,會執行Looper.loop()方法,該方法中獲取與執行緒繫結的Looper物件,繼而獲取該Looper物件的成員變數MessageQueue物件,並開啟一個會阻塞(不佔用資源)的死迴圈,只要MessageQueue中有msg,就會獲取該msg,並執行msg.target.dispatchMessage(msg)方法(msg.target即上一步引用的handler物件),此方法中呼叫了我們第二步建立handler子類物件時覆寫的handleMessage()方法,之後將該msg物件存入回收池;

Looper.loop()為什麼不會阻塞主執行緒

Android是基於事件驅動的,即所有Activity的生命週期都是通過Handler事件驅動的。loop方法中會呼叫MessageQueue的next方法獲取下一個message,當沒有訊息時,基於Linux pipe/epoll機制會阻塞在loop的queue.next()中的nativePollOnce()方法裡,並不會消耗CPU。

IdleHandler (閒時機制)

IdleHandler是一個回撥介面,可以通過MessageQueue的addIdleHandler新增實現類。當MessageQueue中的任務暫時處理完了(沒有新任務或者下一個任務延時在之後),這個時候會回撥這個介面,返回false,那麼就會移除它,返回true就會在下次message處理完了的時候繼續回撥。

同步屏障機制(sync barrier)

同步屏障可以通過MessageQueue.postSyncBarrier函式來設定。該方法傳送了一個沒有target的Message到Queue中,在next方法中獲取訊息時,如果發現沒有target的Message,則在一定的時間內跳過同步訊息,優先執行非同步訊息。再換句話說,同步屏障為Handler訊息機制增加了一種簡單的優先順序機制,非同步訊息的優先順序要高於同步訊息。在建立Handler時有一個async引數,傳true表示此handler傳送的時非同步訊息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪製優先執行。

View的繪製原理

View的繪製從ActivityThread類中Handler的處理RESUME_ACTIVITY事件開始,在執行performResumeActivity之後,建立Window以及DecorView並呼叫WindowManager的addView方法新增到螢幕上,addView又呼叫ViewRootImpl的setView方法,最終執行performTraversals方法,依次執行performMeasure,performLayout,performDraw。也就是view繪製的三大過程。
measure過程測量view的檢視大小,最終需要呼叫setMeasuredDimension方法設定測量的結果,如果是ViewGroup需要呼叫measureChildren或者measureChild方法進而計算自己的大小。
layout過程是擺放view的過程,View不需要實現,通常由ViewGroup實現,在實現onLayout時可以通過getMeasuredWidth等方法獲取measure過程測量的結果進行擺放。 draw過程先是繪製背景,其次呼叫onDraw()方法繪製view的內容,再然後呼叫dispatchDraw()呼叫子view的draw方法,最後繪製滾動條。ViewGroup預設不會執行onDraw方法,如果複寫了onDraw(Canvas)方法,需要呼叫 setWillNotDraw(false);清楚不需要繪製的標記。
Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二)

什麼是MeasureSpec

MeasureSpec代表一個32位int值,高兩位代表SpecMode(測量模式),低30位代表SpecSize(具體大小)。 SpecMode有三類:

  • UNSPECIFIED 表示父容器不對View有任何限制,一般用於系統內部,表示一種測量狀態;
  • EXACTLY 父容器已經檢測出view所需的精確大小,這時候view的最終大小SpecSize所指定的值,相當於match_parent或指定具體數值。
  • AT_MOST 父容器指定一個可用大小即SpecSize,view的大小不能大於這個值,具體多大要看view的具體實現,相當於wrap_content。

getWidth()方法和getMeasureWidth()區別呢?

首先getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的,而getWidth()方法中的值則是通過檢視右邊的座標減去左邊的座標計算出來的。

事件分發機制

圖解 Android 事件分發機制

requestLayout,invalidate,postInvalidate區別與聯絡

相同點:三個方法都有重新整理介面的效果。 不同點:invalidate和postInvalidate只會呼叫onDraw()方法;requestLayout則會重新呼叫onMeasure、onLayout、onDraw。

呼叫了invalidate方法後,會為該View新增一個標記位,同時不斷向父容器請求重新整理,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製需要重繪的檢視)。
呼叫requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會呼叫三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量onMeasure、佈局onLayout、繪製onDraw。
Android View 深度分析requestLayout、invalidate與postInvalidate

Binder機制,共享記憶體實現原理

為什麼使用Binder?

v2-30dce36be4e6617596b5fab96ef904c6_hd.jpg

概念 程式隔離 程式空間劃分:使用者空間(User Space)/核心空間(Kernel Space) 系統呼叫:使用者態與核心態

原理 跨程式通訊是需要核心空間做支援的。傳統的 IPC 機制如管道、Socket 都是核心的一部分,因此通過核心支援來實現程式間通訊自然是沒問題的。但是 Binder 並不是 Linux 系統核心的一部分,那怎麼辦呢?這就得益於 Linux 的動態核心可載入模組(Loadable Kernel Module,LKM)的機制;模組是具有獨立功能的程式,它可以被單獨編譯,但是不能獨立執行。它在執行時被連結到核心作為核心的一部分執行。這樣,Android 系統就可以通過動態新增一個核心模組執行在核心空間,使用者程式之間通過這個核心模組作為橋樑來實現通訊。

在 Android 系統中,這個執行在核心空間,負責各個使用者程式通過 Binder 實現通訊的核心模組就叫 Binder 驅動(Binder Dirver)。

那麼在 Android 系統中使用者程式之間是如何通過這個核心模組(Binder 驅動)來實現通訊的呢?難道是和前面說的傳統 IPC 機制一樣,先將資料從傳送方程式拷貝到核心快取區,然後再將資料從核心快取區拷貝到接收方程式,通過兩次拷貝來實現嗎?顯然不是,否則也不會有開篇所說的 Binder 在效能方面的優勢了。

這就不得不通道 Linux 下的另一個概念:記憶體對映

Binder IPC 機制中涉及到的記憶體對映通過 mmap() 來實現,mmap() 是作業系統中一種記憶體對映的方法。記憶體對映簡單的講就是將使用者空間的一塊記憶體區域對映到核心空間。對映關係建立後,使用者對這塊記憶體區域的修改可以直接反應到核心空間;反之核心空間對這段區域的修改也能直接反應到使用者空間。
一次完整的 Binder IPC 通訊過程通常是這樣:

  1. 首先 Binder 驅動在核心空間建立一個資料接收快取區;
  2. 接著在核心空間開闢一塊核心快取區,建立核心快取區和核心中資料接收快取區之間的對映關係,以及核心中資料接收快取區和接收程式使用者空間地址的對映關係;
  3. 傳送方程式通過系統呼叫 copyfromuser() 將資料 copy 到核心中的核心快取區,由於核心快取區和接收程式的使用者空間存在記憶體對映,因此也就相當於把資料傳送到了接收程式的使用者空間,這樣便完成了一次程式間的通訊。

Binder通訊模型 Binder是基於C/S架構的,其中定義了4個角色:Client、Server、Binder驅動和ServiceManager。

  • Binder驅動:類似網路通訊中的路由器,負責將Client的請求轉發到具體的Server中執行,並將Server返回的資料傳回給Client。
  • ServiceManager:類似網路通訊中的DNS伺服器,負責將Client請求的Binder描述符轉化為具體的Server地址,以便Binder驅動能夠轉發給具體的Server。Server如需提供Binder服務,需要向ServiceManager註冊。 具體的通訊過程
  1. Server向ServiceManager註冊。Server通過Binder驅動向ServiceManager註冊,宣告可以對外提供服務。ServiceManager中會保留一份對映表。
  2. Client向ServiceManager請求Server的Binder引用。Client想要請求Server的資料時,需要先通過Binder驅動向ServiceManager請求Server的Binder引用(代理物件)。
  3. 向具體的Server傳送請求。Client拿到這個Binder代理物件後,就可以通過Binder驅動和Server進行通訊了。
  4. Server返回結果。Server響應請求後,需要再次通過Binder驅動將結果返回給Client。

ServiceManager是一個單獨的程式,那麼Server與ServiceManager通訊是靠什麼呢? 當Android系統啟動後,會建立一個名稱為servicemanager的程式,這個程式通過一個約定的命令BINDERSETCONTEXT_MGR向Binder驅動註冊,申請成為為ServiceManager,Binder驅動會自動為ServiceManager建立一個Binder實體。並且這個Binder實體的引用在所有的Client中都為0,也就說各個Client通過這個0號引用就可以和ServiceManager進行通訊。Server通過0號引用向ServiceManager進行註冊,Client通過0號引用就可以獲取到要通訊的Server的Binder引用。 寫給 Android 應用工程師的 Binder 原理剖析
一篇文章瞭解相見恨晚的 Android Binder 程式間通訊機制

序列化的方式

Serializable是Java提供的一個序列化介面,是一個空介面,用於標示物件是否可以支援序列化,通過ObjectOutputStrean及ObjectInputStream實現序列化和反序列化的過程。注意可以為需要序列化的物件設定一個serialVersionUID,在反序列化的時候系統會檢測檔案中的serialVersionUID是否與當前類的值一致,如果不一致則說明類發生了修改,反序列化失敗。因此對於可能會修改的類最好指定serialVersionUID的值。
Parcelable是Android特有的一個實現序列化的介面,在Parcel內部包裝了可序列化的資料,可以在Binder中自由傳輸。序列化的功能由writeToParcel方法來完成,最終通過Parcel的一系列write方法完成。反序列化功能由CREAOR來完成,其內部標明瞭如何建立序列化物件和陣列,並通過Parcel的一系列read方法來完成反序列化的過程。

Fragment的懶載入實現

Fragment可見狀態改變時會被呼叫setUserVisibleHint()方法,可以通過複寫該方法實現Fragment的懶載入,但需要注意該方法可能在onVIewCreated之前呼叫,需要確保介面已經初始化完成的情況下再去載入資料,避免空指標。
Fragment的懶載入

RecyclerView與ListView(快取原理,區別聯絡,優缺點)

快取區別:

  1. 層級不同: ListView有兩級快取,在螢幕與非螢幕內。
    RecyclerView比ListView多兩級快取,支援多個離屏ItemView快取(匹配pos獲取目標位置的快取,如果匹配則無需再次bindView),支援開發者自定義快取處理邏輯,支援所有RecyclerView共用同一個RecyclerViewPool(快取池)。
  2. 快取不同: ListView快取View。
    RecyclerView快取RecyclerView.ViewHolder,抽象可理解為: View + ViewHolder(避免每次createView時呼叫findViewById) + flag(標識狀態);

優點 RecylerView提供了區域性重新整理的介面,通過區域性重新整理,就能避免呼叫許多無用的bindView。 RecyclerView的擴充套件性更強大(LayoutManager、ItemDecoration等)。

Android兩種虛擬機器區別與聯絡

Android中的Dalvik虛擬機器相較於Java虛擬機器針對手機的特點做了很多優化。
Dalvik基於暫存器,而JVM基於棧。在基於暫存器的虛擬機器裡,可以更為有效的減少冗餘指令的分發和減少記憶體的讀寫訪問。
Dalvik經過優化,允許在有限的記憶體中同時執行多個虛擬機器的例項,並且每一個 Dalvik應用作為一個獨立的Linux程式執行。
java虛擬機器執行的是java位元組碼。(java類會被編譯成一個或多個位元組碼.class檔案,打包到.jar檔案中,java虛擬機器從相應的.class檔案和.jar檔案中獲取相應的位元組碼) Dalvik執行的是自定義的.dex位元組碼格式。(java類被編譯成.class檔案後,會通過一個dx工具將所有的.class檔案轉換成一個.dex檔案,然後dalvik虛擬機器會從其中讀取指令和資料)
Android開發之淺談java虛擬機器和Dalvik虛擬機器的區別

adb常用命令列

檢視當前連線的裝置:adb devices 安裝應用:adb install -r <apk_path> -r表示覆蓋安裝 解除安裝apk:adb uninstall

ADB 用法大全

apk打包流程

  1. aapt工具打包資原始檔,生成R.java檔案
  2. aidl工具處理AIDL檔案,生成對應的.java檔案
  3. javac工具編譯Java檔案,生成對應的.class檔案
  4. 把.class檔案轉化成Davik VM支援的.dex檔案
  5. apkbuilder工具打包生成未簽名的.apk檔案
  6. jarsigner對未簽名.apk檔案進行簽名
  7. zipalign工具對簽名後的.apk檔案進行對齊處理

Android應用程式(APK)的編譯打包過程

apk安裝流程

  1. 複製APK到/data/app目錄下,解壓並掃描安裝包。
  2. 資源管理器解析APK裡的資原始檔。
  3. 解析AndroidManifest檔案,並在/data/data/目錄下建立對應的應用資料目錄。
  4. 然後對dex檔案進行優化,並儲存在dalvik-cache目錄下。
  5. 將AndroidManifest檔案解析出的四大元件資訊註冊到PackageManagerService中。
  6. 安裝完成後,傳送廣播。

apk瘦身

APK主要由以下幾部分組成:

  • META-INF/ :包含了簽名檔案CERT.SF、CERT.RSA,以及 manifest 檔案MANIFEST.MF。
  • assets/ : 存放資原始檔,這些資源不會被編譯成二進位制。
  • lib/ :包含了一些引用的第三方庫。
  • resources.arsc :包含res/values/中所有資源,例如strings,styles,以及其他未被包含在resources.arsc中的資源路徑資訊,例如layout 檔案、圖片等。
  • res/ :包含res中沒有被存放到resources.arsc的資源。
  • classes.dex :經過dx編譯能被android虛擬機器理解的Java原始碼檔案。
  • AndroidManifest.xml :清單檔案

其中佔據較大記憶體的是res資源、lib、class.dex,因此我們可以從下面的幾個方面下手:

  1. 程式碼方面可以通過程式碼混淆,這個一般都會去做。平時也可以刪除一些沒有使用類。
  2. 去除無用資源。使用lint工具來檢測沒有使用到的資源,或者在gradle中配置shrinkResources來刪除包括庫中所有的無用的資源,需要配合proguard壓縮程式碼使用。這裡需要注意專案中是否存在使用getIdentifier方式獲取資源,這種方式類似反射lint及shrinkResources無法檢測情況。如果存在這種方式,則需要配置一個keep.xml來記錄使用反射獲取的資源。壓縮程式碼和資源
  3. 去除無用國際化支援。對於一些第三庫來說(如support),因為國際化的問題,它們可能會支援了幾十種語言,但我們的應用可能只需要支援幾種語言,可以通過配置resConfigs提出不要的語言支援。
  4. 不同尺寸的圖片支援。通常情況下只需要一套xxhpi的圖片就可以支援大部分解析度的要求了,因此,我們只需要保留一套圖片。
  5. 圖片壓縮。 png壓縮或者使用webP圖片,完美支援需要Android版本4.2.1+
  6. 使用向量圖形。簡單的圖示可以使用向量圖片。

HTTP快取機制

圖片來自上述連結

快取的響應頭:

20171103144205821.png

Cache-control:標明快取的最大存活時常; Date:伺服器告訴客戶端,該資源的傳送時間; Expires:表示過期時間(該欄位是1.0的東西,當cache-control和該欄位同時存在的條件下,cache-control的優先順序更高); Last-Modified:伺服器告訴客戶端,資源的最後修改時間; 還有一個欄位,這個圖沒給出,就是E-Tag:當前資源在伺服器的唯一標識,可用於判斷資源的內容是否被修改了。 除以上響應頭欄位以外,還需瞭解兩個相關的Request請求頭:If-Modified-since、If-none-Match。這兩個欄位是和Last-Modified、E-Tag配合使用的。大致流程如下: 伺服器收到請求時,會在200 OK中回送該資源的Last-Modified和ETag頭(伺服器支援快取的情況下才會有這兩個頭哦),客戶端將該資源儲存在cache中,並記錄這兩個屬性。當客戶端需要傳送相同的請求時,根據Date + Cache-control來判斷是否快取過期,如果過期了,會在請求中攜帶If-Modified-Since和If-None-Match兩個頭。兩個頭的值分別是響應中Last-Modified和ETag頭的值。伺服器通過這兩個頭判斷本地資源未發生變化,客戶端不需要重新下載,返回304響應。

元件化

  • 在gradle.properties宣告一個變數用於控制是否是除錯模式,並在dependencies中根據是否是除錯模式依賴必要元件。
  • 通過resourcePrefix規範module中資源的命名字首。
  • 元件間通過ARouter完成介面跳轉和功能呼叫。

MVP

三方庫

okhttp原理

OkHttpClient通過newCall可以將一個Request構建成一個Call,Call表示準備被執行的請求。Call呼叫executed或enqueue會呼叫Dispatcher對應的方法在當前執行緒或者一步開始執行請求,經過RealInterceptorChain獲得最終結果,RealInterceptorChain是一個攔截器鏈,其中依次包含以下攔截器:

  • 自定義的攔截器
  • retryAndFollowUpInterceptor 請求失敗重試
  • BridgeInterceptor 為請求新增請求頭,為響應新增響應頭
  • CacheInterceptor 快取get請求
  • ConnectInterceptor 連線相關的攔截器,分配一個Connection和HttpCodec為最終的請求做準備
  • CallServerInterceptor 該攔截器就是利用HttpCodec完成最終請求的傳送

okhttp原始碼解析

Retrofit的實現與原理

Retrofit採用動態代理,建立宣告service介面的實現物件。當我們呼叫service的方法時候會執行InvocationHandler的invoke方法。在這方法中:首先,通過method把它轉換成ServiceMethod,該類是對宣告方法的解析,可以進一步將設定引數變成Request ;然後,通過serviceMethod, args獲取到okHttpCall 物件,實際呼叫okhttp的網路請求方法就在該類中,並且會使用serviceMethod中的responseConverter對ResponseBody轉化;最後,再把okHttpCall進一步封裝成宣告的返回物件(預設是ExecutorCallbackCall,將原本call的回撥轉發至UI執行緒)。

Retrofit2使用詳解及從原始碼中解析原理
Retrofit2 完全解析 探索與okhttp之間的關係

ARouter原理

可能是最詳細的ARouter原始碼分析

RxLifecycle原理

在Activity中,定義一個Observable(Subject),在不同的生命週期發射不同的事件; 通過compose操作符(內部實際上還是依賴takeUntil操作符),定義了上游資料,當其接收到Subject的特定事件時,取消訂閱; Subject的特定事件並非是ActivityEvent,而是簡單的boolean,它已經內部通過combineLast操作符進行了對應的轉化。

RxJava

Java

類的載入機制

程式在啟動的時候,並不會一次性載入程式所要用的所有class檔案,而是根據程式的需要,通過Java的類載入機制(ClassLoader)來動態載入某個class檔案到記憶體當中的,從而只有class檔案被載入到了記憶體之後,才能被其它class所引用。所以ClassLoader就是用來動態載入class檔案到記憶體當中用的。
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中準備、驗證、解析3個部分統稱為連線(Linking)。

  • 載入:查詢和匯入Class檔案;
  • 連結:把類的二進位制資料合併到JRE中;  (a) 驗證:檢查載入Class檔案資料的正確性;  (b) 準備:給類的靜態變數分配儲存空間;  (c) 解析:將符號引用轉成直接引用;
  • 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作

什麼時候發生類初始化

  1. 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
  5. 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項左後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼鎖對應的類沒有進行過初始化時。

雙親委派模型

Java中存在3種類載入器: (1) Bootstrap ClassLoader : 將存放於<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用 。 (2) Extension ClassLoader : 將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫載入。開發者可以直接使用擴充套件類載入器。 (3) Application ClassLoader : 負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可直接使用。 每個ClassLoader例項都有一個父類載入器的引用(不是繼承關係,是一個包含的關係),虛擬機器內建的類載入器(Bootstrap ClassLoader)本身沒有父類載入器,但是可以用做其他ClassLoader例項的父類載入器。 當一個ClassLoader 例項需要載入某個類時,它會試圖在親自搜尋這個類之前先把這個任務委託給它的父類載入器,這個過程是由上而下依次檢查的,首先由頂層的類載入器Bootstrap ClassLoader進行載入,如果沒有載入到,則把任務轉交給Extension ClassLoader載入,如果也沒有找到,則轉交給AppClassLoader進行載入,還是沒有的話,則交給委託的發起者,由它到指定的檔案系統或者網路等URL中進行載入類。還沒有找到的話,則會丟擲CLassNotFoundException異常。否則將這個類生成一個類的定義,並將它載入到記憶體中,最後返回這個類在記憶體中的Class例項物件。

為什麼使用雙親委託模型

JVM在判斷兩個class是否相同時,不僅要判斷兩個類名是否相同,還要判斷是否是同一個類載入器載入的。

  1. 避免重複載入,父類已經載入了,則子CLassLoader沒有必要再次載入。
  2. 考慮安全因素,假設自定義一個String類,除非改變JDK中CLassLoader的搜尋類的預設演算法,否則使用者自定義的CLassLoader如法載入一個自己寫的String類,因為String類在啟動時就被引導類載入器Bootstrap CLassLoader載入了。

HashMap原理,Hash衝突

在JDK1.6,JDK1.7中,HashMap採用陣列+連結串列實現,即使用連結串列處理衝突,同一hash值的連結串列都儲存在一個連結串列裡。但是當位於一個連結串列中的元素較多,即hash值相等的元素較多時,通過key值依次查詢的效率較低。而JDK1.8中,HashMap採用位陣列+連結串列+紅黑樹實現,當連結串列長度超過閾值(8)時,將連結串列轉換為紅黑樹,這樣大大減少了查詢時間。
當連結串列陣列的容量超過初始容量*載入因子(預設0.75)時,再雜湊將連結串列陣列擴大2倍,把原連結串列陣列的搬移到新的陣列中。為什麼需要使用載入因子?為什麼需要擴容呢?因為如果填充比很大,說明利用的空間很多,如果一直不進行擴容的話,連結串列就會越來越長,這樣查詢的效率很低,擴容之後,將原來連結串列陣列的每一個連結串列分成奇偶兩個子連結串列分別掛在新連結串列陣列的雜湊位置,這樣就減少了每個連結串列的長度,增加查詢效率。
HashMap是非執行緒安全的,HashTable、ConcurrentHashMap是執行緒安全的。 HashMap的鍵和值都允許有null存在,而HashTable、ConcurrentHashMap則都不行。 因為執行緒安全、雜湊效率的問題,HashMap效率比HashTable、ConcurrentHashMap的都要高。 HashTable裡使用的是synchronized關鍵字,這其實是對物件加鎖,鎖住的都是物件整體,當Hashtable的大小增加到一定的時候,效能會急劇下降,因為迭代時需要被鎖定很長的時間。 ConcurrentHashMap引入了分割(Segment),可以理解為把一個大的Map拆分成N個小的HashTable,在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪個Segment,如果檢視Segment的put操作,我們會發現內部使用的同步機制是基於lock操作的,這樣就可以對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼做的),相對於HashTable提高了多執行緒環境下的效能,因此HashTable已經被淘汰了。

Java中HashMap底層實現原理(JDK1.8)原始碼分析

什麼是Fail-Fast機制

Fail-Fast是Java集合的一種錯誤檢測機制。當遍歷集合的同時修改集合或者多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制,記住是有可能,而不是一定。其實就是丟擲ConcurrentModificationException 異常。
集合的迭代器在呼叫next()、remove()方法時都會呼叫checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則丟擲ConcurrentModificationException 異常,從而產生fail-fast機制。modCount是在每次改變集合數量時會改變的值。

Java提高篇(三四)-----fail-fast機制

Java泛型

Java泛型詳解

Java多執行緒中呼叫wait() 和 sleep()方法有什麼不同?

Java程式中wait 和 sleep都會造成某種形式的暫停,它們可以滿足不同的需要。wait()方法用於執行緒間通訊,如果等待條件為真且其它執行緒被喚醒時它會釋放鎖,而 sleep()方法僅僅釋放CPU資源或者讓當前執行緒停止執行一段時間,但不會釋放鎖。

volatile的作用和原理

Java程式碼在編譯後會變成Java位元組碼,位元組碼被類載入器載入到JVM裡,JVM執行位元組碼,最終需要轉化為彙編指令在CPU上執行。 volatile是輕量級的synchronized(volatile不會引起執行緒上下文的切換和排程),它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。
由於記憶體訪問速度遠不及CPU處理速度,為了提高處理速度,處理器不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取後在進行操作,但操作完不知道何時會寫到記憶體。普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。如果對宣告瞭volatile的變數進行寫操作,JVM就會想處理器傳送一條Lock字首的指令,表示將當前處理器快取行的資料寫回到系統記憶體。

一個int變數,用volatile修飾,多執行緒去操作++,執行緒安全嗎?

不安全。volatile只能保證可見性,並不能保證原子性。i++實際上會被分成多步完成:1)獲取i的值;2)執行i+1;3)將結果賦值給i。volatile只能保證這3步不被重排序,多執行緒情況下,可能兩個執行緒同時獲取i,執行i+1,然後都賦值結果2,實際上應該進行兩次+1操作。

那如何才能保證i++執行緒安全?

可以使用java.util.concurrent.atomic包下的原子類,如AtomicInteger。
其實現原理是採用CAS自旋操作更新值。CAS即compare and swap的縮寫,中文翻譯成比較並交換。CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。自旋就是不斷嘗試CAS操作直到成功為止。

CAS實現原子操作會出現什麼問題?

  • ABA問題。因為CAS需要在操作之的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成,有變成A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但實際上發生了變化。ABA問題可以通過新增版本號來解決。Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。
  • 迴圈時間長開銷大。pause指令優化。
  • 只能保證一個共享變數的原子操作。可以合併成一個物件進行CAS操作。

synchronized

Java中每個物件都可以作為鎖:

  • 對於普通同步方法,鎖是當前例項物件;
  • 對於靜態同步方法,鎖是當前類的Class物件;
  • 對於同步方法塊,鎖是括號中配置的物件;

當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或丟擲異常時必須釋放鎖。synchronized用的鎖是存在Java物件頭裡的MarkWord,通常是32bit或者64bit,其中最後2bit表示鎖標誌位

java物件結構

Java SE1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了偏向鎖和輕量級鎖,在1.6中鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾種狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級。

偏向鎖

偏向鎖獲取過程:

  1. 訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01,確認為可偏向狀態。
  2. 如果為可偏向狀態,則測試執行緒ID是否指向當前執行緒,如果是,進入步驟5,否則進入步驟3。
  3. 如果執行緒ID並未指向當前執行緒,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中執行緒ID設定為當前執行緒ID,然後執行5;如果競爭失敗,執行4。
  4. 如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。(撤銷偏向鎖的時候會導致stop the word)
  5. 執行同步程式碼。

輕量級鎖

  1. 在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。
  2. 拷貝物件頭中的Mark Word複製到鎖記錄中;
  3. 拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock record裡的owner指標指向object mark word。如果更新成功,則執行步驟4,否則執行步驟5。
  4. 如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,即表示此物件處於輕量級鎖定狀態。
  5. 如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行。否則說明多個執行緒競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。 而當前執行緒便嘗試使用自旋來獲取鎖,自旋就是為了不讓執行緒阻塞,而採用迴圈去獲取鎖的過程。
    自旋 如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。
    但是執行緒自旋是需要消耗cup的,說白了就是讓cup在做無用功,如果一直獲取不到鎖,那執行緒也不能一直佔用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。
    如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態。

執行緒池

好處:1)降低資源消耗;2)提高相應速度;3)提高執行緒的可管理性。 執行緒池的實現原理:

  • 當提交一個新任務到執行緒池時,判斷核心執行緒池裡的執行緒是否都在執行。如果不是,則建立一個新的執行緒執行任務。如果核心執行緒池的執行緒都在執行任務,則進入下個流程。
  • 判斷工作佇列是否已滿。如果未滿,則將新提交的任務儲存在這個工作佇列裡。如果工作佇列滿了,則進入下個流程。
  • 判斷執行緒池是否都處於工作狀態。如果沒有,則建立一個新的工作執行緒來執行任務。如果滿了,則交給飽和策略來處理這個任務。

假如有n個網路執行緒,你需要當n個網路執行緒完成之後,再去做資料處理,你會怎麼解決?

這題考的其實是多執行緒同步的問題。這種情況可以可以使用thread.join();join方法會阻塞直到thread執行緒終止才返回。更復雜一點的情況也可以使用CountDownLatch,CountDownLatch的構造接收一個int引數作為計數器,每次呼叫countDown方法計數器減一。做資料處理的執行緒呼叫await方法阻塞直到計數器為0時。

Java中interrupted 和 isInterruptedd方法的區別?

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

懶漢式單例的同步問題

同步的懶載入雖然是執行緒安全的,但是導致效能開銷。因此產生了雙重檢查鎖定。但雙重檢查鎖定存在隱藏的問題。instance = new Instance()實際上會分為三步操作:1)分配物件的記憶體空間;2)初始化物件;3)設定instance指向剛分配的記憶體地址;由於指令重排序,2和3的順序並不確定。在多執行緒的情況下,第一個執行緒執行了1,3,此時第二個執行緒判斷instance不為null,但實際上操作2還沒有執行,第二個執行緒就會獲得一個還未初始化的物件,直接使用就會造成空指標。
解決方案是用volatile修飾instance,在JDK 1.5加強了volatile的語意之後,用volatile修飾instance就阻止了2和3的重排序,進而避免上述情況的發生。
另一種方式則是使用靜態內部類:

public class Singleton {
    private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}
複製程式碼

其原理是利用類初始化時會加上初始化鎖確保類物件的唯一性。

什麼是ThreadLocal

ThreadLocal即執行緒變數,它為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。從執行緒的角度看,目標變數就象是執行緒的本地變數,這也是類名中“Local”所要表達的意思。ThreadLocal的實現是以ThreadLocal物件為鍵。任意物件為值得儲存結構。這個結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal物件查詢到繫結在這個執行緒上的一個值。

什麼是資料競爭

資料競爭的定義:在一個執行緒寫一個變數,在另一個執行緒讀同一個變數,而且寫和讀沒有通過同步來排序。

Java記憶體模型(Java Memory Model JMM)

JM遮蔽各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。
執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體,本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是一個抽象概念,它涵蓋了快取、寫快取區、暫存器以及其他的硬體和編譯器優化。 在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。在多執行緒中重排序會對程式的執行結果有影響。
JSR-133記憶體模型採用happens-before的概念來闡述操作之間的記憶體可見性。happens-before會限制重排序以滿足規則。 主要的happens-before規則有如下:

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before與鎖隨後對這個鎖的加鎖。
  • volatile變數規則:對一個volatile域的寫,happens-before與任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

Java記憶體區域

  • 程式計數器:當前執行緒鎖執行的位元組碼的行號指示器,用於執行緒切換恢復,是執行緒私有的;
  • Java虛擬機器棧(棧):虛擬機器棧也是執行緒私有的。每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
  • 本地方法棧:與虛擬機器棧類似,服務於Native方法。
  • Java堆:堆是被所有執行緒共享的一塊記憶體,用於存放物件例項。是垃圾收集器管理的主要區域,也被稱作GC堆。
  • 方法區:與Java堆一樣,是執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊、常量、靜態常量、即時編譯器編譯後的程式碼等資料。
  • 執行時常量池:是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。

判斷物件是否需要回收的方法

  • 引用計數演算法。實現簡單,判定效率高,但不能解決迴圈引用問題,同時計數器的增加和減少帶來額外開銷,JDK1.1以後廢棄了。
  • 可達性分析演算法/根搜尋演算法 。根搜尋演算法是通過一些“GC Roots”物件作為起點,從這些節點開始往下搜尋,搜尋通過的路徑成為引用鏈(Reference Chain),當一個物件沒有被GC Roots 的引用鏈連線的時候,說明這個物件是不可用的。 Java中可作為“GC Root”的物件包括:虛擬機器棧(本地變數表)中引用的物件;方法區中類靜態屬性和常量引用的物件。本地方法棧中引用的物件。

引用型別

  • 強引用:預設的引用方式,不會被垃圾回收,JVM寧願丟擲OutOfMemory錯誤也不會回收這種物件。
  • 軟引用(SoftReference):如果一個物件只被軟引用指向,只有記憶體空間不足夠時,垃圾回收器才會回收它;
  • 弱引用(WeakReference):如果一個物件只被弱引用指向,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收該物件。
  • 虛引用(PhantomReference):虛引用和前面的軟引用、弱引用不同,它並不影響物件的生命週期。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。虛引用通常和ReferenceQueue配合使用。
    ReferenceQueue 作為一個Java物件,Reference物件除了具有儲存引用的特殊性之外,也具有Java物件的一般性。所以,當物件被回收之後,雖然這個Reference物件的get()方法返回null,但這個SoftReference物件已經不再具有存在的價值,需要一個適當的清除機制,避免大量Reference物件帶來的記憶體洩漏。 在java.lang.ref包裡還提供了ReferenceQueue。我們建立Reference物件時使用兩個引數的構造傳入ReferenceQueue,當Reference所引用的物件被垃圾收集器回收的同時,Reference物件被列入ReferenceQueue。也就是說,ReferenceQueue中儲存的物件是Reference物件,而且是已經失去了它所軟引用的物件的Reference物件。另外從ReferenceQueue這個名字也可以看出,它是一個佇列,當我們呼叫它的poll()方法的時候,如果這個佇列中不是空佇列,那麼將返回佇列前面的那個Reference物件。於是我們可以在適當的時候把這些失去所軟引用的物件的SoftReference物件清除掉。

垃圾收集演算法

  1. 標記-清楚演算法(Mark-Sweep) 在標記階段,確定所有要回收的物件,並做標記。清除階段緊隨標記階段,將標記階段確定不可用的物件清除。標記—清除演算法是基礎的收集演算法,有兩個不足:1)標記和清除階段的效率不高;2)清除後回產生大量的不連續空間,這樣當程式需要分配大記憶體物件時,可能無法找到足夠的連續空間。
  2. 複製演算法(Copying) 複製演算法是把記憶體分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的物件複製到另一塊上,然後把這塊記憶體整個清理掉。複製演算法實現簡單,執行效率高,但是由於每次只能使用其中的一半,造成記憶體的利用率不高。現在的JVM 用複製方法收集新生代,由於新生代中大部分物件(98%)都是朝生夕死的,所以會分成1塊大記憶體Eden和兩塊小記憶體Survivor(大概是8:1:1),每次使用1塊大記憶體和1塊小記憶體,當回收時將2塊記憶體中存活的物件賦值到另一塊小記憶體中,然後清理剩下的。
  3. 標記—整理演算法(Mark-Compact) 標記—整理演算法和複製演算法一樣,但是標記—整理演算法不是把存活物件複製到另一塊記憶體,而是把存活物件往記憶體的一端移動,然後直接回收邊界以外的記憶體。標記—整理演算法提高了記憶體的利用率,並且它適合在收集物件存活時間較長的老年代。
  4. 分代收集(Generational Collection) 分代收集是根據物件的存活時間把記憶體分為新生代和老年代,根據各代物件的存活特點,每個代採用不同的垃圾回收演算法。新生代採用複製演算法,老年代採用標記—整理演算法。

記憶體分配策略

  • 物件優先在Eden分配。
  • 大物件直接進入老年代。 大物件是指需要大量連續記憶體空間的Java物件,最典型的就是那種很長的字串以及陣列。
  • 長期存活的物件進入老年代。存活過一次新生代的GC,Age+1,當達到一定程度(預設15)進入老年代。
  • 動態物件年齡判定。如果在Survivor空間中相同Age所有物件大小的總和大於Survivor空間一半。那麼Age大於等於該Age的物件就可以直接進入老年代。
  • 空間分配擔保。 在發生新生代GC之前,會檢查老年代的剩餘空間是否大於新生代所有物件的總和。如果大於則是安全的,如果不大於有風險。

相關文章