阿里面試題若干 2020

JVM內存模型及分區

Java虛擬機在程序執行過程會把jvm的內存分為若干個不同的數據區域來管理,這些區域有自己的用途,以及創建和銷毀時間。
jvm管理的內存區域包括以下幾個區域:

image

棧區
棧分為java虛擬機棧和本地方法棧
重點是Java虛擬機棧,它是線程私有的,生命周期與線程相同。
每個方法執行都會創建一個棧幀,用于存放局部變量表,操作棧,動態鏈接,方法出口等。每個方法從被調用,直到被執行完。對應著一個棧幀在虛擬機中從入棧到出棧的過程。
通常說的棧就是指局部變量表部分,存放編譯期間可知的8種基本數據類型,及對象引用和指令地址。局部變量表是在編譯期間完成分配,當進入一個方法時,這個棧中的局部變量分配內存大小是確定的。
會有兩種異常StackOverFlowError和 OutOfMemoneyError。當線程請求棧深度大于虛擬機所允許的深度就會拋出StackOverFlowError錯誤;虛擬機棧動態擴展,當擴展無法申請到足夠的內存空間時候,拋出OutOfMemoneyError。
本地方法棧 為虛擬機使用到本地方法服務(native)
堆區
堆被所有線程共享區域,在虛擬機啟動時創建,唯一目的存放對象實例。
堆區是gc的主要區域,通常情況下分為兩個區塊年輕代和年老代。更細一點年輕代又分為Eden區最要放新創建對象,From survivor 和 To survivor 保存gc后幸存下的對象,默認情況下各自占比 8:1:1。
不過很多文章介紹分為3個區塊,把方法區算著為永久代。這大概是基于Hotspot虛擬機劃分, 然后比如IBM j9就不存在永久代概論。不管怎么分區,都是存放對象實例。
會有異常OutOfMemoneyError
方法區
被所有線程共享區域,用于存放已被虛擬機加載的類信息,常量,靜態變量等數據。被Java虛擬機描述為堆的一個邏輯部分。習慣是也叫它永久代(permanment generation)
垃圾回收很少光顧這個區域,不過也是需要回收的,主要針對常量池回收,類型卸載。
常量池用于存放編譯期生成的各種字節碼和符號引用,常量池具有一定的動態性,里面可以存放編譯期生成的常量;運行期間的常量也可以添加進入常量池中,比如string的intern()方法。
程序計數器
當前線程所執行的行號指示器。通過改變計數器的值來確定下一條指令,比如循環,分支,跳轉,異常處理,線程恢復等都是依賴計數器來完成。
Java虛擬機多線程是通過線程輪流切換并分配處理器執行時間的方式實現的。為了線程切換能恢復到正確的位置,每條線程都需要一個獨立的程序計數器,所以它是線程私有的。
唯一一塊Java虛擬機沒有規定任何OutofMemoryError的區塊
jvm分區大致就這個塊,具體里面還有很多細節,及其各個模塊工作的算法都很復雜,這里只是對分區進行簡單介紹,掌握一些基本的知識點。


GC可達性,哪些可以作為GCRoots

可達性分析算法的思想:從一個被稱為GC Roots的對象開始向下搜索,如果一個對象到GC Roots沒有任何引用鏈相連時,則說明此對象不可用。
在java中可以作為GC Roots的對象有以下幾種:
虛擬機棧中引用的對象、方法區類靜態屬性引用的對象、方法區常量池引用的對象、本地方法棧JNI引用的對象。

雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象 不一定會被回收。當一個對象不可達GC Roots時,這個對象并不會馬上被回收,而是處于一個死緩的階段,若要被真正的回收需要經歷兩次標記。如果對象在可達性分析中沒有與GC Roots的引用鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者已經被虛擬機調用過,那么就認為是沒必要的。

如果該對象有必要執行finalize()方法,那么這個對象將會放在一個稱為F-Queue的隊列中,虛擬機會觸發一個finalize()線程去執行,此線程是低優先級的,并且虛擬機不會承諾一直等待它運行完,這還是因為如果finalize()執行緩慢或者發生了死鎖,那么就會造成F-Queue隊列一直等待,造成了內存回收系統的崩潰。GC對處于F-Queue中的對象進行第二次被標記,這時,該對象將被移除“即將回收”集合,等待回收。


GC管理的主要區域是Java堆,一般情況下只針對堆進行垃圾回收。

