面試題整理

數(shù)組實現(xiàn)隊列

public class ArrayQueue {

    private String [] items;  //定義數(shù)組
    private int n = 0;           //數(shù)組大小
    private int head = 0;     //表示隊頭下標
    private int tail = 0;        //表示隊尾下標

    //申請一個大小為capacity的數(shù)組
    public ArrayQueue(int capacity) {
        this.n = capacity;
        this.items = new String[capacity];  //初始化數(shù)組
    }

    public boolean enQueue(String item) {
        if (tail == n) {  //隊列已經(jīng)滿了
            return false;
        }
        items[tail] = item;
        tail++;
        return true;
    }

    public String deQueue() {
        if (head == tail) {   //隊列為空
            return null;
        }
        String item = items[head];
        head++;
        return item;
    }

}
public class ArrayStack {
    String[] arrays;
    int i = 0;

    public ArrayStack(int n) {
        arrays = new String[n];
    }

    public void push(String item) {
        if (i > arrays.length - 1) {
            return;
        }
        arrays[i] = item;
        i++;
    }

    public String pop() {
        if (i > 0) {
            i--;
        }
        String temp = arrays[i];
        arrays[i] = null;
        return temp;
    }

}

java軟引用與弱引用區(qū)別

參考了一些資料

  • 強引用
    • 我們平常典型編碼Object obj = new Object()中的obj就是強引用。通過關鍵字new創(chuàng)建的對象所關聯(lián)的引用就是強引用。
    • 當JVM內(nèi)存空間不足,JVM寧愿拋出OutOfMemoryError運行時錯誤(OOM),使程序異常終止,也不會靠隨意回收具有強引用的“存活”對象來解決內(nèi)存不足的問題。
    • 對于一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應強引用賦值為 null,就是可以被垃圾收集的了,具體回收時機還是要看垃圾收集策略。
  • 軟引用(SoftReference)
    • 只有當 JVM 認為內(nèi)存不足時,才會去試圖回收軟引用指向的對象,即JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。
    • 當JVM認為內(nèi)存充足的時候,不會去回收軟引用
    • 軟引用什么時候會被回收
  • 弱引用(WeakReference)
    • 當發(fā)生GC時,如果掃描到一個對象只有弱引用,不管當前內(nèi)存是否足夠,都會對它進行回收。
        String str = new String("abc");
        //創(chuàng)建一個弱引用,讓這個弱引用引用到str字符串
        WeakReference weakReference = new WeakReference(str);
        //切斷str引用和 str 字符串之間的引用,此時str 只有一個弱引用weakReference指向它
        str = null;
        // 沒有進行垃圾回收,我們還可以通過弱引用來訪問他
        System.out.println(weakReference.get()); // abc
        //強制進行垃圾回收
        System.gc();
        //再次取出弱引用所引用的對象
        System.out.println(weakReference.get()); // null
    
  • 虛引用(PhantomReference)
    • 垃圾回收時回收,無法通過引用取到對象值
    • 虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數(shù)據(jù)為null,因此也被成為幽靈引用。
    • 虛引用主要用于檢測對象是否已經(jīng)從內(nèi)存中刪除。程序可以通過檢查與虛引用關聯(lián)的引用隊列中是否已經(jīng)包含了該虛引用,從而了解虛引用所引用對象是否即將被回收。
    • 虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯(lián)合使用。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之 關聯(lián)的引用隊列中。

final變量用反射修改

  • 當final修飾的成員變量在定義的時候就初始化了值,那么java反射機制就已經(jīng)不能動態(tài)修改它的值了。

    • 原因:編譯期間final類型的數(shù)據(jù)自動被優(yōu)化了,即:所有用到該變量的地方都被替換成了常量。
    public final String name = "abc";
    public String getName() {
        return "abc";
    }
    
  • 當final修飾的成員變量在定義的時候并沒有初始化值的話,那么就還能通過java反射機制來動態(tài)修改它的值。

  1. HashMap的內(nèi)部結構,給定一個key,如何找到對應的value,使用equal
  2. volatile
  3. Java線程池有什么作用
  4. Java動態(tài)代理
  5. handler機制
  6. android跨進程通信的方式
  7. 自定義控件方式
  8. Canvas繪制過什么 手寫功能

斷點續(xù)傳的實現(xiàn)

  • 從字面上理解,所謂斷點續(xù)傳就是從停止的地方重新下載。 斷點:線程停止的位置。 續(xù)傳:從停止的位置重新下載。
  • 用代碼解析就是:斷點: 當前線程已經(jīng)下載完成的數(shù)據(jù)長度。續(xù)傳: 向服務器請求上次線程停止位置之后的數(shù)據(jù)。原理知道了,功能實現(xiàn)起來也簡單。每當線程停止時就把已下載的數(shù)據(jù)長度寫入記錄文件,當重新下載時,從記錄文件讀取已經(jīng)下載了的長度。而這個長度就是所需要的斷點。
  • 總結來說就是下載過程中要使用數(shù)據(jù)庫實時存儲到底存儲到文件的哪個位置了,這樣點擊開始繼續(xù)傳遞時,才能通過HTTP的GET請求中的 urlConnection.setRequestProperty("Range","bytes=" + start + "-" + info.getLength()); 方法可以告訴服務器,數(shù)據(jù)從哪里開始,到哪里結束。同時在本地的文件寫入時,RandomAccessFile的seek()方法也支持在文件中的任意位置進行寫入操作。同時通過廣播將子線程的進度告訴Activity的ProcessBar。

