八股文


目錄

1 操作系統

1.1 進程和線程的區別

  • 進程是資源分配的最小單位,線程是任務執行的最小單位。
  • 進程有自己獨立的地址空間,每啟動一個線程,都會為它分配地址空間,建立數據表來維護代碼段、堆棧端和數據段,這種操作非常昂貴。而線程是共享進程數據的,使用相同的地址空間,因此 CPU 切換一個線程的開銷遠比進程要小得多,同時創建一個線程的開銷也比進程小得多。
  • 線程間的通信更加方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以進程間通信進行。不過如何處理同步和互斥是多線程編程的難點。
  • 多進程更加健壯,多線程程序只要有一個線程死掉,整個進程也就死掉了,而一個進程死掉,並不會對其他進程產生影響,因為進程有獨立的地址空間。

1.2 進程的調度算法有哪些?

  • 先來先去服務

  • 時間片輪轉

  • 短作業優先

  • 多級反饋隊列調度算法

  • 優先級調度

1.3 常用的 IO 模型

  • 同步阻塞 IO:應用進程被阻塞,直到數據從內核緩沖區復制到應用進程緩沖區中才返回。
  • 同步非阻塞 IO:進程發起 IO 系統調用后,內核返回一個錯誤碼而不會被阻塞;應用進程可以繼續執行,但是需要不斷的執行系統調用來獲知 I/O 是否完成。如果內核緩沖區有數據,內核就會把數據返回進程。一個輸入操作通常包含兩個過程:
    • 等待數據准備好,對於一個 socket 上的操作,這一步驟涉及到數據從網絡送達,並將其復制到內核的某個緩沖區。
    • 將數據從內核緩沖區復制到進程緩沖區。
  • 異步 IO:進程發起一個 IO 操作,進程返回不阻塞,但也不能返回結果;內核把 IO 處理完后,通知進程結果。
  • IO 復用:使用 select 、poll 等待數據,可以等待多個 socket 中的任何一個變為可讀。這一過程會被阻塞,當某一個 socket 可讀時返回,之后把數據從內核復制到進程中。

1.4 select、poll 和 epoll 的區別?

select、poll、epoll 允許應用程序監視一組文件描述符,等待一個或多個描述符成為就緒狀態,從而完成 IO 操作。

select 和 poll 的功能基本相同,不過在實現細節有些不同:

  • select 的描述符類型使用數組實現,FD_SETSIZE 大小默認為 1024,因此默認只能監視少於 1024 個描述符。如果要監聽更多描述符,需要修改 FD_SETSIZE 后重新編譯。
  • poll 本質上和 select 沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個 fd 對應的設備狀態,但 poll 沒有描述符的限制,原因是它基於鏈表來存儲。

epoll 可以理解為 event poll,不同於 select、poll,epoll 會將哪個流發生了怎樣的變化通知我們,epoll 是基於事件驅動的。已注冊的描述符在內核中會被維護在一顆紅黑樹中,調用 callback 的描述符會被加入一個鏈表中去管理;epoll 的觸發模式有兩種:

  • LT(水平觸發):當 epoll_wait() 檢測到描述符事件到達時,將此事件通知進程,進程可以不立即處理該事件,下次調用 epoll_wait()會再次通知進程。
  • ET(垂直觸發):和 LT 模式不同的是,通知之后進程必須立即處理事件,下次再調用 epoll_wait() 時不會再得到事件到達的通知。

1.5 進程的通信方式有哪些?線程呢?

1.5.1 進程間的通信方式
  • 匿名管道
  • 有名管道
  • 消息隊列
  • 信號
  • 信號量
  • 共享內存
  • 套接字
1.5.2 線程間通信方式
  • 互斥量
  • 信號量
  • 事件

1.6 fork 函數的作用?

fork 函數的作用是在已經存在的線程中創建一個子進程,原進程為父進程。

調用 fork,當控制轉移到內核中的 fork 代碼后,內核開始做:

  • 分配新的內存塊和內核數據結構給子進程。
  • 將父進程部分數據結構內容拷貝至子進程。
  • 將子進程添加到系統進程列表。
  • fork 返回開始調用器進程調度。

fork 調用一次返回兩次,返回給父進程子進程的 pid,返回給子進程 0;父進程和子進程的代碼段是共享的,其他都是得到父進程一個副本,所以父進程和子進程是兩個進程,兩者並不會共享內存地址。

1.7 講一下用戶態和內核態?所有的系統調用都會進入內核態嗎?

操作系統是管理計算機硬件與軟件資源的程序,根據進程訪問資源的特點,我們可以把進程在操作系統的運行分為兩個級別:

  • 用戶態:用戶運行的進程可以讀取用戶程序的數據。
  • 內核態:內核態運行的程序幾乎可以訪問計算機的任何資源。

運行的程序基本上都是運行在用戶態,如果我們調用操作系統提供的內核級子功能就需要進程系統調用來切換到內核態去執行。

系統調用:與系統級別的資源有關的操作(文件管理、進程管理、內存管理等),都必須通過系統調用的方式向操作系統發起請求,由操作系統代為執行。

用戶態切換到內核態的幾種方法:

  • 系統調用:系統調用是用戶態主動切換到內核態的一種方式,用戶應用程序通過操作系統調用來完成上層應用程序開放的接口。
  • 異常:當 cpu 執行用戶應用程序發生異常后,發生了某些不可知的異常,於是當前用戶應用程序切換到處理此異常的內核程序。
  • 硬件設備的中斷:當硬件設備完成用戶請求后,會向 cpu 發送相應的中斷信號,此時 cpu 會暫停執行下一步需要執行的指令,轉而去執行與中斷信號對應的應用程序,如果之前下一步要執行的指令是用戶態,那么這個操作也可以將用戶態切換成內核態。

2 計算機網絡

2.1 為什么網絡要分層?

  • 各層之間相互獨立:各層之間相互獨立,不需要關心其他層如何工作的,只需要調用其他層提供的接口。
  • 提高整體靈活性:每一層都可以使用最合適的技術來實現。
  • 大問題化小問題:將功能進行分解。

2.2 TCP/IP 4 層模型了解么?

  • 應用層
  • 傳輸層
  • 網絡層
  • 網絡接口層

2.3 HTTP 是哪一層的協議?http 常見的狀態碼?

HTTP 是應用層的協議。

  • 200:請求成功
  • 301:永久重定向
  • 302:臨時重定向
  • 400:請求參數錯誤
  • 401:請求為授權
  • 403:沒有訪問權限
  • 404:找不到對應資源
  • 405:請求方式不支持
  • 500:服務器錯誤
  • 502:網關錯誤
  • 504:代理服務無法及時從上游服務器獲得響應