方法區、棧和本地方法區不被GC所管理,因而選擇這些區域內的對象作為GC roots,被GC roots引用的對象不被GC回收。

詳細:
GC Root常說的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的對象,GC會收集那些不是GC roots且沒有被GC roots引用的對象。
一個對象可以屬于多個root,GC root有幾下種:
Class - 由系統類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態字段的方式保存持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應的java.lang.Class實例以其它的某種(或多種)方式成為roots,否則它們并不是roots。
Thread - 活著的線程
Stack Local - Java方法的local變量或參數
JNI Local - JNI方法的local變量或參數
JNI Global - 全局JNI引用
Monitor Used - 用于同步的監控對象
Held by JVM - 用于JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現是有關的。可能已知的一些類型是:系統類加載器、一些JVM知道的重要的異常類、一些用于處理異常的預分配對象以及一些自定義的類加載器等。然而,JVM并沒有為這些對象提供其它的信息,因此需要去確定哪些是屬于"JVM持有"的了。

常用JVM參數

JVM 參數設置大全

JVM類加載機制(類加載器的雙親委派加載機制)

JVM類加載機制詳解(一)JVM類加載過程
JVM類加載機制詳解(二)類加載器與雙親委派模型

Java線程安全的集合

一、早期線程安全的集合
我們先從早期的線程安全的集合說起,它們是Vector和HashTable

1.Vector
Vector和ArrayList類似,是長度可變的數組,與ArrayList不同的是,Vector是線程安全的,它給幾乎所有的public方法都加上了synchronized關鍵字。由于加鎖導致性能降低,在不需要并發訪問同一對象時,這種強制性的同步機制就顯得多余,所以現在Vector已被棄用

2.HashTable
HashTable和HashMap類似,不同點是HashTable是線程安全的,它給幾乎所有public方法都加上了synchronized關鍵字,還有一個不同點是HashTable的K,V都不能是null,但HashMap可以,它現在也因為性能原因被棄用了

二、Collections包裝方法
Vector和HashTable被棄用后,它們被ArrayList和HashMap代替,但它們不是線程安全的,所以Collections工具類中提供了相應的包裝方法把它們包裝成線程安全的集合

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

Collections針對每種集合都聲明了一個線程安全的包裝類,在原集合的基礎上添加了鎖對象,集合中的每個方法都通過這個鎖對象實現同步

三、java.util.concurrent包中的集合
1.ConcurrentHashMap
ConcurrentHashMap和HashTable都是線程安全的集合,它們的不同主要是加鎖粒度上的不同。HashTable的加鎖方法是給每個方法加上synchronized關鍵字,這樣鎖住的是整個Table對象。而ConcurrentHashMap是更細粒度的加鎖
在JDK1.8之前,ConcurrentHashMap加的是分段鎖,也就是Segment鎖,每個Segment含有整個table的一部分,這樣不同分段之間的并發操作就互不影響
JDK1.8對此做了進一步的改進,它取消了Segment字段,直接在table元素上加鎖,實現對每一行進行加鎖,進一步減小了并發沖突的概率

2.CopyOnWriteArrayList和CopyOnWriteArraySet
它們是加了寫鎖的ArrayList和ArraySet,鎖住的是整個對象,但讀操作可以并發執行

3.除此之外還有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于為什么沒有ConcurrentArrayList,原因是無法設計一個通用的而且可以規避ArrayList的并發瓶頸的線程安全的集合類,只能鎖住整個list,這用Collections里的包裝類就能辦到

常用線程池參數

線程池參數詳解

Redis過期策略 實現原理

我們在使用redis時,一般會設置一個過期時間,當然也有不設置過期時間的,也就是永久不過期。

當我們設置了過期時間,redis是如何判斷是否過期,以及根據什么策略來進行刪除的。

1.redis設置過期時間:

expire key time(以秒為單位)--這是最常用的方式
setex(String key, int seconds, String value)--字符串獨有的方式
注:
除了字符串自己獨有設置過期時間的方法外,其他方法都需要依靠expire方法來設置時間
如果沒有設置時間,那緩存就是永不過期
如果設置了過期時間,之后又想讓緩存永不過期,使用persist key

2.三種過期策略:

a.定時刪除
含義:在設置key的過期時間的同時,為該key創建一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除
優點:保證內存被盡快釋放
缺點:若過期key很多,刪除這些key會占用很多的CPU時間,在CPU時間緊張的情況下,CPU不能把所有的時間用來做要緊的事兒,還需要去花時間刪除這些key.定時器的創建耗時,若為每一個設置過期時間的key創建一個定時器(將會有大量的定時器產生),性能影響嚴重
b.懶漢式刪除
含義:key過期的時候不刪除,每次通過key獲取值的時候去檢查是否過期,若過期,則刪除,返回null。
優點:刪除操作只發生在通過key取值的時候發生,而且只刪除當前key,所以對CPU時間的占用是比較少的,而且此時的刪除是已經到了非做不可的地步(如果此時還不刪除的話,我們就會獲取到了已經過期的key了)
缺點:若大量的key在超出超時時間后,很久一段時間內,都沒有被獲取過,那么可能發生內存泄露(無用的垃圾占用了大量的內存)
c.定期刪除
含義:每隔一段時間執行一次刪除過期key操作
優點:通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的占用(處理"定時刪除"的缺點),定期刪除過期key(處理"懶漢式刪除"的缺點)
缺點:在內存友好方面,不如"定時刪除"(會造成一定的內存占用,但是沒有懶漢式那么占用內存)在CPU時間友好方面,不如"懶漢式刪除"(會定期的去進行比較和刪除操作,cpu方面不如懶漢式,但是比定時好)
難點:合理設置刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除)(這個要根據服務器運行情況來定了),每次執行時間太長,或者執行頻率太高對cpu都是一種壓力。
每次進行定期刪除操作執行之后,需要記錄遍歷循環到了哪個標志位,以便下一次定期時間來時,從上次位置開始進行循環遍歷

memcached只是用了惰性刪除,而redis同時使用了惰性刪除與定期刪除,這也是二者的一個不同點(可以看做是redis優于memcached的一點);

對于懶漢式刪除而言,并不是只有獲取key的時候才會檢查key是否過期,在某些設置key的方法上也會檢查(eg.setnx key2 value2:該方法類似于memcached的add方法,如果設置的key2已經存在,那么該方法返回false,什么都不做;如果設置的key2不存在,那么該方法設置緩存key2-value2。假設調用此方法的時候,發現redis中已經存在了key2,但是該key2已經過期了,如果此時不執行刪除操作的話,setnx方法將會直接返回false,也就是說此時并沒有重新設置key2-value2成功,所以對于一定要在setnx執行之前,對key2進行過期檢查)。

3.Redis采用的過期策略

懶漢式刪除+定期刪除

懶漢式刪除流程:

在進行get或setnx等操作時,先檢查key是否過期;
若過期,刪除key,然后執行相應操作;
若沒過期,直接執行相應操作;
定期刪除流程(簡單而言,對指定個數個庫的每一個庫隨機刪除小于等于指定個數個過期key):

遍歷每個數據庫(就是redis.conf中配置的"database"數量,默認為16)
檢查當前庫中的指定個數個key(默認是每個庫檢查20個key,注意相當于該循環執行20次,循環體是下邊的描述)
如果當前庫中沒有一個key設置了過期時間,直接執行下一個庫的遍歷
隨機獲取一個設置了過期時間的key,檢查該key是否過期,如果過期,刪除key
判斷定期刪除操作是否已經達到指定時長,若已經達到,直接退出定期刪除。

對于定期刪除,在程序中有一個全局變量current_db來記錄下一個將要遍歷的庫,假設有16個庫,我們這一次定期刪除遍歷了10個,那此時的current_db就是11,下一次定期刪除就從第11個庫開始遍歷,假設current_db等于15了,那么之后遍歷就再從0號庫開始(此時current_db==0)

在實際中,如果我們要自己設計過期策略,在使用懶漢式刪除+定期刪除時,控制時長和頻率這個尤為關鍵,需要結合服務器性能,已經并發量等情況進行調整,以致最佳。

Redis單線程速度快的原因

1、完全基于內存,絕大部分請求是純粹的內存操作,非??焖?。數據存在內存中,類似于HashMap,HashMap的優勢就是查找和操作的時間復雜度都是O(1);

2、數據結構簡單,對數據操作也簡單,Redis中的數據結構是專門進行設計的;

3、采用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗;

4、使用多路I/O復用模型,非阻塞IO;

5、使用底層模型不同,它們之間底層實現方式以及與客戶端之間通信的應用協議不一樣,Redis直接自己構建了VM 機制 ,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求;