app 啟動速度的優(yōu)化

影響啟動的因素
  • 高耗時任務
  • 復雜的View層級
  • 類過于復雜
  • 主題及Activity配置
啟動耗時檢測
  • 查看Logcat
  • adb shell
  • 代碼打點(函數(shù)插樁)
  • 啟動速度分析工具 — TraceView
  • 啟動速度分析工具 — Systrace
解決方案
  • 異步初始化 : 充分利用CPU多核,自動梳理任務順序。
  • 延遲初始化 :第三方庫懶加載,按需初始化; 利用IdleHandler特性,在CPU空閑時執(zhí)行,對延遲任務進行分批初始化
  • Multidex預加載優(yōu)化 啟動時單獨開一個進程去異步進行Multidex的第一次加載,即Dex提取和Dexopt操作。
  • 復雜的View層級 減少層級嵌套
  • 主題切換:使用Activity的windowBackground主題屬性預先設置一個啟動圖片
  • ?;?/li>
  • WebView啟動優(yōu)化

線程優(yōu)化

線程調(diào)度模型
  • 分時調(diào)度模型 輪流獲取、均分CPU
  • 搶占式調(diào)度模型 優(yōu)先級高的獲取
如何干預線程調(diào)度?
  • 設置線程優(yōu)先級。
Android異步方式
  • Thread 直接創(chuàng)建,缺點很多,比如說不容易被復用,導致頻繁創(chuàng)建和銷毀線程的開銷大,不建議使用
  • HandlerThread 本質(zhì)上也是一個 Thread,自帶了消息循環(huán),串行執(zhí)行
  • IntentService 是 Service 組件的子類,它的內(nèi)部有一個 HandlerThread,所以它具備了 HandlerThread 的特性。優(yōu)點:使用了 Service,會提高應用的優(yōu)先級;異步,不占用主線程
  • AsyncTask 內(nèi)部實現(xiàn)使用了線程池
  • 線程池 易復用,減少頻繁創(chuàng)建、銷毀的時間;功能強大,如定時、任務隊列、并發(fā)數(shù)控制等
  • RxJava 功能強大,提供了不同的線程池:IO Computation
Android線程優(yōu)化實戰(zhàn)
  • 嚴禁使用new Thread方式。
  • 提供基礎線程池供各個業(yè)務線使用,避免各個業(yè)務線各自維護一套線程池,導致線程數(shù)過多。
  • 根據(jù)任務類型選擇合適的異步方式:優(yōu)先級低,長時間執(zhí)行,HandlerThread;定時執(zhí)行耗時任務,線程池。
  • 創(chuàng)建線程必須命名,以方便定位線程歸屬,在運行期Thread.currentThread().setName修改名字。
  • 關鍵異步任務監(jiān)控,注意異步不等于不耗時,建議使用AOP的方式來做監(jiān)控。
  • 重視優(yōu)先級設置(根據(jù)任務具體情況),Process.setThreadPriority();可以設置多次。
線程收斂優(yōu)雅實踐初步
  • 基礎庫內(nèi)部暴露API:setExecutor。
  • 初始化的時候注入統(tǒng)一的線程庫。

布局優(yōu)化

  • 減少層級
    • 合理使用RelativeLayout和LinearLayout,RelativeLayout會對子View做兩次測量。但如果在LinearLayout中有weight屬性,也需要進行兩次測量,因為沒有更多的依賴關系,所以仍然會比RelativeLayout的效率高。
    • 合理使用Merge
  • 提供顯示速度
    • ViewStub
  • 布局復用
    • 通過標簽來實現(xiàn)
  • 其他
    • 使用標簽加載一些不常用的布局。
    • 盡可能少用wrap_content,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時,不用wrap_content。
    • 使用TextView替換RL、LL。
    • 使用低端機進行優(yōu)化,以發(fā)現(xiàn)性能瓶頸。
    • 使用TextView的行間距替換多行文本:lineSpacingExtra/lineSpacingMultiplier。
    • 使用Spannable/Html.fromHtml替換多種不同規(guī)格文字。
    • 盡可能使用LinearLayout自帶的分割線。
    • 使用Space添加間距。
    • 多利用lint + alibaba規(guī)約修復問題點。
    • 嵌套層級過多可以考慮使用約束布局

減少過度繪制

導致過度繪制的主要原因是:
  • XML布局:控件有重疊且都有設置背景。
  • View自繪:View.OnDraw里面同一個區(qū)域被繪制多次。