2.4 HTTP 和 HTTPS 什么區別?

  1. HTTP 默認采用 80 端口,HTTPS 默認采用 443 端口;
  2. HTTP 協議運行在 TCP 之上,所有傳輸的內容都是明文,客戶端和服務器都無法鑒別對方的身份;HTTPS 是運行在 SSL 之上的 HTTP 協議,所有傳輸過程都經過了對稱加密,但對稱加密的密鑰用服務器的證書進行了非堆成加密;
  3. HTTP 的安全性沒有 HTTPS 高,但是 HTTPS 需要消耗更多的服務器資源。

2.5 講一下對稱加密算法和非對稱加密算法?

對稱加密:密鑰只有一個,加密解密為同一個密碼,加解密速度快,常見的加密算法有 DES、AES 等。

非對稱加密:加密和解密采用不同的密鑰。通信發送方使用接收方的公開密鑰加密,接收方使用私鑰解密。運算速度慢,常見的非對稱加密有 RSA、DSA 等。

2.6 HTTP報文詳解?詳細說一下請求報文,以及HTTP和TCP的區別

HTTP 有兩種報文,分別是請求報文和響應報文。

請求報文包括請求行、請求頭、空行、請求體。

響應報文包括狀態行、響應頭、響應體。

2.7 TCP三次握手的過程,以及三次握手的原因?

三次握手

三次握手的目的是為了確定雙方的發送和接受都是正常的;第三次握手是為了防止失效的連接請求到達服務器,讓服務器錯誤的打開連接。

2.8 TCP四次揮手的過程,以及四次揮手的原因?

四次揮手CLOSE-WAIT 狀態問題:

客戶端發送了 FIN 連接釋放報文后,服務端收到這個報文后,就進入了 CLOSE-WAIT 狀態。這個狀態是為了讓服務器發送未發送完畢的數據,傳送完畢后,服務端會發送 FIN 連接釋放報文。

TIME-WAIT 狀態問題

客戶端收到服務器發來的 FIN 連接釋放報文后進入此狀態,此時不是直接進入 CLOSED 狀態,還需要等待 2MSL,有兩個原因:

確保最后一個報文能到達服務端,如果服務端沒有收到客戶端發來的確認報文,就會重新向客戶端發送 FIN 連接釋放報文;當重新收到服務端發來的 FIN 連接釋放報文后,會向服務端發送確認報文並重置計時器。等待一段時間是為了讓本連接產生的所有報文都在網絡上消失,使得下一個連接不會出現舊的連接請求報文。

2.9 TCP 滑動窗口是干什么的?TCP 的可靠性體現在哪里?擁塞控制如何實現的?

滑動窗口:

窗口是緩存的一部分,用來暫存字節流。發送方和接收方各有一個窗口,接收方通過報文中的窗口字段告知發送方自己的窗口大小。發送方根據這個值和其他信息設置自己的窗口大小。發送窗口內的字節都被允許發送,接受窗口內的字節都被允許接收。接收方只會對窗口內的最后一個字節確認,如果發送窗口內的字節已經發送並且已經得到確認,那么就將發送窗口向右滑動一定距離;接收窗口類似,接收窗口左部字節已經發送確定並交付主機,就向左滑動一定距離。

流量控制實現:

流量控制是為了控制發送發的發送速率以保證接收方來得及接收。接收方發送的確認報文中的窗口字段告知發送方自己的窗口的大小,從而影響發送方的發送速率。將窗口字段設為 0,則發送方不能發送數據。

擁塞控制:

如果網絡出現擁塞,則分組就會丟失,此時發送方會繼續重傳,從而導致擁塞更多嚴重。因此發生網絡擁塞時,應該控制發送發的發送速率。TCP 通過四個算法來控制發送方的發送速率:慢開始、擁塞避免、快重傳、快恢復。發送方需要維護一個叫做擁塞窗口(cwnd)的狀態變量。

  • 慢開始與擁塞避免:發送方最初執行慢開始,此時將 cwnd 設為 1,發送方只能發送一個報文段,收到確認后,將 cwnd 加倍。設置一個慢開始門限,超過門限后進入擁塞避免,每次將 cwnd + 1,如果出現了超時,則將慢開始門限設為 cwnd / 2,然后重新執行慢開始。
  • 在接收方,要求每次接收到報文段都應該對最后一個報文段進行確認,如果收到三個重復的確認,那么就可以認為下一報文丟失,此時執行快重傳,立即重傳下一報文段。在這種情況下,只是丟失個別報文段,並不是網絡擁塞,所以將慢開始門限設為 cwnd / 2,擁塞窗口設為慢開始門限,直接進入擁塞避免。

可靠性如何保證:

TCP 使用超時重傳來實現可靠傳輸。如果一個已經發送的報文段在超時時間沒有收到確認,那么就重傳這個報文段。

  1. 應用程序被分割成 TCP 認為最適合發送的數據塊;

  2. TCP 給發送的每一個包進行編號,接收方對數據包進行排序,把有序數據傳送給應用層;

  3. 檢驗和:TCP 將保持它首部和數據的檢驗和。這是一個端到端的檢驗和,目的是檢測數據在傳輸過程中的任何變化。如果收到段的檢驗和有差錯,TCP 將丟棄這個報文段和不確認收到此報文段;

  4. TCP 的接收端會丟棄重復的報文;

  5. 流量控制;

  6. 擁塞控制;

  7. ARQ 協議:每發送完一個分組就停止發送,等待確認,收到確認后再發送下一個分組;

  8. 超時重傳:當 TCP 發出一個段后,它啟動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段。

2.10 TCP 和 UDP 有什么區別?及其適用的場景?

UDP 是無連接的,盡最大可能交付,沒有擁塞控制,面向報文,支持一對一、一對多、多對多的交互通信。

TCP 是面向連接的,提供可靠支付,有流量控制、擁塞控制,提供全雙工通信,面向字符流,每一個 TCP 是點對點的。

TCP 應用場景:

  • 文件傳輸
  • 郵件發送
  • 遠程登錄

UDP 應用場景:

  • QQ 聊天
  • 在線視頻
  • 語音聊天

UDP 為什么快?

  • 不需要建立連接
  • 不需要對接收的數據給出確認
  • 沒有超時重發
  • 沒有流量控制和擁塞控制

2.11 Mac 地址和 IP 地址的區別?既然有了 Mac 地址,為什么還要 IP 地址呢?

Mac 地址是燒錄在網卡的物理地址,具有全球唯一性,IP 地址是網絡中的設備在局域網的邏輯地址,具有局域網唯一性。

2.12 當你打開一個電商網站,都需要經歷哪些過程?分別用到了什么協議?

  1. 瀏覽器查找域名對應的 IP 地址;
  2. 瀏覽器向 web 服務器發送 HTTP 請求;
  3. 服務器處理請求;
  4. 服務器返回 HTTP 報文,發送 HTML 響應;
  5. 瀏覽器解析渲染頁面;

