您好,登錄后才能下訂單哦!
首先,java代碼會被編譯成字節(jié)碼,字節(jié)碼就是java虛擬機定義的一種編碼格式,需要java虛擬機才能夠解析,java虛擬機需要將字節(jié)碼轉(zhuǎn)換成機器碼才能在cpu上執(zhí)行。 我們可以用硬件實現(xiàn)虛擬機,這樣雖然可以提高效率但是就沒有了一次編譯到處運行的特性了,所以一般在各個平臺上用軟件來實現(xiàn),目前的虛擬機還提供了一套運行環(huán)境來進行垃圾回收,數(shù)組越界檢查,權(quán)限校驗等。虛擬機一般將一行字節(jié)碼解釋成機器碼然后執(zhí)行,稱為解釋執(zhí)行,也可以將一個方法內(nèi)的所有字節(jié)碼解釋成機器碼之后在執(zhí)行,前者執(zhí)行效率低,后者會導致啟動時間慢,一般根據(jù)二八法則,將百分之20的熱點代碼進行即時編譯。JIT編譯的機器碼存放在一個叫codecache的地方,這塊內(nèi)存屬于堆外內(nèi)存,如果這塊內(nèi)存不夠了,那么JIT編譯器將不再進行即時編譯,可能導致程序運行變慢。
第一步:加載,雙親委派:啟動類加載器(jre/lib),系統(tǒng)擴展類加載器(ext/lib),應用類加載器(classpath),前者為c++編寫,所以系統(tǒng)加載器的parent為空,后面兩個類加載器都是通過啟動類加載器加載完成后才能使用。加載的過程就是查找字節(jié)流,可以通過網(wǎng)絡(luò),也可以自己在代碼生成,也可以來源一個jar包。另外,同一個類,被不同的類加載器加載,那么他們將不是同一個類,java中通過類加載器和類的名稱來界定唯一,所以我們可以在一個應用成存在多個同名的類的不同實現(xiàn)。
第二步:鏈接:(驗證,準備,解析) 驗證主要是校驗字節(jié)碼是否符合約束條件,一般在字節(jié)碼注入的時候關(guān)注的比較多。準備:給靜態(tài)字段分配內(nèi)存,但是不會初始化,解析主要是為了將符號引用轉(zhuǎn)換為實際引用,可能會觸發(fā)方法中引用的類的加載。
第三步:初始化,如果賦值的靜態(tài)變量是基礎(chǔ)類型或者字符串并且是final的話,該字段將被標記為常量池字段,另外靜態(tài)變量的賦值和靜態(tài)代碼塊,將被放在一個叫cinit的方法內(nèi)被執(zhí)行,為了保證cinit方法只會被執(zhí)行一次,這個方法會加鎖,我們一般實現(xiàn)單例模式的時候為保證線程安全,會利用類的初始化上的鎖。 初始化只有在特定條件下才會被觸發(fā),例如new 一個對象,反射被調(diào)用,靜態(tài)方法被調(diào)用等。
java中每一個非基本類型的對象,都會有一個對象頭,對象頭中有64位作為標記字段,存儲對象的哈希碼,gc信息,鎖信息,另外64位存儲class對象的引用指針,如果開啟指針壓縮的話,該指針只需要占用32位字節(jié)。
Java對象中的字段,會進行重排序,主要為了保證內(nèi)存對齊,使其占用的空間正好是8的倍數(shù),不足8的倍數(shù)會進行填充,所以想知道一個屬性相對對象其始地址的偏移量需要通過unsafe里的fieldOffset方法,內(nèi)存對齊也為了避免讓一個屬性存放在兩個緩存行中,disruptor中為了保證一個緩存行只能被一個屬性占用,也會用空對象進行填充,因為如果和其他對象公用一個緩存行,其他對象的失效會將整個緩存行失效,影響性能開銷,jdk8中引入了contended注解來讓一個屬性獨占一個緩存行,內(nèi)部也是進行填充,用空間換取時間,如何計算一個對象占用多少內(nèi)存,如果不精確的話就進行遍歷然后加上對象頭,這種情況沒辦法考慮重排序和填充,如果精確的話只能通過javaagent的instrument工具。
反射真的慢么?
首先class.forname和class.getmethod 第一個是一個native方法,第二個會遍歷自己和父類中的方法,并返回方法的一個拷貝,所以這兩個方法性能都不好,建議在應用層進行緩存。 而反射的具體調(diào)用有兩種方式,一種是調(diào)用本地native方法,一種是通過動態(tài)字節(jié)碼生成一個類來調(diào)用,默認采用第一種,當被調(diào)用15次之后,采用第二種動態(tài)字節(jié)碼方式,因為生成字節(jié)碼也耗時,如果只調(diào)用幾次沒必要,而第一種方式由于需要在java和c++之間切換,native 方法本身性能消耗嚴重,所以對于熱點代碼頻繁調(diào)用反射的話,性能并不會很差。
屬性的反射,采用unsafe類中setvalue來實現(xiàn),需要傳入該屬性相對于對象其始地址的偏移量,也就是直接操作內(nèi)存。其實就是根據(jù)這個屬性在內(nèi)存中的起始地址和類型來讀取一個字段的值,在LockSupport類中,park和unpark方法,設(shè)置誰將線程掛起的時候也有用到這種方式。
java本身的動態(tài)代理也是通過字節(jié)碼實現(xiàn)的
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
工具類中需要提供 類加載器,需要實現(xiàn)的接口,攔截器的實現(xiàn),也就是需要在InvocationHandler中調(diào)用原方法并做增強處理。并且這個實現(xiàn),一定會被放到新生成的動態(tài)代理類里。
生成動態(tài)代理類的步驟:先通過聲明的接口生成一個byte數(shù)組,這個數(shù)組就是字節(jié)流,通過傳入的類加載進行加載生成一個class對象,這個class 里面有個構(gòu)造方法接收一個參數(shù),這個參數(shù)就是InvocationHandler,通過這個構(gòu)造方法的反射獲取一個實例類,在這個class里面,接口的實現(xiàn)中會調(diào)用InvocationHandler,而這個class對象為了防止生成太多又沒有被回收,所以是一個弱引用對象。
并發(fā)問題的根源:可見性,原子性,亂序執(zhí)行
java內(nèi)存模型定義了一些規(guī)則來禁止cpu緩存和編譯器優(yōu)化,happen-before用來描述兩個操作的內(nèi)存的可見性,有以下6條
1.程序的順序執(zhí)行,前一個語句對后一個語句可見 (當兩個語句沒有依賴的情況下還是可以亂序執(zhí)行)
2.volatile變量的寫對另一個線程的讀可見
3.happen-before 具有傳遞性
4.一個線程對鎖的釋放對另外一個線程的獲取鎖可見 (也就是一個線程在釋放鎖之前對共享變量的操作,另外一個線程獲取鎖后會看的到)
5.線程a調(diào)用了線程b的start()方法,那么線程a在調(diào)用start方法之前的操作,對線程b內(nèi)的run()方法可見
6.線程a調(diào)用了線程b的join方法,那么線程b里的所有操作,將對線程a調(diào)用join之后的操作可見。
兩種實現(xiàn):引用計數(shù)和可達性分析,引用計數(shù)會出現(xiàn)循環(huán)引用的問題,目前一般采用可達性分析。
為了保證程序運行線程和垃圾回收線程不會發(fā)生并發(fā)影響,jvm采用安全點機制來實現(xiàn)stop the world,也就是當垃圾收集線程發(fā)起stop the world請求后,工作線程開始進行安全點檢測,只有當所有線程都進入安全點之后,垃圾收集線程才開始工作,在垃圾收集線程工作過程中,工作線程每執(zhí)行一行代碼都會進行安全點檢測,如果這行代碼安全就繼續(xù)執(zhí)行,如果這行代碼不安全就將該線程掛起,這樣可以保證垃圾收集線程運行過程中,工作線程也可以繼續(xù)執(zhí)行。
安全點:例如阻塞線程肯定是安全點,運行的jni線程如果不訪問java對象也是安全的,如果線程正在編譯生成機器碼那他也是安全的,Java虛擬機在有垃圾回收線程執(zhí)行期間,每執(zhí)行一個字節(jié)碼都會進行安全檢測。
基礎(chǔ)垃圾收集算法:清除算法會造成垃圾碎片,清除后整理壓縮浪費cpu耗時,復制算法浪費內(nèi)存。
基礎(chǔ)假設(shè):大部分的java對象只存活了一小段時間,只有少部分java對象存活很久。新建的對象放到新生代,當經(jīng)過多次垃圾回收還存在的,就把它移動到老年代。針對不同的區(qū)域采用不同的算法。因為新生代的對象存活周期很短,經(jīng)常需要垃圾回收,所以需要采用速度最快的算法,也就是復制,所以新生代會分成兩塊。一塊eden區(qū),兩塊大小相同的survivor區(qū)。
新的對象默認在eden區(qū)進行分配,由于堆空間是共享的,所以分配內(nèi)存需要加鎖同步,不然會出現(xiàn)兩個對象指向同一塊內(nèi)存,為了避免頻繁的加鎖,一個線程可以申請一塊連續(xù)內(nèi)存,后續(xù)內(nèi)存的分配就在這里進行,這個方案稱為tlab。tlab里面維護兩個指針,一個是當前空余內(nèi)存起始位置,另外一個tail指向尾巴申請的內(nèi)存結(jié)束位置,分配內(nèi)存的時候只需要進行指針加法并判斷是否大于tail,如果超過則需要重新申請tlab。
如果eden區(qū)滿了則會進行一次minorGc ,將eden區(qū)的存活對象和from區(qū)的對象移動到to區(qū),然后交換from和to的指針。
垃圾收集器的分類:針對的區(qū)域,老年代還是新生代,串行還是并行,采用的算法分類復制還是標記整理
g1 基于可控的停頓時間,增加吞吐量,取代cms g1將內(nèi)存分為多個塊,每個塊都可能是 eden survivor old 三種之一 首先清除全是垃圾的快 這樣可以快速釋放內(nèi)存。
如果發(fā)現(xiàn)JVM經(jīng)常進行full gc 怎么排查?
不停的進行full gc表示可能老年代對象占有大小超過閾值,并且經(jīng)過多次full gc還是沒有降到閾值以下,所以猜測可能老年代里有大量的數(shù)據(jù)存活了很久,可能是出現(xiàn)了內(nèi)存泄露,也可能是緩存了大量的數(shù)據(jù)一直沒有釋放,我們可以用jmap將gc日志dump下來,分析下哪些對象的實例個數(shù)很多,以及哪些對象占用空間最多,然后結(jié)合代碼進行分析。
線程的狀態(tài)機
線程池參數(shù):核心線程數(shù),最大線程數(shù),線程工廠,線程空閑時間,任務(wù)隊列,拒絕策略 先創(chuàng)建核心線程,之后放入任務(wù)隊列,任務(wù)隊列滿了創(chuàng)建線程直到最大線程數(shù),在超過最大線程數(shù)就會拒絕,線程空閑后超過核心線程數(shù)的會釋放,核心線程也可以通過配置來釋放,針對那些一天只跑一個任務(wù)的情況。newCachedThreadPool線程池會導致創(chuàng)建大量的線程,因為用了同步隊列。
synchronized
同步塊會有一個monitorenter和多個monitorexist ,重量級鎖是通過linux內(nèi)核pthread里的互斥鎖實現(xiàn)的,包含一個waitset和一個阻塞隊列。 自旋鎖,會不停嘗試獲取鎖,他會導致其他阻塞的線程沒辦法獲取到鎖,所以他是不公平鎖,而輕量級鎖和偏向鎖,均是在當前對象的對象頭里做標記,用cas方法設(shè)置該標記,主要用于多線程在不同時間點獲取鎖,以及單線程獲取鎖的情況,從而避免重量級鎖的開銷,鎖的升級和降級也需要在安全點進行。
reentrantlock相對synchronized的優(yōu)勢:可以控制公平還是非公平,帶超時,響應中斷。
CyclicBarrier 多個線程相互等待,只有所有線程全部完成后才通知一起繼續(xù) (調(diào)用await 直到所有線程都調(diào)用await才一起恢復繼續(xù)執(zhí)行)
countdownlatch 一個線程等待,其他線程執(zhí)行完后它才能繼續(xù)。(調(diào)用await后被阻塞,直到其他地方調(diào)用countdown()將state減到1 這個地方的其他可以是其他多個線程也可以其他單個任務(wù))
semaphore 同一個時刻只運行n個線程,限制同時工作的線程數(shù)目。
阻塞隊列一般用兩個鎖,以及對應的條件鎖來實現(xiàn),默認為INTEGER.MAX為容量,而同步隊列沒有容量,優(yōu)先級隊列內(nèi)部用紅黑樹來實現(xiàn)。
如果要頻繁讀取和插入建議用concurrenthashmap 如果頻繁修改建議用 concurrentskiplistmap,copyonwrite適合讀多寫少,寫的時候進行拷貝,并加鎖。讀不加鎖,可能讀取到正在修改的舊值。concurrent系列實際上都是弱一致性,而其他的都是fail-fast,拋出ConcurrentModificationException,而弱一致性允許修改的時候還可以遍歷。例如concurrent類的size方法可能不是百分百準確。
AQS 的設(shè)計,用一個state來表示狀態(tài),一個先進先出的隊列,來維護正在等待的線程,提供了acquire和release來獲取和釋放鎖,鎖,條件,信號量,其他并發(fā)工具都是基于aqs實現(xiàn)。
字符串可以通過intern()方法緩存起來,放到永久代,一般一個字符串申明的時候會檢查常量區(qū)是否存在,如果存在直接返回其地址,字符串是final的,他的hashcode算法采用31進制相加,字符串的拼接需要創(chuàng)建一個新的字符串,一般使用stringbuilder。String s1 = "abc"; String s2 = "abc"; String s1 = new String("abc"); s1和s2可能是相等的,因為都指向常量池。
vector 線程安全,arraylist 實現(xiàn) randomaccess 通過數(shù)組實現(xiàn)支持隨機訪問,linkedlist 雙向鏈表可以支持快速的插入和刪除。
treeset 依賴于 treemap 采用紅黑樹實現(xiàn),可以支持順序訪問,但是插入和刪除復雜度為 log(n)
hashset 依賴于 hashmap 采用哈希算法實現(xiàn),可以支持常數(shù)級別的訪問,但是不能保證有序
linkedhashset 在hashset的節(jié)點上加了一個雙向鏈表,支持按照訪問和插入順序進行訪問
hashtable早版本實現(xiàn),線程安全 不支持空鍵。
hashmap:根據(jù)key的hashcode的低位進行位運算,因為高位沖突概率較高,根據(jù)數(shù)組長度計算某個key對應數(shù)組位置,類似求余算法,在put的時候會進行初始化或者擴容,當元素個數(shù)超過 數(shù)組的長度乘以負載因子的時候進行擴容,當鏈表長度超過8會進行樹化,數(shù)組的長度是2的多少次方,主要方便位運算,另一個好處是擴容的時候遷移數(shù)據(jù)只需要遷移一半。當要放 15個元素的時候,一般數(shù)組初始化的長度為 15/0.75= 20 然后對應的2的多少次方,那么數(shù)組初始化長度為 32.
ConcurrentHashMap 內(nèi)部維護了一個segment數(shù)組,這個segment繼承自reentrantlock,他本身是一個hashmap,segment數(shù)組的長度也就是并發(fā)度,一般為16. hashentry內(nèi)部的value字段為volatile來保證可見性.size()方法需要獲取所有的segment的鎖,而jdk8的size()方法用一個數(shù)組存儲每個segment對應的長度。
輸入輸出流的數(shù)據(jù)源有 文件流,字節(jié)數(shù)組流,對象流 ,管道。帶緩存的輸入流,需要執(zhí)行flush,reader和writer是字符流,需要根據(jù)字節(jié)流封裝。
bytebuffer里面有position,capcity,limit 可以通過flip重置換,一般先寫入之后flip后在從頭開始讀。
文件拷貝 如果用一個輸入流和一個輸出流效率太低,可以用transfer方法,這種模式不用到用戶空間,直接在內(nèi)核進行拷貝。
一個線程一個連接針對阻塞模式來說效率很高,但是吞吐量起不來,因為沒辦法開那么多線程,而且線程切換也有開銷,一般用多路復用,基于事件驅(qū)動,一個線程去掃描監(jiān)聽的連接中是否有就緒的事件,有的話交給工作線程進行讀寫。一般用這種方式實現(xiàn)C10K問題。
堆外內(nèi)存(direct) 一般適合io頻繁并且長期占用的內(nèi)存,一般建議重復使用,只能通過Native Memory Tracking(NMT)來診斷,MappedByteBuffer可以通過FileChannel.map來創(chuàng)建,可以在讀文件的時候少一次內(nèi)核的拷貝,直接將磁盤的地址映射到用戶空間,使用戶感覺像操作本地內(nèi)存一樣,只有當發(fā)生缺頁異常的時候才會觸發(fā)去磁盤加載,一次只會加載要讀取的數(shù)據(jù)頁,例如rocketmq里一次映射1g的文件,并通過在每個數(shù)據(jù)頁寫1b的數(shù)據(jù)進行預熱,將整個1G的文件都加載到內(nèi)存。
創(chuàng)建對象:工廠 構(gòu)建 單例
結(jié)構(gòu)型: 門面 裝飾 適配器 代理
行為型:責任鏈 觀察者 模版
封裝(隱藏內(nèi)部實現(xiàn)) 繼承(代碼復用) 多態(tài)(方法的重寫和重載)
設(shè)計原則:單一指責,開關(guān)原則,里氏替換,接口分離,依賴反轉(zhuǎn)
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。