如何避免過度繪制
  • 布局上的優(yōu)化

    • 移除XML中非必需的背景
    • 有選擇性地移除窗口背景:getWindow().setBackgroundDrawable(null)
    • 按需顯示占位背景圖片
  • 自定義View優(yōu)化

    • 通過canvas.clipRect()來幫助系統(tǒng)識別那些可見的區(qū)域
    • 繪制一個單元之前,首先判斷該單元的區(qū)域是否在Canvas的剪切域內(nèi)。若不在,直接返回

webview的優(yōu)化

  • WebView首次創(chuàng)建比較耗時,需要預先創(chuàng)建WebView提前將其內(nèi)核初始化。
  • 使用WebView緩存池,用到WebView的時候都從緩存池中拿,注意內(nèi)存泄漏問題。
  • 本地離線包,即預置靜態(tài)頁面資源
  • 本地接口請求,緩存
  • DNS解析優(yōu)化
  • 另外開啟webView進程
  • 首屏靜態(tài)html
  • 骨架屏

fresco加載圖片原理 優(yōu)勢是什么

緩存怎么處理的

a、根據(jù)Uri在已解碼的(Bitmap緩存)內(nèi)存緩存中查找,找到了則返回Bitmap對象;如果沒找到,則開啟后臺線程開始后續(xù)的工作。
b、根據(jù)Uri在未解碼的內(nèi)存緩存中查找,若找到了則解碼,然后緩存到已解碼的內(nèi)存緩存中,并且返回Bitmap對象。
d、如果在未解碼的內(nèi)存緩存中沒找到,則根據(jù)Uri在磁盤緩存中查找,若找到了則讀取數(shù)據(jù)(byte數(shù)組),并緩存到未解碼的內(nèi)存緩存中,解碼、然后緩存到已解碼的內(nèi)存緩存中,并且返回Bitmap對象。
e、如果在磁盤緩存中沒找到,則從網(wǎng)絡或者本地加載數(shù)據(jù)。加載完成后,依次緩存到磁盤緩存、未解碼的內(nèi)存緩存中。解碼、然后緩存到已解碼的內(nèi)存緩存中,并且返回Bitmap對象。

bitmap 內(nèi)存分配
  • 在4.x及以下的系統(tǒng)上,F(xiàn)resco的bitmap decode會把bitmap的pixel data(像素數(shù)據(jù))放到一個“特殊的內(nèi)存區(qū)域’ ”,這個特殊的內(nèi)存區(qū)域其實就是ashmem
  • 為什么在4.x及以下系統(tǒng)需要這樣做?原因是如果Bitmap數(shù)量很多時會占用大量的內(nèi)存(這里內(nèi)存特指Java Heap),必然就會更加頻繁的觸發(fā)虛擬機進行GC,GC 會導致stop the world
  • 怎么實現(xiàn)的?
    • BitmapFactory.Options 的參數(shù) inPurgeable能使bitmap的內(nèi)存分配到ashmem上。inPurgeable的作用是,在KITKAT及以下,使用該參數(shù)的話,當系統(tǒng)需要回收內(nèi)存的時候,bitmap的pixels可以被清除。好在的是,當pixels需要被重新訪問的時候(例如bitmap draw或者調(diào)用getPixels()的時候),它們又可以重新被decode出來
    • isPurgeable為true的圖片,內(nèi)存分配的時機和流程,只有當bitmap進行draw或者getPixels(這個大抵一致)的時候,bitmap 的pixels才進行實際的內(nèi)存分配
    • bitmap draw的整個流程看,操作都處于UI線程,由于存在耗時的操作,會導致drop frames
  • ART模式下,BitmapOptions的inBitmap和inTempStorage去優(yōu)化內(nèi)存使用。inBitmap是由上層的BitmapPool 去分配內(nèi)存,inTempStorage是由 SynchronizedPool分配內(nèi)存,都是用緩存池的方式分配和回收內(nèi)存,做到對這些區(qū)域的內(nèi)存可管理,減少各個不同地方自行分配內(nèi)存 。
  • 為啥使用 Ashmem
    • Java Heap:大小受系統(tǒng)限制,內(nèi)存自動回收。
    • Native Heap:大小不受系統(tǒng)限制,僅受物理內(nèi)存限制,內(nèi)存需要手動釋放。
    • Ashmem:大小不受系統(tǒng)限制,僅受物理內(nèi)存限制,系統(tǒng)在需要的時候可以自動回收,也可以手動阻止回收。

多進程通訊的方式

  • bundle 四大組件可以通過這種方式傳遞數(shù)據(jù)
  • 文件共享 簡單的文件讀寫,序列化文件,sp
  • AIDL 客戶端,服務端,接口
  • Messenger 底層是binder,客戶端和服務端兩端,主要是對消息的處理,串行的方式處理消息
  • ContentProvider 底層是binder,和數(shù)據(jù)庫操作息息相關
  • Socket serverSocket

Handler 如何規(guī)避內(nèi)存泄漏

  • 原因:非靜態(tài)內(nèi)部類會隱性地持有外部類的引用
  • 監(jiān)控內(nèi)存泄漏
    • LeakCanary
    • 原理,利用了Java的WeakReference和ReferenceQueue,通過將Activity包裝到WeakReference中,被WeakReference包裝過的Activity對象如果被回收,該WeakReference引用會被放到ReferenceQueue中,通過監(jiān)測ReferenceQueue里面的內(nèi)容就能檢查到Activity是否能夠被回收
  • 解決方法
    • 在onDestory中移除 handler.removeCallbacksAndMessages(null)
    • 聲明一個靜態(tài)的Handler內(nèi)部類,并持有外部類的弱引用

data binding

  • 原理
    • 將xml文件拆分成兩部分,一部分是數(shù)據(jù)文件,一部分是布局文件,在布局文件中,為每個View設置一個tag,數(shù)據(jù)文件中,標示了相對應的tag的View所需要使用的數(shù)據(jù)
    • 根據(jù)上面的兩個xml在編譯時生成 ActivityDataBindBinding和BR類
    • 在 ActivityDataBindBinding 當中,會更加tag對每一個View進行賦值
    • 根據(jù)上面的xml對數(shù)據(jù)進行綁定操作 (executeBindings方法)
  • 數(shù)據(jù)的實時刷新
    • 在進行set的方法時候,最終會調(diào)用到 executeBindings方法

Android中動畫的原理

  • 幀動畫

    • animation-list --> AnimationDrawable
    • 容易oom
  • view動畫

    • a.getTransformation方法會返回一個布爾值,表示是否需要繼續(xù)動畫,一旦為true,則父控件會重新計算需要動畫需要刷新的區(qū)域并且更新該區(qū)域。在動畫結束之前會不斷重繪,從而形成連續(xù)的動畫效果。
    • getTransformation 在這個方法中,根據(jù)流逝的時間計算當前動畫時間百分比,然后通過插值器(Interpolator)重新計算這個百分比,并且以此來計算當前動畫屬性值
  • 屬性動畫

    • 屬性動畫是通過VSYNC信號來持續(xù)改變屬性值進行動畫的
    • 相比于補間動畫,屬性動畫重繪明顯會少很多
    • 流程 ObjectAnimator.ofFloat(button,"translationX",0,200);
      • 首先會把我們傳入的View保存起來
      • 然后會初始化一個Holder對象,這個Holder對象是用來干啥的呢?我們傳入了一個translationX值,Holder對象中提供一個方法,把translationX拼接成一個set方法,setTranslationX,然后通過反射找到View中的此方法。當數(shù)值計算出來之后,執(zhí)行獲取的這個方法。
      • KeyframeSet是一個關鍵幀的集合,最后我們傳入0-200就是關鍵幀
      • 插值器,用來計算某一時間中動畫長度播放的百分比
      • 估值器,根據(jù)插值器計算的百分比,來計算某個時間,動畫所要更新的值。然后交給Holder執(zhí)行動畫
      • 屬性動畫會監(jiān)聽系統(tǒng)發(fā)出的VSYNC信號,每收到一次信號就執(zhí)行一次
  • 插值器和估值器

    • 插值器:設置 屬性值 從初始值過渡到結束值 的變化規(guī)律
    • 估值器:設置 屬性值 從初始值過渡到結束值 的變化具體數(shù)值
    • 插值器只是根據(jù)時間百分比計算出一個屬性值百分比,而把屬性值百分比轉換為真正屬性值則交給估值器來做。例如:插值器返回的是一個百分比,估值器就可以將這個百分比轉換成色值(0-255)或者一個坐標

字符串hash函數(shù)

  • 源碼
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
  • 為什么是31
    • 31是一個不大不小的質(zhì)數(shù),是作為 hashCode 乘子的優(yōu)選質(zhì)數(shù)之一,為啥選擇質(zhì)數(shù),質(zhì)數(shù)可以降低哈希算法的沖突率
    • 31可以被 JVM 優(yōu)化,31 * i = (i << 5) - i
    • Effective Java : 選擇數(shù)字31是因為它是一個奇質(zhì)數(shù),如果選擇一個偶數(shù)會在乘法運算中產(chǎn)生溢出,導致數(shù)值信息丟失,因為乘二相當于移位運算。選擇質(zhì)數(shù)的優(yōu)勢并不是特別的明顯,但這是一個傳統(tǒng)。同時,數(shù)字31有一個很好的特性,即乘法運算可以被移位和減法運算取代,來獲取更好的性能:31 * i == (i << 5) - i,現(xiàn)代的 Java 虛擬機可以自動的完成這個優(yōu)化。

ThreadLocal

  • ThreadLocal是并發(fā)場景下用來解決變量共享問題的類,它能使原本線程間共享的對象進行線程隔離,即一個對象只對一個線程可見
  • 當設置value時,變量的值保存在一個與線程相關的map中(ThreadLocalMap),這樣做是為了避免多線程競爭,因為放在Thread對象中就相當于線程私有了,處理的時候不需要加鎖
  • ThreadLocalMap里面的 Entry extends WeakReference<ThreadLocal<?>>
  • 存在的潛在問題
    • 如果任務對象結束而線程實例仍然存在(常見于線程池的使用中,需要復用線程實例),那么仍然會發(fā)生內(nèi)存泄露。
    • 線程復用會產(chǎn)生臟數(shù)據(jù)
    • ThreadLocalMap在它的getEntry、set、remove、rehash等方法中都會主動清除ThreadLocalMap中key為null的Entry,但如果我們聲明ThreadLocal變量后,再也沒有調(diào)用過上述方法,依然會發(fā)生內(nèi)存泄露
  • 參考

編碼格式

  • 文字到0、1的映射稱為編碼,反過來從0、1到文字叫解碼。
  • 最早的計算機在設計時采用8個比特(bit)作為一個字節(jié)(byte),所以,一個字節(jié)能表示的最大的整數(shù)就是255(二進制11111111=十進制255),0 - 255被用來表示大小寫英文字母、數(shù)字和一些符號,這個編碼表被稱為ASCII編碼
  • Unicode編碼定義了這個世界上幾乎所有字符的數(shù)字表示,已經(jīng)擴展到了 21 位
    • Unicode給這串數(shù)字ID起了個名字叫[碼點]。[碼點]經(jīng)過映射后得到的二進制串的轉換格式單位稱之為[碼元]
    • [碼點]就是一串二進制數(shù),【碼元】就是切分這個二進制數(shù)的方法。
    • 編碼空間被分成 17 個平面(plane),每個平面有 65,536 個字符(2個字節(jié),16位)。0 號平面叫做「基本多文種平面」(BMP),涵蓋了幾乎所有你能遇到的字符,除了 emoji(emoji位于1號平面 - -)。其它平面叫做補充平面,大多是空的
  • UTF-32 UTF-32也就是說它的碼元是32位,每32位去讀一下碼點
  • UTF-16 它的碼元是16位的,也就是說每16位去讀一下碼點,獲取碼點的前16位數(shù)字,直到讀取完成。
    • BMP平面(plane0)中的每一個碼點都直接與一個UTF-16 的碼元一一映射。
    • 其它平面里很少使用的碼點都是用兩個 16 位的碼元來編碼的
  • UTF-8 使用一到四個字節(jié)來編碼一個碼點
    • 從 0 到 127 的這些碼點直接映射成 1 個字節(jié),西文,都位于此段,該編碼方式非常節(jié)約空間
    • 接下來的 1,920 個碼點映射成 2 個字節(jié)
    • 在 BMP 里所有剩下的碼點需要 3 個字節(jié) 對于中文,就位于此段,3個字節(jié)
    • 參考

volatile

用于保持內(nèi)存可見性(隨時見到的都是最新值)和防止指令重排序 參考

java內(nèi)存模型
  • 主內(nèi)存,工作內(nèi)存
    • 主內(nèi)存:虛擬機中的一塊內(nèi)存,對應java堆中的對象實例數(shù)據(jù)
    • 工作內(nèi)存:每條線程自己的內(nèi)存空間,保存了該線程使用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作,都必須在工作內(nèi)存中進行,不能直接讀取主內(nèi)存中的變量,對應虛擬機棧中的部分區(qū)域
  • 內(nèi)存間的交互
    • lock:作用于主內(nèi)存,把變量標識為線程獨占狀態(tài)。
    • unlock:作用于主內(nèi)存,解除獨占狀態(tài)。
    • read:作用主內(nèi)存,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存。
    • load:作用于工作內(nèi)存,把read操作傳過來的變量值放入工作內(nèi)存的變量副本中。
    • use:作用工作內(nèi)存,把工作內(nèi)存當中的一個變量值傳給執(zhí)行引擎。
    • assign:作用工作內(nèi)存,把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量。
    • store:作用于工作內(nèi)存的變量,把工作內(nèi)存的一個變量的值傳送到主內(nèi)存中。
    • write:作用于主內(nèi)存的變量,把store操作傳來的變量的值放入主內(nèi)存的變量中。
  • 可見性:當一條線程修改了某個變量的值,新值對于其他線程來說是可以立即得知的
    • 普通變量的值是線程間傳遞需要通過主內(nèi)存來完成的,一個值在一個線程被修改,需要向主內(nèi)存進行回寫,另一條線程在去從主內(nèi)存中進行讀取操作,新值才會對另外一條線程可見
volatile保持可見性
  • 關鍵字修飾的變量看到的隨時是自己的最新值
  • volatile的特殊規(guī)則就是:
    • read、load、use動作必須連續(xù)出現(xiàn)。
    • assign、store、write動作必須連續(xù)出現(xiàn)。
  • 使用volatile變量能夠保證:
    • 每次讀取前必須先從主內(nèi)存刷新最新的值。
    • 每次寫入后必須立即同步回主內(nèi)存當中。
  • 注意:volatile關鍵字使變量的讀、寫具有了“原子性”。然而這種原子性僅限于變量(包括引用)的讀和寫,無法涵蓋變量上的任何操作,即:
    • 基本類型的自增(如count++)等操作不是原子的。
    • 對象的任何非原子成員調(diào)用(包括成員變量和成員方法)不是原子的。
防止指令重排
  • 指令重排序是cpu采用了允許將多條指令不按照程序規(guī)定的順序分開發(fā)送給各相應電路單元來處理

  • volatile關鍵字通過“內(nèi)存屏障”來防止指令被重排序 (只有在Happens-Before內(nèi)存模型中才會出現(xiàn)指令重排)

  • volatile 讀操作的性能消耗與普通變量幾乎沒有什么差別,寫操作可能會慢一些,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不返生亂序執(zhí)行

  • 來看一個單利模式 DCL(Double Check Lock,雙重檢查鎖)

  class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        if ( instance == null ) { //當instance不為null時,仍可能指向一個“被部分初始化的對象”
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

它可以”抽象“為下面幾條JVM指令:

memory = allocate();    //1:分配對象的內(nèi)存空間
initInstance(memory);   //2:初始化對象
instance = memory;      //3:設置instance指向剛分配的內(nèi)存地址

JVM可以以“優(yōu)化”為目的對它們進行重排序,經(jīng)過重排序后如下:

memory = allocate();    //1:分配對象的內(nèi)存空間
instance = memory;      //3:設置instance指向剛分配的內(nèi)存地址(此時對象還未初始化)
ctorInstance(memory);   //2:初始化對象

引用instance指向了一個"被部分初始化的對象"。此時,如果另一個線程調(diào)用getInstance方法,由于instance已經(jīng)指向了一塊內(nèi)存空間,從而if條件判為false,方法返回instance引用,用戶得到了沒有完成初始化的“半個”單例。

解決這個該問題,只需要將instance聲明為volatile變量:

private static volatile Singleton instance;

Thread類的sleep() yield() 和 wait()的區(qū)別?

  • sleep()方法(休眠)是線程類(Thread)的靜態(tài)方法,調(diào)用此方法會讓當前線程暫停執(zhí)行指定的時間,將執(zhí)行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束后會自動恢復
  • wait()是Object類的方法,調(diào)用對象的wait()方法導致當前線程放棄對象的鎖(線程暫停執(zhí)行),進入對象的等待池(wait pool),只有調(diào)用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態(tài)。
  • sleep()方法給其他線程運行機會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程以運行的機會;yield()方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的機會

多線程如何保證線程安全

  • 同步 Synchronized 參考
    • 普通同步方法,鎖是當前實例對象.JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區(qū)分一個方法是否同步方法
    • 靜態(tài)同步方法,鎖時當前類的Class對象。
    • 代碼塊同步:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。使用monitorenter和monitorexit指令實現(xiàn)。
  • 使用原子類(atomic concurrent classes) 如AtomicInteger等,AtomicInteger通過聲明一個volatile value(內(nèi)存鎖定,同一時刻只有一個線程可以修改內(nèi)存值)類型的變量,再加上unsafe.compareAndSwapInt的方法,來保證實現(xiàn)線程同步的。
  • 實現(xiàn)并發(fā)鎖
  • 使用volatile關鍵字
  • 使用不變類和線程安全類

GC

參考

找到被回收的對象
  • 引用計數(shù)法
    • 堆中每個對象(不是引用)都有一個引用計數(shù)器。當一個對象被創(chuàng)建并初始化賦值后,該變量計數(shù)設置為1。每當有一個地方引用它時,計數(shù)器值就加1(a = b, b被引用,則b引用的對象計數(shù)+1)。當引用失效時(一個對象的某個引用超過了生命周期(出作用域后)或者被設置為一個新值時),計數(shù)器值就減1。任何引用計數(shù)為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數(shù)減1。
    • 優(yōu)點:引用計數(shù)收集器執(zhí)行簡單,判定效率高,交織在程序運行中。
    • 缺點: 難以檢測出對象之間的循環(huán)引用。同時,引用計數(shù)器增加了程序執(zhí)行的開銷
    • 早期的JVM使用引用計數(shù)
  • 可達性分析法
    • 通過一系列名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下繼續(xù)尋找它們的引用節(jié)點,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證明此對象是不可用的。
    • GC Roots對象:虛擬機棧中引用的對象;方法區(qū)中類靜屬性引用的對象;方法區(qū)中常量引用的對象;本地方法中JNI引用的對象
    • 真正宣告一個對象死亡,至少要經(jīng)歷兩次標記過程:
      • 如果對象在進行根搜索后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它會被第一次標記并且進行一次篩選。篩選的條件是此對象是否有必要執(zhí)行 finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為沒有必要執(zhí)行。
      • 如果該對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會被放置在一個名為F-Queue隊列中,并在稍后由一條由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行finalize()方法。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標記,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對象重新引用鏈上的任何一個對象建立關聯(lián)即可。而如果對象這時還沒有關聯(lián)到任何鏈上的引用,那它就會被回收掉。
回收算法
  • 標記清除算法

    • 首先標記出所需回收的對象,在標記完成后統(tǒng)一回收掉所有被標記的對象
    • 優(yōu)點:不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效。
    • 缺點:
      • 標記和清除過程的效率都不高。這種方法需要使用一個空閑列表來記錄所有的空閑區(qū)域以及大小。對空閑列表的管理會增加分配對象時的工作量。
      • 標記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片
  • 復制算法

    • 它將內(nèi)存按容量分為大小相等的兩塊,每次只使用其中的一塊(對象面),當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊內(nèi)存上面(空閑面),然后再把已使用過的內(nèi)存空間一次清理掉。
    • 優(yōu)點:
      • (1)標記階段和復制階段可以同時進行。
      • (2)每次只對一塊內(nèi)存進行回收,運行高效。
      • (3)只需移動棧頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單。
      • (4)內(nèi)存回收時不用考慮內(nèi)存碎片的出現(xiàn)
    • 缺點:需要一塊能容納下所有存活對象的額外的內(nèi)存空間。因此,可一次性分配的最大內(nèi)存縮小了一半。
    • 復制算法比較適合于新生代(短生存期的對象),在老年代(長生存期的對象)中,對象存活率比較高,如果執(zhí)行較多的復制操作,效率將會變低,所以老年代一般會選用其他算法,如標記—整理算法
  • 標記—整理算法

    • 標記的過程與標記—清除算法中的標記過程一樣,但對標記后出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存
    • 優(yōu)點:
      • (1)經(jīng)過整理之后,新對象的分配只需要通過指針碰撞便能完成(Pointer Bumping),相當簡單。
      • (2)使用這種方法空閑區(qū)域的位置是始終可知的,也不會再有碎片的問題了。
    • 缺點:GC暫停的時間會增長,因為你需要將所有的對象都拷貝到一個新的地方,還得更新它們的引用地址。
分代收集
  • Java的堆內(nèi)存劃分:新生代、年老代和持久代。新生代又被進一步劃分為Eden和Survivor區(qū),最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)組成

  • 年輕代:

    • 幾乎所有新生成的對象首先都是放在年輕代的。
    • 新生代內(nèi)存按照8:1:1的比例分為一個Eden區(qū)和兩個Survivor(Survivor0,Survivor1)區(qū)。
    • 大部分對象在Eden區(qū)中生成。 當新對象生成,Eden Space申請失敗(因為空間不足等),則會發(fā)起一次GC(Scavenge GC)。
    • 回收時先將Eden區(qū)存活對象復制到一個Survivor0區(qū),然后清空Eden區(qū),當這個Survivor0區(qū)也存放滿了時,則將Eden區(qū)和Survivor0區(qū)存活對象復制到另一個Survivor1區(qū),然后清空Eden和這個Survivor0區(qū),此時Survivor0區(qū)是空的,然后將Survivor0區(qū)和Survivor1區(qū)交換,即保持Survivor1區(qū)為空, 如此往復。
    • 當Survivor1區(qū)不足以存放 Eden和Survivor0的存活對象時,就將存活對象直接存放到老年代。當對象在Survivor區(qū)躲過一次GC的話,其對象年齡便會加1,默認情況下,如果對象年齡達到15歲,就會移動到老年代中。
  • 年老代

    • 在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。內(nèi)存比新生代也大很多(大概比例是1:2)
    • 當老年代內(nèi)存滿時觸發(fā)Major GC即Full GC,F(xiàn)ull GC發(fā)生頻率比較低,老年代對象存活時間比較長,存活率標記高
    • 一般來說,大對象會被直接分配到老年代。所謂的大對象是指需要大量連續(xù)存儲空間的對象,最常見的一種大對象就是大數(shù)組
  • 持久代

    • 用于存放靜態(tài)文件(class類、方法)和常量等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態(tài)生成或者調(diào)用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。
    • 對永久代的回收主要回收兩部分內(nèi)容:廢棄常量和無用的類。
    • 永久代空間在Java SE8特性中已經(jīng)被移除。取而代之的是元空間(MetaSpace)。因此不會再出現(xiàn)“java.lang.OutOfMemoryError: PermGen error”錯誤。
  • 堆內(nèi)存分配策略

    • 對象優(yōu)先在Eden分配。
    • 大對象直接進入老年代。
    • 長期存活的對象將進入老年代。
  • 分代的回收算法

    • 新生代GC(Minor GC/Scavenge GC):發(fā)生在新生代的垃圾收集動作。因為Java對象大多都具有朝生夕滅的特性,因此Minor GC非常頻繁。在新生代中,每次垃圾收集時都會發(fā)現(xiàn)有大量對象死去,只有少量存活,因此可選用復制算法來完成收集。
    • 老年代GC(Major GC/Full GC):發(fā)生在老年代的垃圾回收動作。由于老年代中的對象生命周期比較長,因此Major GC并不頻繁。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。
    • 新生代采用空閑指針的方式來控制GC觸發(fā),指針保持最后一個分配的對象在新生代區(qū)間的位置,當有新的對象要分配內(nèi)存時,用于檢查空間是否足夠,不夠就觸發(fā)GC。當連續(xù)分配對象時,對象會逐漸從Eden到Survivor,最后到老年代
其他
  • 垃圾回收執(zhí)行時間

    • GC分為Scavenge GC和Full GC。
      • Scavenge GC :發(fā)生在Eden區(qū)的垃圾回收。
      • Full GC :對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢
    • 有如下原因可能導致Full GC:
      • 1.年老代(Tenured)被寫滿;
      • 2.持久代(Perm)被寫滿;
      • 3.System.gc()被顯示調(diào)用;
      • 4.上一次GC之后Heap的各域分配策略動態(tài)變化.
  • 相關函數(shù)

    • System.gc(),請求Java的垃圾回收。僅僅是一個請求(建議)。JVM接受這個消息后,并不是立即做垃圾回收,而只是對幾個垃圾回收算法做了加權,使垃圾回收操作容易發(fā)生,或提早發(fā)生,或回收較多而已。
    • finalize()
      • 在finalize()方法返回之后,對象消失,垃圾收集開始執(zhí)行。
      • finalize()的主要用途是釋放一些其他做法開辟的內(nèi)存空間,以及做一些清理工作。其他做法開辟的內(nèi)存空間,例如:1)由于在分配內(nèi)存的時候可能采用了類似 C語言的做法,而非JAVA的通常new做法,(2)打開的文件資源等
      • 一旦垃圾回收器準備好釋放對象占用的存儲空間,首先會去調(diào)用finalize()方法進行一些必要的清理工作。只有到下一次再進行垃圾回收動作的時候,才會真正釋放這個對象所占用的內(nèi)存空間。
  • 觸發(fā)主GC的條件

    • 當應用程序空閑時,即沒有應用線程在運行時,GC會被調(diào)用。因為GC在優(yōu)先級最低的線程中進行,所以當應用忙時,GC線程就不會被調(diào)用
    • Java堆內(nèi)存不足時,GC會被調(diào)用。當應用線程在運行,并在運行過程中創(chuàng)建新對象,若這時內(nèi)存空間不足,JVM就會強制地調(diào)用GC線程,以便回收內(nèi)存用于新的分配。若GC一次之后仍不能滿足內(nèi)存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。
    • 在編譯過程中作為一種優(yōu)化技術,Java 編譯器能選擇給實例賦 null 值,從而標記實例為可回收
  • 減少GC開銷的措施

    • 不要顯式調(diào)用System.gc()。 這樣會增加了間歇性停頓的次數(shù)。
    • 盡量減少臨時對象的使用。 少用臨時變量就相當于減少了垃圾的產(chǎn)生,從而延長了出現(xiàn)第二個垃圾回收的時間,減少了主GC的機會。
    • 對象不用時最好顯式置為Null。 有利于GC收集器判定垃圾,從而提高了GC的效率。
    • 盡量使用StringBuffer,而不用String來累加字符串。 String是固定長的字符串對象,累加String對象時,本質(zhì)上是重新創(chuàng)建新的String對象
    • 能用基本類型如Int,Long,就不用Integer,Long對象。 基本類型變量占用的內(nèi)存資源比相應對象占用的少得多
    • 盡量少用靜態(tài)對象變量。 靜態(tài)變量屬于全局變量,不會被GC回收,它們會一直占用內(nèi)存。
    • 分散對象創(chuàng)建或刪除的時間。 集中在短時間內(nèi)大量創(chuàng)建新對象,所需內(nèi)存變多,會增加主GC的頻率。集中刪除對象會突然出現(xiàn)了大量的垃圾對象,會增加主GC的頻率。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Java部分 1.GC是什么? 為什么要有GC? GC是垃圾收集的意思(Gabage Collection),內(nèi)存...
    01_小小魚_01閱讀 231評論 0 3
  • Java SE 基礎: 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,247評論 0 8
  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數(shù)? 在 Jav...
    侯蛋蛋_閱讀 2,710評論 1 4
  • OC的理解與特性OC作為一門面向?qū)ο蟮恼Z言,自然具有面向?qū)ο蟮恼Z言特性:封裝、繼承、多態(tài)。它既具有靜態(tài)語言的特性(...
    LIANMING_LI閱讀 580評論 0 0
  • 序言 目前形勢,參加到iOS隊伍的人是越來越多,甚至已經(jīng)到供過于求了。今年,找過工作人可能會更深刻地體會到今年的就...
    恒愛DE問候閱讀 5,610評論 0 9

友情鏈接更多精彩內(nèi)容