使用的協議:

  1. DNS
  2. IP
  3. ARP
  4. OSPF
  5. HTTP

2.13 DNS 解析過程,DNS 劫持了解嗎?

  1. 客戶機提出域名解析請求,並將該請求發送給本地域名服務器;
  2. 當本地域名服務器收到請求后,首先查詢本地緩存,如果有記錄,直接返回;
  3. 如果本地緩存中沒有記錄,則本地域名服務器就直接把請求發給根域名服務器,然后跟域名服務器再返回給本地域名一個所查詢域主域名服務器地址;
  4. 本地服務器再向上一步返回的域名服務器發送請求,然后接收請求的服務器查詢自己的緩存,如果沒有該記錄,則返回相關下級域名服務器地址;
  5. 重復第四步,直到找到記錄;
  6. 本地域名服務器將查詢結果保存,以備下次使用。

DNS 劫持:在 DNS 服務器中,將 www.xx.com 域名對應的 IP 地址進行了變化,解析出來的 IP 地址不是正確的地址。

HTTP劫持:在網站交互過程中劫持了請求,在網站返回請求之前向你返回了偽造的請求。

2.14 GET 和 POST 有什么不一樣?

  • GET 在瀏覽器回退是無害的,而 POST 會再次發送請求。
  • GET 會被瀏覽器主動緩存。
  • GET 有長度限制,POST 沒有長度限制。
  • GET 相對於 POST 不安全,因為參數暴露在 url 中。
  • GET 請求參數會完整的保存在歷史記錄中,而 POST 不會。
  • GET 會將 header 和 body 同時發送給服務器,而 POST 第一次先將 header 發送給服務器,收到瀏覽器返回 100 contine,再將 body 請求給服務器。

2.15 session 和 cookie的問題?

session 和 cookie 都是用來保存瀏覽器用戶身份的會話方式。

cookie 一般用來保存用戶信息,Session 主要作用是通過服務端記錄用戶的狀態。

cookie 數據保存再客戶端,session 數據保存在服務端。

session 比較安全,session 依賴於 cookie 實現,如果禁用 cookie,session 也不可使用。

2.16 HTTP 是不保存狀態的協議,如何保存用戶狀態?

session、cookie、內存數據庫

cookie 被禁用 session 怎么用?

url 攜帶、矩陣變量

2.17 Arp 協議?

  1. 如果發送端和接收端在同一個網段,發送端發送數據幀前先檢查是否有接收端的 mac 地址;
  2. 如果沒有,啟動 arp,檢查緩存 ip-mac 表中是否有接收端的 mac 地址,如果有,拿來就用;
  3. 如果沒有則在本網段廣播,本網段各計算機都收到 arp 請求,從發送來的數據檢查接收方 ip 是否和自己相同,如果相同,則將本機的 mac 地址單播給發送端,如果不相同,直接丟棄。
  4. 如果發送端和接收端不在同一個網段,請求端拿到的 mac 地址是網關的 mac 地址。

2.18 DDos 攻擊了解嗎?

分布式拒絕攻擊,一般來說指攻擊者利用控制的設備對目標網站短時間發送大量的請求,消耗目標網站的服務器資源,讓它無法正常服務。

2.19 什么是跨域,怎么解決?

跨域訪問基於同源策略,它要求通信的雙方必須在同一域中,當一個請求的協議、域名、端口 不完全相同就產生了跨域問題。

解決:

  • JSONP

  • CORS

2.20 常見的 WEB 攻擊手段及解決方案?

CSRF

攻擊者欺騙客戶端去訪問之前認證的網站

解決:

  • Referer
  • 添加校驗 token

XSS

XSS攻擊通常指的是通過利用網頁開發時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網頁,使用戶加載並執行攻擊者惡意制造的網頁程序。

解決:

  • 后台加過濾器

3 Java 基礎

3.1 StringBuilder 和 StringBuffer?

StringBuilder 和 StringBuffer 都是可變的字符串類型,但是 StringBuffer 類中的方法大量使用了 synchronization 修飾,所以 StringBuffer 是線程安全的,而 StringBuilder 是線程不安全的。

3.2 Java實現連續空間的內存分配?

3.3 創建對象的方式有哪幾種?

  • new Obj()

  • clone:調用對象的 clone 方法

  • 反射

    • 無參構造,需要提供對象的無參構造,如果沒有會拋異常

      Object o = clazz.newInstance();
      
    • 有參構造,需要先獲得對象的構造器,通過構造器調用 newInstance 函數創建對象

      Constroctor constroctor = User.class.getConstructor(String.class);
      Object obj =  constroctor.newInstance("name");
      
  • 通過反序列化創建對象,需要對象實現 Serializable 接口

3.4 接口和抽象類有什么區別?

  • 接口和抽象類都不能直接實例化。
  • 抽象類通過 extends 繼承,接口通過 implments 實現。
  • 抽象類可以有非抽象方法,接口只能對方法聲明。
  • 抽象類可以 public、protected、default 修飾符,接口只能存在 public。
  • 抽象類可以有構造器,接口不能有構造器。

3.5 深拷貝和淺拷貝區別?

  • 深拷貝:對於基本類型,拷貝的是基本類型的指;對於引用類型,創建一個新的對象,把值賦值進去。

  • 淺拷貝:對於基本類型,拷貝的基本類型的指;對於引用類型,拷貝的是引用類型的內存地址。

3.6 講一講封裝,繼承,多態(重要)?

編譯期多態

方法重載是編譯期多態,根據參數類型、個數,在編譯期就可以確定執行重載方法中的哪一個。

方法重寫表現兩種多態,當對象引用本類實例時,表現編譯器多態,否則時運行時多態。

運行時多態

通過父類對象引用變量引用子類對象。當父類對象引用子類實例時,通過接口類型變量引用實現接口的類的對象來實現。運行時多態主要通過繼承和接口來實現。

3.7 泛型是什么?類型擦除?

將類型當作參數傳遞給類或者方法。

Java 中的泛型是偽泛型,它是在編譯器層面實現的。在 Java 的編譯期間,所有的泛型信息都會被擦除掉,最后的在字節碼中不包含泛型中的任何信息的。

Java 編譯器先檢查泛型類型,然后在進行泛型擦除,再進行編譯。

3.8 如何實現靜態代理?有啥缺陷?

為現有的每個類編寫一個特定的代理對象,實現相同的接口。

在代理類的構造方法中傳入目標對象,然后在代理類方法中調用目標對象的同命方法,在這行代碼前后做一些操作。