以上幾點都比較好理解,下邊我們針對多路 I/O 復用模型進行簡單的探討:

(1)多路 I/O 復用模型

多路I/O復用模型是利用 select、poll、epoll 可以同時監察多個流的 I/O 事件的能力,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有 I/O 事件時,就從阻塞態中喚醒,于是程序就會輪詢一遍所有的流(epoll 是只輪詢那些真正發出了事件的流),并且只依次順序的處理就緒的流,這種做法就避免了大量的無用操作。

這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡 IO 的時間消耗),且 Redis 在內存中操作數據的速度非???,也就是說內存內的操作不會成為影響Redis性能的瓶頸,主要由以上幾點造就了 Redis 具有很高的吞吐量。

因為Redis是基于內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成為瓶頸,那就順理成章地采用單線程的方案了(畢竟采用多線程會有很多麻煩!)。

MySQL索引失效的問題

1.索引不存儲null值
更準確的說,單列索引不存儲null值,復合索引不存儲全為null的值。索引不能存儲Null,所以對這列采用is null條件時,因為索引上根本沒Null值,不能利用到索引,只能全表掃描。
為什么索引列不能存Null值?
將索引列值進行建樹,其中必然涉及到諸多的比較操作。Null值的特殊性就在于參與的運算大多取值為null。
這樣的話,null值實際上是不能參與進建索引的過程。也就是說,null值不會像其他取值一樣出現在索引樹的葉子節點上。

2.不適合鍵值較少的列(重復數據較多的列)
假如索引列TYPE有5個鍵值,如果有1萬條數據,那么 WHERE TYPE = 1將訪問表中的2000個數據塊。再加上訪問索引塊,一共要訪問大于200個的數據塊。
如果全表掃描,假設10條數據一個數據塊,那么只需訪問1000個數據塊,既然全表掃描訪問的數據塊
少一些,肯定就不會利用索引了。

3.前導模糊查詢不能利用索引(like '%XX'或者like '%XX%')
假如有這樣一列code的值為'AAA','AAB','BAA','BAB' ,如果where code like '%AB'條件,由于前面是模糊的,所以不能利用索引的順序,必須一個個去找,看是否滿足條件。這樣會導致全索引掃描或者全表掃描。如果是這樣的條件where code like 'A % ',就可以查找CODE中A開頭的CODE的位置,當碰到B開頭的數據時,就可以停止查找了,因為后面的數據一定不滿足要求。這樣就可以利用索引了。

4.索引失效的幾種情況

  • 如果條件中有or,即使其中有條件帶索引也不會使用(這也是為什么盡量少用or的原因)
    要想使用or,又想讓索引生效,只能將or條件中的每個列都加上索引
  • 對于多列索引,不是使用的第一部分,則不會使用索引
  • like查詢以%開頭
  • 如果列類型是字符串,那一定要在條件中將數據使用引號引用起來,否則不使用索引
  • 如果mysql估計使用全表掃描要比使用索引快,則不使用索引

5.MySQL主要提供2種方式的索引:B-Tree索引,Hash索引
B樹索引具有范圍查找和前綴查找的能力,對于有N節點的B樹,檢索一條記錄的復雜度為O(LogN)。相當于二分查找。
哈希索引只能做等于查找,但是無論多大的Hash表,查找復雜度都是O(1)。
顯然,如果值的差異性大,并且以等值查找(=、 <、>、in)為主,Hash索引是更高效的選擇,它有O(1)的查找復雜度。
如果值的差異性相對較差,并且以范圍查找為主,B樹是更好的選擇,它支持范圍查找。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數? 在 Jav...
    侯蛋蛋_閱讀 2,482評論 1 4
  • 包含的重點內容:JAVA基礎JVM 知識開源框架知識操作系統多線程TCP 與 HTTP架構設計與分布式算法數據庫知...
    消失er閱讀 4,354評論 1 10
  • 一 基礎篇 1.1 Java基礎 面向對象的特征抽象:將一類對象的共同特征總結出來構建類的過程。繼承:對已有類的一...
    essential_note閱讀 705評論 0 0
  • Java SE 基礎: 封裝、繼承、多態 封裝: 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,130評論 0 8
  • 第二部分 自動內存管理機制 第二章 java內存異常與內存溢出異常 運行數據區域 程序計數器:當前線程所執行的字節...
    小明oh閱讀 1,204評論 0 2