Android面試準備(中高級)

Android

Activity生命週期

這裏寫圖片描述

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

onStart()是activity界面被顯示出來的時候執行的,但不能與它交互;  onResume()是當該activity與用戶能進行交互時被執行,用戶能夠得到activity的焦點,可以與用戶交互。html

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啓動流程java

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

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。git

IdleHandler (閒時機制)

IdleHandler是一個回調接口,能夠經過MessageQueue的addIdleHandler添加實現類。當MessageQueue中的任務暫時處理完了(沒有新任務或者下一個任務延時在以後),這個時候會回調這個接口,返回false,那麼就會移除它,返回true就會在下次message處理完了的時候繼續回調。github

同步屏障機制(sync barrier)

同步屏障能夠經過MessageQueue.postSyncBarrier函數來設置。該方法發送了一個沒有target的Message到Queue中,在next方法中獲取消息時,若是發現沒有target的Message,則在必定的時間內跳過同步消息,優先執行異步消息。再換句話說,同步屏障爲Handler消息機制增長了一種簡單的優先級機制,異步消息的優先級要高於同步消息。在建立Handler時有一個async參數,傳true表示此handler發送的時異步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪製優先執行。web

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有三類:json

  • 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以前,會檢查老年代的剩餘空間是否大於新生代全部對象的總和。若是大於則是安全的,若是不大於有風險。
相關文章
相關標籤/搜索