3.9 動態代理的作用?在哪些地方用到了?(AOP、RPC 框架中都有用到,面試筆試中經常要求手寫一個動態代理)

為其它對象提供一種代理以控制對這個對象的訪問控制,在程序運行時,通過反射機制動態生成。JDK動態代理的調用處理程序必須實現 InvocationHandler 接口,及使用 Proxy 類中的 newProxyInstance 方法動態的創建代理類。

public interface HelloInterface {
    void sayHello();
}

public class Hello implements HelloInterface {
    @Override
    public void sayHello() {
        System.out.println("Hello!");
    }
}

public class ProxyHandler implements InvocationHandler {
    
    private Object object;
    
    public ProxyHandler(Object object){
        this.object = object;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
    
    public static void main(String[] args) {
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        HelloInterface hello = new Hello();
        
        InvocationHandler handler = new ProxyHandler(hello);

        HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

        proxyHello.sayHello();
    }
}

3.10 JDK 的動態代理和 CGLIB 有什么區別?

JDK 可以代理實現了接口的類,CGLIB 可以代理未實現任何接口的類,CGLIB 的原理是通常生成一個被代理類的子類來攔截被代理類的方法調用,因此不能將被代理類聲明成 final 類型。

3.11 談談對 Java 注解的理解,解決了什么問題?

Java 語言中的類、方法、變量、參數和包等都可以注解標記,程序運行期間我們可以獲取到相應的注解以及注解中定義的內容,這樣可以幫助我們做一些事情。比如說 Spring 中如果檢測到說你的類被 @Component 注解標記的話,Spring 容器在啟動的時候就會把這個類歸為自己管理,這樣你就可以通過 @Autowired 注解注入這個對象了。

3.12 Java 反射?反射有什么缺點?你是怎么理解反射的(為什么框架需要反射)?

反射是指在程序運行過程中,我們可以知道任何一個類的所有屬性和方法,對於任何一個對象,可以調用它所有的屬性和方法;這種動態獲取信息和動態調用對象方法的技術稱為反射。

反射的優缺點:

  • 運行期類型的判斷、動態加載類、提高代碼靈活度。
  • 性能問題:反射相當於一系列解釋操作,通知 JVM 要做的事情,性能比直接的 Java 代碼要慢很多
  • 安全問題

3.13 為什么框架需要反射技術?

我認為可以動態的創建和編譯對象,體現出語言強大的靈活性和擴展性。

3.14 獲取 Class 對象的兩種方式

// 第一種
Class alunbarClass = TargetObject.class;        
// 第二種
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3.15 內存泄露和內存溢出的場景?

內存泄漏:內存泄漏是指無用對象持有占有內存或無用對象的內存得不到釋放,從而造成內存空間的浪費稱為內存泄漏。內存泄漏的根本原因是長生命周期的對象持有短生命周期的引用就很可能發生內存泄漏,盡管短生命周期的對象已經不再需要,但因為長生命周期的對象持有它的引用,導致不能回收。

內存溢出:內存溢出是指程序運行過程中無法申請到足夠的內存。

3.16 講一下,強引用,弱引用,軟引用,虛引用?

  • 強引用:被強制關聯的對象不會被回收。
  • 軟引用:被軟引用關聯的對象會在內存不足的情況下回收。
  • 弱引用:被弱引用關聯的對象的生命周期到下一次 GC。
  • 虛引用:弱引用必須和引用隊列配合使用,當 GC 時發現還關聯着虛引用,會在執行完 finalize() 將虛引用加入到引用隊列中,在其關聯的虛引用沒有出隊錢,並不會徹底的清除這個對象,此時該對象為虛可達,只要顯示的在引用隊列中將虛引用出隊,才會徹底回收掉該對象,可以用來控制對象的清除時機。

3.17 講一下 Java 的 NIO,AIO, BIO?

BIO:同步阻塞 IO 模型,讀取寫入阻塞在一個線程中進行

NIO:同步非阻塞 IO 模型,提供了 Channel、Seletor、Buffer 對象,我們可以通過 NIO 進行非阻塞 IO 操作;單線程從通道讀取數據到 Buffer,同時可以繼續做別的事情,等到讀取完畢,線程在繼續處理數據,NIO 底層依賴於 epoll 實現。

AIO:異步非阻塞模型,基於事件和異步回調機制實現。

3.18 Java 中 finalize()方法的使用?

GC 回收對象前會調用此方法,一般用戶非 Java 資源的回收,一般不推薦調用此方法,因為 finalize() 方法調用時間不確定,從一個對象不可達到 finalize() 方法的調用之間的時間不任意長的。我們並不能依賴 finalize() 方法能 及時的回收占用的資源,可能出現的情況是在我們耗盡資源之前,gc 卻仍未觸發,因而通常的做法是提供顯示的 close() 方法供客戶端手動調用。另外,重寫 finalize() 方法意味着延長了回收對象時需要進行更多的操作,從而延長了對象回收的時間。

3.19 GC Root 對象有哪些?

  • 方法區靜態變量和常量引用的對象
  • 虛擬機棧中引用的對象
  • 本地方法棧中引用的對象

3.20 Java 中 Class.forName 和 ClassLoader 的區別?

類的加載過程:

  1. 裝載:通過類的全限定名獲取二進制字節流,將二進制字節流轉換成方法區運行時數據結構,在內存中生成 Java.lang.Class 對象;
  2. 鏈接:執行校驗、准備和解析;
  3. 校驗:檢查類或者接口的二進制數據的正確性;包括文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證;
  4. 准備:給類的靜態變量分配內存並初始化內存空間;
  5. 解析:將常量池中的符號引用轉化成直接引用;
  6. 初始化:執行 方法;包括靜態變量初始化語句和靜態塊的執行。

ClassLoader 遵循雙親委派機制,最終調用啟動類加載器通過某個類的全限定名去獲取類的二進制字節流,然后將二進制字節流放到虛擬機中,不會執行 static 內容,Class.forName() 會執行 static 內容。

4 集合框架

4.1 ArrayList 的擴容機制?

ArrayList 的擴容發生於 add() 方法中,add() 方法添加元素先通過 ensureCapacityInternal() 方法判斷是否需要擴容;

流程:

  1. 獲取原數組的容量;
  2. 原數組長度的 1.5 倍;
  3. 如果新長度小於最小需要的容量,將 minCapacity 賦值給 newCapacity;
  4. 如果新長度大於數組預置的最大長度,將 newCapacity 設置為 Integer 最大值;
  5. 然后通過 copyOf 將原數組元素復制到新數組中。

4.2 HashMap 的底層實現、JDK 1.8 的時候為啥將鏈表轉換成紅黑樹?HashMap 的負載因子?

HashMap 底層是數組+鏈表+紅黑樹來實現的,當向 map 中添加數據時,首先計算 key 的 hash 值,並根據 hash 值確定元素存儲在哪個 bucket 中,但是容易產生 hash 碰撞,hashmap 是采用拉鏈法的方式來解決 hash 碰撞的,當鏈表長度大於等於 8 時,這時候對於這個 bucket 中查詢效率會退化成 O(N),會將底層數據結構變成紅黑樹,這種考慮是因為紅黑樹的查詢效率更加穩定,當鏈表的長度小於等於 6 時,會重新將紅黑樹轉換成鏈表,這種考慮是因為紅黑樹比鏈表的存儲代價要大。

轉換成紅黑樹還有一個必要條件就是數組長度要大於 64,因為作者認為在數組長度低於 64,產生 hash 碰撞的幾率非常高。

hashmap 的負載因子是 0.75。

4.3 為什么 HashMap 長度是 2 的冪?

首先看 hash 方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

獲得 key 被存放在數組的哪個 bucket 中

i = (n - 1) & hash

這兩個需要連起來看,這也是 1.8 做出的優化之一,在 hash() 方法把 hashCode 的高 16 和低 16 都參與運算,這樣產生 hash 碰撞的幾率大大降低,也會讓數據更加平均。

經實際經驗,h % (length-1) == (length - 1) & hash 的前提是這個 length 必須是 2 的冪次方,& 的效率比

4.4 HashMap 的 key 需要注意哪些問題

key 必須是不可變對象,如 String、包裝類和采用 final 修飾的類。

4.5 HashMap hash 算法可以采用隨機數嗎?

不可以,因為下次不能定位到存儲的 bucket。

4.6 HashMap 和 Hashtable 的區別?

  • HashMap 允許一個 key 為 null,多個 value 為 null,Hashtable 不允許 key/value 為 null。
  • HashMap 是線程非安全的,Hashtable 是線程安全的。
  • HashMap 的默認長度是 16, Hashtable 的默認長度是 11。
  • HashMap 擴容兩倍,Hashtable 擴容兩倍 + 1。
  • HashMap 需要調用 hash() 方法計算 hash 值,Hashtable 直接調用 hashCode 值。

4.7 HashMap 和 HashSet 的區別?

HashSet 的構造方法中創建了 一個 HashMap,並且提供了一個空對象 PRESENT,不允許重復的原理實際上使用了 HashMap key 不能重復的特性。

4.8 ConcurrentHashMap 的底層實現?

ConcurrentHashMap 在 1.7 采用了分段的數組+鏈表實現,1.8 采用的數據結構和 HashMap 一致。

在 1.7 時,ConcurrentHashMap 采用了分段鎖,每一把鎖只鎖定他所管轄的那部分數據,多線程訪問不同數據段的數據就不會產生鎖的競爭,提交並發率;1.8 摒棄了這種臃腫的設計,並發控制采用 CAS+Synchronized 來實現,因為 1.7 最多同時有 16 個線程進行讀寫操作,Synchronized 只鎖定當前鏈表或者紅黑樹的首節點,只要 hash 不沖突,就不會產生並發。

4.9 為什么 ConcurrentHashMap 的讀操作不需要加鎖?

因為無論是 Node 還是 HashEntity,value 都已經被 volatile 修飾了,volatile 可以保證工作內存中共享變量的可見性,所以在 get() 方法中拿到的值永遠是最新值。

4.10 HashMap,LinkedHashMap,TreeMap 有什么區別?

LinkedHashMap 記錄元素的插入順序,在使用 iterator 遍歷時,先取到的數據肯定是先插入的;

TreeMap 實現了 SortMap 接口,所以它可以對 key 按照順序排列;

4.11 有哪些集合是線程不安全的,又有哪些集合是線程不安全的?怎么解決呢? 線程安全的集合類.

HashMap -> ConcurrentHashMap

HashSet -> CopyOnWriteHashSet

ArrayList -> CopyOnWriteArrayList

4.12 什么是快速失敗?什么是安全失敗?

快速失敗是集合的一種錯誤檢查機制,在使用迭代器遍歷時,如果在多線程下操作非安全的集合類,就會產成 ConcurrentModificationException 異常,另外,在單線程遍歷時對元素進行修改也會觸發快速失敗。

安全失敗,安全失敗的集合在遍歷時不是在原來的對象上進行操作的,而是在拷貝出的集合進行操作。

public static void main(String[] args) {
    ArrayList<Object> objects = new ArrayList<>();
    objects.add(1);
    objects.add(1);

    Iterator<Object> iterator = objects.iterator();

    // 多線程操作
    new Thread(() -> {
        while (iterator.hasNext()) {
            iterator.next();
        }
    }).start();

    new Thread(() -> {
        while (iterator.hasNext()) {
            objects.remove(iterator.next());
        }
    }).start();

	// 單線程遍歷修改
    for (Object object : objects) {
        objects.remove(object);
    }
}

為什么會產生安全失敗問題?

當調用 ArrayList.iterator() 方法時,首先會將 modCount 賦值給 expectedModCount,當調用 next 方法時,會判斷 modCount 是否 等於 modCount,我們調用 ArrayList.remove() 時,會修改 modCount 的值,但是不會修改 expectedModCount 的值,所以只要在遍歷時修改列表的元素,都會造成 modCount 和 expectedModCount 不一致。

4.13 HashMap 多線程操作導致死循環問題異常

主要原因在於並發下的 rehash 會造成元素之間會形成一個循環鏈表。不過,jdk 1.8 后解決了這個問題,但是還是不建議在多線程下使用 HashMap,因為多線程下使用 HashMap 還是會存在其他問題比如數據丟失。

4.14 講一下 CopyOnWriteArrayList 和 CopyOnWriteArraySet?

寫時復制的容器,當我們向容器中添加一個元素時,不直接向容器中添加,而是將當前容器進行 copy,在 copy 出來的副本上進行添加,添加完成之后,再將原容器的引用指向新容器。這樣做的好處是我們可以對 CopyOnWrite 容器進行並發的讀。在讀時並不需要加鎖。適合讀多寫少的場景。

缺點:

  • 資源浪費
  • 能保證數據的最終一致性,不能保證數據實時一致性

5 多線程

5.1 寫一個死鎖

public static void main(String[] args) {
    Object o1 = new Object();
    Object o2 = new Object();
    new Thread(() -> {
        synchronized (o1) {
            try {
                System.out.println(Thread.currentThread().getName() + " 獲取第一把鎖成功");
                TimeUnit.SECONDS.sleep(1);
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + " 嘗試獲取第二把鎖");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "A").start();
    new Thread(() -> {
        synchronized (o2) {
            try {
                System.out.println(Thread.currentThread().getName() + " 獲取第二把鎖成功");
                TimeUnit.SECONDS.sleep(1);
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + " 嘗試獲取第一把鎖");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "B").start();
}

5.2 講一下 volatile 關鍵字的作用?

voliatile 特性:

  • 可見性

  • 禁止指令重排

  • 不保證原子性

volatile 是怎么保證可見性和禁止指令重排的?

如果對 vlolatile 修飾的變量寫,JVM 會向處理器發送一條 Lock 前綴的指令,將這個變量所在的緩存行的數據強制寫回到主內存。但是即使我這邊將共享變量寫會了主內存,其他線程工作內存中的值還是舊的,所以在多處理器,為了保證各個處理器的緩存是一致的,就要依賴於緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是否過期,當處理器發送自己緩存行對應的內存地址被修改,就會將當前處理器緩存行設為無效狀態,當處理器要對這個數據做修改操作時,強制要求處理器必須從主內存重新讀取。

happend-before 原則

  • 程序順序原則:一個線程中每個操作,happend-before 於該線程中的任意后續操作。
  • 監視器原則:一個監視器的解鎖 happend-before 后續對這個監視器的加鎖。
  • voliate 原則:對一個 volatile 的寫 happend-before 於對這個 volatile 的讀。
  • 傳遞性原則:A happend-before B,B happend-before C,則 A happend-before C。
  • start 原則:如果線程 A 執行 線程 B.start,那么線程 A 中的 線程 B.start() happend-before 於線程 B 的任意操作。
  • join 原則:如果線程 A 執行 線程 B.join(),那么線程 B 的任意操作 happend-before 於線程 A 執行完線程 B.join() 成功返回的后續操作
  • 程序中斷原則:對一個線程的中斷 happend-before 與被中斷線程檢測到中斷時間的發生。
  • 對象的析構原則:一個對象的初始化 happend-before 於這個對象的析構。

5.3 synchronized 作用,講一講底層實現。

synchronized主要有 3 種使用方式:

  • 修飾實例方法,鎖對象為調用這個方法的實例。
  • 修飾靜態方法,鎖對象為該類的 class。
  • 修飾代碼塊,需要指定鎖對象。

synchronized 底層語義原理:

JVM 中的同步基於進入和退出管程對象來實現的,無論是顯示同步還是隱式都是如此。

Java 對象頭和 monitor

在 JVM 中,一個對象由三部分構成:

  • 實例數據:存放類的的屬性數據信息,包括父類的屬性信息,如果是數組的話實例數據部分還包含數組的長度,這部分內存按 4 字節對齊。

  • 對其填充:由於虛擬機要求對象起始地址必須是 8 字節的整數倍,對於不滿 8 字節整數倍的對象對齊。

  • 對象頭:由於對象頭的信息是與對象自身定義的數據沒有關系的額外存儲成本,因此考慮到虛擬機的空間成本,所以對象頭被設計成一個非固定的結構,以便存儲更多有效的數據。

    img

    在虛擬機中,monitor 是由 ObjectMonitor 實現的,其主要數據結構如下所示:

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //記錄個數
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
    }
    

    ObjectMonitor 中有兩個隊列,_waitSet 和 _EntityList,用來保存 ObjectWaiter 對象列表(每個等待的線程都會被封裝成一個 ObjectWaiter 對象),_owner 指向持有 ObjectMonitor 對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntityList 集合,當獲取到 ObjectWaiter,將 monitor 中的 _owner 設為當前線程,同時 _count++,若線程調用 wait(),將釋放持有 monitor,_owner 重新為 null,_count--,同時進入該線程進入 _WaitSet,如果當前線程執行完畢也會釋放 monitor,復位 _owner,以便其他線程獲取 monitor。

5.4 ReetrantLock 和 synchronized的區別

5.5 說說 synchronized關鍵字和 volatile關鍵字的區別

5.6 ReetrantLock實現方式

5.7 AQS 實現原理

5.8 interrupt、interrupted 與 isInterrupted方法的區別? 如何停止一個正在運行的線程

  • interrupt:將當前線程的狀態設為中斷狀態

  • interrupted:返回當前線程的中斷狀態並清除中斷態

  • isInterrupted:返回當前線程的中斷狀態

如何停止一個正在運行的線程

  • 利用標志位
  • 調用 interrupted,根據標志位由用戶處理
  • 調用 stop() 方法

5.9 線程池作用?Java 線程池有哪些參數?阻塞隊列有幾種?拒絕策略有幾種?線程池的工作機制?

線程池作用

  • 減少創建線程的開銷

  • 提高線程的可管理性

七大參數:

  • corePoolSize:核心線程數
  • maximumPoolSize:最下線程數
  • keepAliveTime:空閑線程保活時間
  • unit:keepAliveTime 的單位
  • workQueue:工作隊列
  • threadFactory:線程工廠
  • handler:拒絕策略

阻塞隊列有哪幾種?

  • ArrayBlockingQueue:基於數組實現的有界阻塞隊列,此隊列按照先進先出原則對元素進行排序

  • LinkedBlockingQueue:基於鏈表實現的有界阻塞隊列,此隊列按照先進先出原則對元素進行排序

  • SynchronousQueue:不存儲元素的阻塞隊列,每次插入操作必須等到另一個線程做移除操作,否則插入操作一直處於阻塞狀態

  • PriorityBlockingQueue:優先級隊列,進入隊列的元素按照優先級進行排序

線程池常見的拒絕策略:

  • AbortPolicy:直接拋棄,並拋出異常
  • CallerRunsPolicy:讓調用者去處理
  • DiscardOldestPolicy:丟棄隊列中最老的任務,然后執行當前提交的任務
  • DiscardPolicy:直接拋棄

線程池的工作機制?

img

常見的線程池有哪幾種?

  • newFixedThreadPool:最大線程數和核心線程數一致,工作隊列采用 LinkedBlockingQueue。
  • newSingleThreadExecutor:最大線程數和核心線程數一致,都是 1,工作隊列采用 LinkedBlockingQueue。
  • newCachedThreadPool:沒有核心線程,直接向 SynchronousQueue 提交任務,如果有空閑線程就執行,否則就新建一個線程,執行完任務的線程有 60s 的存活時間
  • newScheduledThreadPool:最大線程數是 Integer.MAX_VALUE,工作隊列采用 DelayedWorkQueue。

5.10 線程池拒絕策略分別使用在什么場景?

AbortPolicy:沒有特別的場景,默認

DiscardPolicy:提交的任務無關緊要

DiscardOldestPolicy:發布消息和修改消息,如果發布消息任務還未執行,修改消息就過來了,拋棄發布消息,直接執行修改消息

CallerRunsPolicy:不允許失敗,並發量小的場景

5.11 線程死鎖,解除線程死鎖有哪幾種方式?

線程死鎖描述的是這樣一種情況:多個線程同時被阻塞,它們中的一個或者全部都在等待某 個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

解決死鎖的策略

死鎖預防:

  • 破壞保持和等待條件:一次性申請所有資源
  • 破壞不可剝奪條件:當一個進程獲取了某個不可剝奪的條件時,若要提出新的資源申請,申請不到釋放所有資源
  • 破壞循環等待條件:按某一順序申請資源,釋放倒序

死鎖避免:進程每次申請資源時判斷這些操作是否安全

死鎖檢測: 判斷系統是否屬於死鎖的狀態,如果是,則執行死鎖解除策略

死鎖解除:將某進程所占資源進行強制回收,然后分配給其他進程

5.12 ThreadLocal 是什么,應用場景是什么,原理是怎樣的?

ThreadLocal 可以讓每個線程都有自己的本地變量,如果創建了一個 ThreadLocal,那么訪問這個變量的每個線程都會有這個變量的本地副本。可以使用 get()、set() 來獲取值和設置值,從而避免線程安全問題。

ThreadLocal 的值最終是存放到 ThreadLocalMap 中,並不是存到 ThreadLocal。在 ThreadLocal 中存在一個 ThreadLocalMap 內部類,在執行 get() 方法流程就根據當前的 Thread 獲取 ThreadLocalMap,在通過 put 往 ThreadLocal 存放數據的時候,是調用 ThreadLocalMap 的 put() 方法把 ThreadLocal 作為 key,每個線程可以有多個 ThreadLocal 方便存儲多個變量。

ThreadLocalMap 采用二次探測法處理 hash 碰撞。

5.13 ThreadLocal類為什么要加上private static修飾?

5.14 ThreadLocal有什么缺陷?如果線程池的線程使用ThreadLocal會有什么問題?

5.15 介紹一下 Java 有哪些鎖

5.16 樂觀鎖和悲觀鎖講一下,哪些地方用到。

6 虛擬機

6.1 說一下 JVM 的主要組成部分及其作用?

6.1.1 程序計數器

程序計時器是一塊很小的內存空間,可以簡單的認為它是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時改變這個計時器的值來獲取下一條需要執行的指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計時器來完成。

為了線程切換后能恢復到正確的位置,每條線程都需一個獨立的線程計數器,各線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為線程私有區域。

注意:程序計數器是唯一一個不會產生 OutOfMemoryError 的內存區域,它的生命周期隨着線程創建而創建,隨着線程消亡而消亡。

6.1.2 Java 虛擬機棧

與程序計數器一樣,虛擬機棧也是線程私有的內存區域,它的生命周期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。

Java 內存區域可以粗略的分為堆和棧,其中棧就是現在說的 Java 虛擬機棧(實際上棧是由一個個的棧幀組成,棧幀包括:局部變量表、操作數棧、動態鏈接、方法出口等信息)。

局部變量表主要存放了編譯期可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(refence 類型,它不同於對象本身,可能是一個指向對象起始位置的引用指針,也可能是指向一個代表對象的句柄)。

Java 虛擬機會出現的兩種錯誤:StackOverFlowError 和 OutOfMemoryError

  • StackOverFlowError:若虛擬機棧的內存大小不允許動態擴展,那么當前線程請求深度超過當前虛擬機棧的最大深度的時候,就會拋出 StackOverFlowError
  • OutOfMemoryError:若虛擬機棧中沒有空閑內存,並且垃圾回收期也無法提供更多內存的話,就會拋出 OutOfMemoryError。

那么方法、函數如何調用呢?

虛擬機棧中保存的主要內容是棧幀,每一次函數調用都會對應着棧幀的被壓入棧,每一個函數調用結束,都會有一個棧幀被彈出。

Java 方法有兩種返回方式:return 和 拋出異常。

不管哪種方式都會導致棧幀被彈出。

6.1.3 本地方法棧

和虛擬機棧發揮的作用相同,只不過兩者服務的對象的不同,虛擬機棧服務的對象是 Java 方法,本地方法棧服務的對象是虛擬機使用到的 Native 方法。

6.1.4 堆

Java 虛擬機所管理的內存中最大的一塊,堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。但是,隨着 JIT 編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致所有對象都分配到堆上也漸漸變得不那么絕對了。從 JDK1.7 開始默認開啟逃逸分析,如果某些方法的對象引用沒有被返回到外部,那么對象可以直接從棧上分配。

Java 堆是垃圾收集器管理的主要區域,因此也被稱為 GC 堆。現代收集器都基本采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代;再細致還可以分為:Eden、From Survivor、To Survivor 等。進一步划分的目的是更好的進行垃圾回收,或者更快的分配內存。

JVM堆內存結構-JDK8

對象分配的流程:

大部分情況,對象首先從 Eden 分配,在一次垃圾回收后,如果對象還存活,會進入 s0 或 s1,而且對象的年齡還會增加 1,當它的年齡增加到一定程度(默認是 15,Hotspot 遍歷所有對象時,按照年齡分別對其所占用的大小進行統計,如果某個年齡段的對象總和超過了 survivor 的一半,取這個年齡和 MaxTenuringThreshold 對比,取較小的那個作為新的晉升年齡閾值),就會晉升到老年代。

6.1.5 方法區

方法區和 Java 堆一樣,是各個線程共享的區域,它同於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。方法區是 Java 虛擬機規范描述的一個邏輯部分。

方法區和永久代的關系:

方法區是 Java 虛擬機規范中的定義,永久代是 Hotspot 虛擬機對虛擬機規范中方法區的一個具體實現。

常用參數:

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小

  • -XX:PermSize=N:方法區 (永久代) 初始大小
  • -XX:MaxPermSize=N:方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了,取而代之是元空間,元空間使用的是直接內存

  • -XX:MetaspaceSize=N:設置 Metaspace 的初始和最小大小
  • -XX:MaxMetaspaceSize=N:設置 Metaspace 的最大大小

為什么要講永久代替換為元空間?

  1. 整個永久代有一個 JVM 設置固定的大小上限,無法進行調整,而元空間使用的是直接內存,受本機內存的限制,雖然還可以發生溢出但是幾率會更小;
  2. 元空間里面存放的是類的元數據,這樣加載多少類的元數據就不會受 PermSize 的限制了,能加載更多類的元數據。
6.1.6 運行時常量池

運行時常量池也是方法區的一部分,用來存放運行期間產生的新的常量。JDK 1.7 之前的運行時常量池包括字符串常量池存放在方法區,此時 Hotspot 虛擬機對方法區的實現為永久代;JDK 1.7 字符串常量池被從方法區拿到了堆中,JDK 1.8 取消了永久代,這時字符串常量池還在堆中,運行時常量池被拿到了元空間中。

6.1.7 直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也有可能導致 OOM。

JDK 1.4 中新加入的 NIO 類,引入了一種基於通道與緩沖區的 I/O 方式,它可以直接使用 Native 函數庫分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的的引用進行操作。這樣就能在一些場景中顯著提高性能,避免了在 Java 堆和 Native 堆之間來回復制數據。

6.2 說一下堆和棧的區別?

6.3 HotSpot虛擬機對象探秘

6.3.1 對象的創建

Step1:類加載檢查

虛擬機遇到一條 new 指令時,首先去檢查能否在常量池中定位到這類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

Step2:分配內存

在類加載檢查通過后,虛擬機就可以為新生對象分配內存。對象所需大小在類加載通過即可確定,為對象分配內存等同於把一塊確定大小的內存空間從堆中划分出來。分配方式有指針碰撞和空閑列表兩種,選擇哪種方式由堆是否規整決定,堆是否規整由所采用的垃圾收集器是否帶有壓縮整理決定。

內存分配的兩種方式

內存分配的並發問題?

在創建對象的時候有一個很重要的問題,就是線程安全,在實際開發中,創建對象是很繁瑣的問題,作為虛擬機來說,必須保證線程安全,通常虛擬機會采用以下兩種方式來保證線程安全:

  • CAS+失敗重試:CAS 是樂觀鎖的一種實現。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS+失敗重試來保證更新操作的原子性。
  • TLAB:為每個線程在 Eden 區分配一塊內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大於 TLAB 的剩余空間或者 TLAB 的空間用盡后,再采用 CAS+失敗重試的方式。

Step3:初始化零值

內存分配完畢后,虛擬機需要將分配好的內存空間都初始化為零值,這一步操作保證了對象的實例字段在 Java 代碼中可以不賦值直接使用。

Step4:設置對象頭

初始化零值完成后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。這些信息都存放在對象頭中,另外,虛擬機當前運行狀態的不同,如是否采用偏向鎖,對象頭也會有不同的設置方式。

Step5:執行 init 方法

在上面的工作都完成后,從虛擬機來說這個對象已經創建完成了,但從 Java 程序的角度來說,對象創建才剛剛開始,init 方法還沒有執行,所有的字段都還是零,所有一般來說執行完 new 指令后執行 init 方法,按照程序的設置進行對象的初始化操作。

6.3.2 對象的內存布局

在 Hotspot 虛擬機中,對象在內存中的布局可以分為 3 塊區域:對象頭、實例數據和對齊填充:

Hotspot 虛擬機的對象頭包括兩部分信息,第一部分是用戶存儲對象自身運行時數據(哈希碼、GC 分代年齡、鎖標志等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

實例數據是對象真正存儲的有效信息,是程序中定義的各種類型的字段內容。

對齊填充部分不是必須存在的,也沒用什么特殊含義,僅僅起占位的作用。因為 Hotspot 虛擬機自動內存管理系統要求對象的起始地址必須是 8 字節的整數倍,也就是對象的大小必須是 8 字節的整數倍,因此對象的大小不夠 8 字節的整數倍時,通過這部分來補齊。

6.3.3 對象的訪問定位

Java 通過棧的引用來操作堆中的對象。對象的訪問方式由虛擬機的實現方式來確定,目前主流的訪問方式有使用句柄和直接指針:

6.4 說一下類裝載的執行過程?

6.5 什么是雙親委派機制?

6.6 垃圾回收時如何確定垃圾?

6.7 什么是 GC Roots?

6.8 談談對 OOM 的理解?

6.9 說一下 JVM 有哪些垃圾回收算法?

6.10 說一下 JVM 有哪些垃圾回收器?

6.11 詳細介紹一下 CMS 垃圾回收器?

6.12 詳細介紹一下 CMS 垃圾回收器?

6.13 談談內存分配策略?

6.14 為什么要采用分代收集?

7 MySQL

7.1 數據庫三大范式?

邏輯架構

7.2 MySQL存儲引擎MyISAM與InnoDB區別

什么是數據庫事務

數據庫的四大特性

原子 一致 隔離 持久

什么是臟讀?幻讀?不可重復讀?

什么是事務的隔離級別?默認的隔離級別是什么?

讀未提交 讀已提交 可重復讀 串行化

多版本並發控制

trx_id

roll_pointer 老版本寫入 undo 日志

readview

什么是最左前綴原則?什么是最左匹配原則?

B 和 B+ 區別?

為什么 InnoDB 選用 B+ 樹?

B 樹的每個節點都存儲數據,而 B+ 樹只有葉子節點才存儲數據,所以在查詢相同數據量的情況下,B 樹的高度更高,IO 更頻繁。數據索引是存儲在磁盤上的,當數據量大時,就不能把整個索引全部加載到內存了,只能逐一加載每個磁盤頁。

什么是聚簇索引?什么是非聚簇索引?

聚簇索引的葉子節點就是數據節點,非聚簇索引的葉子節點還是索引節點。

什么是覆蓋索引?

如果一個索引包含了查詢語句中的條件和字段的數據叫做覆蓋索引,具有以下優點:

  • 索引通常遠遠小於數據行,只讀取索引能減少數據訪問量
  • 一些存儲引擎在內存中只緩存索引,而數據依賴於操作系統來緩存
  • 對於 InnoDB 引擎,若輔助索引能覆蓋查詢,則無需訪問主鍵索引

什么是索引回表?

什么是索引下堆?

在索引遍歷過程中,對索引中包含的字段先做判斷,過濾不符合記錄的記錄,減少回表的次數

主從復制的原理?

8 Redis

Redis 是單進程的嗎?

Redis 為什么這么快?

常用的數據結構和應用場景?

Redis6.0 之后為何引入了多線程?

Redis是如何判斷數據是否過期的呢?

過期的數據的刪除策略了解么?

Redis 內存淘汰機制了解么?

Redis 持久化機制

Redis 事務

緩存穿透、緩存擊穿、緩存雪崩的區別及解決方案?

布隆過濾器用過嗎?講講?

9 Spring

談談自己對於 IOC 和 AOP 的理解

Bean 的生命周期?

BeanFactory、FactoryBean 和 ApplicationContext 的區別?

Spring 事務中哪幾種事務傳播行為?

@Transactional(rollbackFor = Exception.class)注解了解嗎?

Spring MVC 的處理流程?

Spring Boot 自動配置原理?

10 MyBatis

#{} 和 ${} 的區別是什么?

通常一個 Xml 映射文件,都會寫一個 Dao 接口與之對應,請問,這個 Dao 接口的工作原理是什么?Dao 接口里的方法,參數不同時,方法能重載嗎?

MyBatis 是如何進行分頁的?分頁插件的原理是什么?

一級緩存和二級緩存?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM