Java高階深入與JVM
1.關於HashCode
不能根據hashCode值判斷兩個物件是否相等,但可以直接根據hashCode值判斷兩個物件不相等。
如果兩個物件的hashCode值不等,一定是不同的物件,要判斷兩個物件是否真正相等,必須通過equals()方法
如果呼叫equals()方法得到的結果為true,則兩個物件的hashCode值一定相等
如果equals()方法得到的結果為false,則兩個物件的hashCode值不一定不同
如果兩個物件的hashcode值不等,則equals方法得到的結果必定為false;
如果兩個物件的hashcode值相等,則equals方法得到的結果未知。
在重寫equals()方法時,必須重寫hashCode方法,讓equals()方法和hashCode()方法始終在邏輯上保持一致性
hashCode方法的主要作用是為了配合基於雜湊的集合一起正常執行,這樣的雜湊集合包括HashSet、HashMap以及HashTable
Java裡靜態語句塊是優先物件存在,也就是優先於構造方法存在,我們通常用來做只建立一次物件使用,類似於單列模式而且執行的順序是:
父類靜態語句塊 -> 子類靜態語句塊 -> 父類非靜態程式碼塊、父類構造方法 -> 子類非靜態程式碼塊構造方法
靜態語句塊可以看作在類載入的時候執行,且只執行一次
Java物件初始化順序:
靜態程式碼塊內容先執行(父>子),接著執行父類非靜態程式碼塊和構造方法,然後執行子類非靜態程式碼塊和構造方法
首先執行父類靜態的內容,父類靜態的內容執行完畢後,接著去執行子類的靜態的內容,當子類的靜態內容執行完畢之後,再去看父類有沒有非靜態程式碼塊,如果有就執行父類的非靜態程式碼塊,
父類的非靜態程式碼塊執行完畢,接著執行父類的構造方法;父類的構造方法執行完畢之後,它接著去看子類有沒有非靜態程式碼塊,如果有就執行子類的非靜態程式碼塊。子類的非靜態程式碼塊執行完畢再去執行子類的構造方法
2.JVM記憶體模型
程式計數器
用來指示 執行哪條指令的
如果執行緒執行的是非native方法,則程式計數器保持的是當前需要執行的指令的地址
如果執行緒執行的是native方法,程式計數器中的值是undefined
Java棧
存放一個個的棧幀,每個棧幀對應一個被呼叫的方法。
棧幀中包括:區域性變數表、運算元棧、指向當前方法所屬的類的執行時常量池的引用、方法返回地址和一些額外的附加資訊
當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓入棧,當方法執行完後,便會將棧幀出棧
執行緒當前執行的方法所對應的棧幀位於Java棧的頂部
棧幀
區域性變數表
運算元棧
指向執行時常量池的引用
方法返回地址
額外的附加資訊
堆
儲存物件本身以及陣列
方法區 --- 執行緒共享區域
1、儲存了每個類的資訊(包括:類名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯後的程式碼等
2、常量池
本地方法棧
3.Hash表
雜湊表,能快速定位到想要查詢的記錄,而不是與表中存在的記錄的關鍵字比較來進行查詢。
Hash表的設計,採用了函式對映思想,將記錄的儲存位置與記錄的關鍵字關聯起來,從而能夠很快速地進行查詢
eg:
張三 13980593357
李四 15828662334
王五 13409821234
張帥 13890583472
Hash表能通過"李四"這個資訊直接獲取到該記錄在表中的位置,複雜度為O(1)
原理:
Hash表採用一個對映函式
f:key --> address
將關鍵字key對映到該記錄在表中的儲存位置
說明:
1.對映關係 稱為 Hash函式
2.通過Hash函式和關鍵字計算出來的儲存位置(hash表中的儲存位置,不是實際的實體地址) 稱為 Hash地址
聯絡人資訊採用Hash表儲存時,當想找 "李四" 的資訊時,直接根據 "李四" 和 Hash函式,計算出Hash地址即可
4.Java執行緒安全的本質:執行緒中並沒有存放任何物件資料,而是在執行時,去主記憶體(堆)中去同步資料,所有的物件資料都存在JVM的堆中,因此需要對資源進行共享鎖!!!
堆 --- JVM的核心資料儲存區 --- 執行緒共享的主記憶體
堆中為JVM的所有物件分配了記憶體空間用以儲存和維護變數值等
棧 --- 執行緒私有的記憶體區,由棧幀(執行緒棧)組成,存放8中基本資料型別和物件引用
每個執行緒都會生成一個自有的執行緒棧,執行緒棧中用儲存了該執行緒的基本資料常量,變數值,以及物件長變數的引用
每個執行緒執行時,根據程式碼順序,壓棧 棧幀(棧記憶體)
物件變數線上程執行時的過程:!!! --- 由JVM記憶體模型決定
1.執行緒根據棧中的引用去堆上同步該物件資料下來,然後線上程自己的記憶體中進行操作
2.操作之後再將執行緒棧撒花姑娘的運算結果同步到堆(主記憶體)中
3.多執行緒時,因為每個執行緒都操作自己從主記憶體(JVM堆)中同步過來的資料,如果不加鎖,會導致執行緒安全問題(資料提交到主記憶體時不一致)
5.堆 --- JVM中所有物件的記憶體空間 分為: Young Gen, Old Gen
Young Gen 又分為:Eden區和兩個大小相同的Survivor區(from 和 to)
Eden和Survivor預設比例 8:1 由 -XX:SurvivorRation設定
堆大小 -Xmx -Xms 設定
Young Gen -Xmn 設定
-XX:NewSize和-XX:MaxNewSize
用於設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。
Minor GC --- 發生在新生代的垃圾回收,Java 物件大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,
年輕代的GC使用複製演算法(將記憶體分為兩塊,每次只用其中一塊,當這塊記憶體用完,就將還活著的物件複製到另外一塊上面,複製演算法不會產生記憶體碎片)
Full GC --- 發生在年老代的GC, Full GC比較慢,儘量避免
新建立物件都會被分配到Eden區(一些大物件特殊處理),當Eden區滿則進行Minor GC,
這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區, 物件在Survivor區中每熬過一次Minor GC,年齡增加1歲,
當年齡增加到一定程度後(預設15歲),會被移動到年老代中,
當年老代滿時,經常Full GC
執行緒Stack 每個執行緒獨有的運算元棧區域性變數表方法入口 -Xss 設定
方法區 -XX:PermSize和-XX:MaxPermSize設定
6.JVM class類載入載入機制 --- JVM把描述類的資料從class載入到記憶體,並對資料進行校驗,轉化解析和初始化,最終得到可被虛擬機器直接使用的Java型別
類裝載器: 尋找類的位元組碼檔案, 並構造出類在JVM內部表示的物件元件
JVM類載入器把一個類裝入JVM過程:
(1) 裝載:查詢和匯入Class檔案;
(2) 連結:把類的二進位制資料合併到JRE中;
(a)校驗:檢查載入Class檔案資料的正確性,確保被載入類資訊符合JVM規範;
(b)準備:給類的靜態變數分配儲存空間,並將其初始化為預設值;
(c)解析:將虛擬機器常量池中符號引用轉成直接引用;
(3) 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作,為類的靜態變數賦初始值
說明:
1.類載入的雙親委託機制
某個特定的類載入器在接到載入類的請求時,首先將載入任務交給父類載入器,父類載入器又將載入任務向上委託,直到最高層的父類載入器,
如果最高層的父類載入器可以完成類載入任務,就成功返回,否則向下傳遞載入任務,由其子類載入器進行載入
2.初始化步驟
1.如果類沒有被載入和連結,則先進行載入和連結
2.如果類存在直接父類,並且父類未被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類
3.如果存在static塊,一次執行這些初始化語句
4.static塊,會在類第一次被使用時執行(呼叫類的靜態變數,初始化物件,Class.forName等)
7.正規表示式總計:
^ 和$表示以字串開始和以字串結尾。例:^abc 表示必須以abc開始(如:abcd,abcefd),abc$ 表示必須以abc結尾(如:);^abc$ 只能是abc(abc是個整體,abcabc不匹配) ;abc 表示包含abc的字串
* 和 + 和 ? 分別表示出現0次或多次,1次或多次,0次或1次。例:abc*表示有0個或多個abc,其他兩個同理
上面的*+?完全可以用範圍代替,abc{2}表示ab後面有至少兩個c,如abcc,dfdabccccc都是符合的;abc{2}$ 只有以abcc結尾的符合,如343abcc
abc{1,2} 表示ab後面跟著1或2個c;
abc{3,} 表示ab後面跟著至少3個c; {,3}這種是不正確的
| 或運算 ab|cd 表示字串裡有ab或者cd;
. 可以替換任意字元
下面是幾種是需要記住的
"[ab]":表示一個字串有一個"a"或"b"(相當於"a|b");
"[a-d]":表示一個字串包含小寫的'a'到'd'中的一個(相當於"a|b|c|d"或者"[abcd]");
"^[a-zA-Z]":表示一個以字母開頭的字串;
"[0-9]%":表示一個百分號前有一位的數字;
",[a-zA-Z0-9]$":表示一個字串以一個逗號後面跟著一個字母或數字結束。
下面看看具體的例項,比如我今天做的:一個輸入框,可以輸入數字,也可以輸入多個數字用逗號隔開,或者兩個數字用~分隔。
我寫的正規表示式 : ((^[0-9]+[~]?)?|^([0-9]+[,])+)[0-9]+$
8.JVM中的GC垃圾回收和記憶體分配策略 ---- JVM高階 參考:
http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
http://www.cnblogs.com/zhguang/tag/Java/
http://blog.csdn.net/column/details/javavirtualmachine.html
http://blog.csdn.net/eric_sunah/article/details/7870906
1.判斷回收物件
1.引用計數演算法
給物件中新增一個引用計數器,每當有一個地方引用它,計數器值+1,當引用失效,計數器值-1,任何時刻計數器為0的物件就不可能再被使用
缺點:
無法解決物件的迴圈依賴問題!!!
2.可達性演算法 --- JVM使用的GC判斷演算法
通過一系列稱為 GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鏈,
當一個物件到 GC Roots 沒有任何引用鏈相連時(即:GC Roots到這個物件不可達),則此物件是不可用的
Java中可作為 GC Roots的物件包括以下幾種:
1.虛擬機器棧(棧幀的本地變數表)中引用的物件
2.方法區中類靜態屬性引用的物件
3.方法區中常量引用的物件
4.本地方法棧中JNI(Native方法)引用的物件
2.JVM的垃圾回收機制
1.幾個概念:
記憶體分代 新生代(Eden + 2 Survivor )、老年代、永久代
新建立的物件分配在Eden區,經歷一次Minor GC後被移到 Survivor1區,再經歷一次Minor GC被移到Survivor2區,直到升至老年代
大物件(長字串或大陣列)可能直接存放到老年代
物件建立都在堆上,類資訊、常量、靜態變數儲存在方法區,堆和方法區是線性共享的
GC由守護執行緒執行,在從記憶體回收一個物件之前,會呼叫物件的finalize()方法
GC的觸發由JVM堆記憶體大小決定,System.gc()和Runtime.gc()會向JVM傳送執行GC的請求,但JVM不保證一定會執行GC
堆中沒有記憶體建立新物件時,會丟擲 OutOfMemoryError
2.GC回收什麼物件?
可達性演算法 --- 根搜尋法
--- 通過一系列稱為GC Roots的物件作為起始點,從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,證明此物件不可用,可以被回收了
程式把所有的引用關係看做一張圖,從一個節點 GC Roots開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢後,剩餘的節點被認為是沒有被引用的節點,可被回收
--- 可被作為GC Roots的物件
1.虛擬機器棧中引用的物件(本地變數表)
2.方法靜態屬性引用的物件
3.方法區中常量引用的物件
4.本地方法棧用引用的物件(Native物件)
引用計數法
--- 可能有迴圈依賴問題,導致計數器不為0,物件永遠無法被回收
多次掃描,而都不可達的物件,經歷幾次掃描後會被回收
3.垃圾回收演算法: 參考:http://www.cnblogs.com/sunniest/p/4575144.html
任何垃圾回收演算法,只做2件基本事情:
1.發現無用可被回收物件 (可達性演算法)
2.回收被無用物件佔用的空間,使得該空間可被再次使用
1.標記-清除演算法:
從GC Roots根集合進行掃描,對存活的物件進行標記,標記完成後,在掃描整個空間中未被標記的物件,進行回收
優點:不必移動物件,在存活物件比較多的情況下極為高效
缺點:GC時會暫停整個應用,容易造成不連續的記憶體碎片
2.複製演算法:
為了解決記憶體碎片問題而產生的一種演算法。它的整個過程可以描述為:標記所有的存活物件;通過重新調整存活物件位置來縮並物件圖;更新指向被移動了位置的物件的指標
將記憶體劃分為大小相等的2塊,每次只是用其中一塊,當一塊用完後,將還存活的物件複製到另一塊記憶體上,然後把已使用過的記憶體空間一次清理掉,
這樣使得每次都是對其中的一塊進行記憶體回收,不會產生碎片等情況,只要移動堆訂的指標,按順序分配記憶體即可,實現簡單,執行高效
優點:不會造成記憶體碎片
缺點:記憶體縮小為原來的一半,需要移動物件
3.標記-整理演算法:
先進行 標記-清除演算法,然後在清理完無用物件後,將所有存活的物件移動到一端,並更新引用其物件的指標
4.分代演算法:
基於物件生命週期分析得出的垃圾回收演算法。把物件分為年輕代、年老代、持久代,對不同的生命週期使用不同的演算法
年輕代: --- Minor GC, 物件生命週期短 使用: Serial、PraNew、Parallel Scavenge 收集器
1.所有新生成物件,目標是儘快收集掉生命週期短的物件
2.新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分物件在Eden區中生成。回收時先將eden區存活物件複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活物件複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
Eden : Survivor0:Survivor1 = 8:1:1
3.當survivor1區不足以存放 eden和survivor0的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)
老年代: --- Full GC 物件生命週期長 使用:Serial Old、Parallel Old、CMS收集器
1.在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。
2.記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。
永久代: --- 用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響
4.GC實現 --- 垃圾收集器
Serial --- 複製演算法
新生代單執行緒收集器,標記和清理都是單執行緒,優點:簡單高效
Serial Old --- 標記-整理演算法
老年代單執行緒收集器
ParNew --- 停止複製演算法
Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現
Parallel Scavenge ---
並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間)。適合後臺應用等對互動相應要求不高的場景
Parallel Old收集器(停止-複製演算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS --- 標記清理演算法
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
5.GC執行機制
Minor GC: 新物件在年輕代的Eden區申請記憶體失敗時,觸發Minor GC --- 年輕代
--- 只對年輕代的Eden去進行,不影響老年代
--- 頻繁進行
--- 選用速度快、效率高的演算法,使Eden儘快空閒
Full GC: 對整個堆進行整理,包括 年輕代、老年代、永久代
--- 對整個堆進行回收,速度比 Minor GC 慢
--- 儘量減少 Full GC次數
--- 物件JVM調優過程,大部分是針對Full GC的調節
可能導致Full GC的原因:
1.老年代滿
2.持久代滿
3.System.gc()被顯示呼叫
4.上一次GC之後Heap的各域分配策略動態變化
6.容易出現洩露的地方:
1.靜態集合類(HashMap、Vector),這些靜態變數生命週期和應用程式一樣,所有的物件也不能被釋放
2.各種連線、資料了連線、網路連線、IO連線沒被顯示呼叫close(),不被GC回收導致記憶體洩露
3.監聽器,在釋放物件的同時沒有相應刪除監聽器時可能導致記憶體洩露
9.JVM記憶體模型 http://www.infoq.com/cn/articles/java-memory-model-1
記憶體模型:描述了程式中各個變數(例項域、靜態域和陣列元素)直接的關係,以及在實際計算機系統中將變數儲存到記憶體、從記憶體中取出變數這樣的底層細節
JVM中存在一個主存,Java中所有物件成員變數都儲存在主存中,對於所有執行緒是共享的,每條執行緒都有自己的工作記憶體,
工作記憶體中儲存的是主存中某些物件成員變數的拷貝,執行緒對所有成員變數的操作都是在工作記憶體中進行,然後同步到主存,執行緒之間無法相互直接訪問,變數傳遞都需要通過主存
JMM(Java記憶體模型,Java Memory Model簡稱)是控制Java執行緒之間、執行緒和主存之間通訊的協議。
JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體(local memory),
本地記憶體中儲存了該執行緒以讀/寫共享變數的副本,執行緒在本地私有記憶體中操作完後,要將資料同步到主記憶體
Java記憶體模型中規定了:
所有變數都儲存在主記憶體中,每個執行緒都有自己的工作記憶體,執行緒的工作記憶體中使用到的變數,是主記憶體的副本拷貝,
執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,操作完後,要將資料同步到主記憶體
不同的執行緒之間工作記憶體是獨立的,執行緒間變數值的傳遞均需要在主記憶體來完成
特別注意:
(根據Java虛擬機器規範的規定,volatile變數依然有共享記憶體的拷貝,但是由於它特殊的操作順序性規定——
從工作記憶體中讀寫資料前,必須先將主記憶體中的資料同步到工作記憶體中,所有看起來如同直接在主記憶體中讀寫訪問一般,因此這裡的描述對於volatile也不例外)。
不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值得傳遞均需要通過主記憶體來完成
8種記憶體間的互動操作:
Java記憶體模型定義了8種操作來完成主記憶體與工作記憶體之間互動的實現細節
1.lock(鎖定)
作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態
2.unlock(解鎖)
作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定
3.read(讀取)
作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
4.load(載入)
作用於主記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中
5.use(使用)
作用於工作記憶體的變數,它把工作記憶體中的一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值得位元組碼指令時,將會執行這個操作
6.assign(賦值)
作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作
7.store(儲存)
作用於工作記憶體的變數,它把工作記憶體中的一個變數的值傳遞到主記憶體中,一般隨後的write操作使用
8.write(寫入)
作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數值放入主記憶體的變數中
8種基本操作必須滿足的規則:
1.不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行(即:read和load直接、store和write之間是可插入其他指令的)
2.不允許一個執行緒丟棄它的最近的assign操作,即:變數在工作記憶體中改變了之後,必須把該變化同步回主記憶體
3.不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從執行緒的工作記憶體同步回主記憶體中
4.一個新的變數只能從主記憶體中"誕生",不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數,
即:對一個變數實施use和store操作之前,必須先執行過了assign河load操作
5.一個變數在同一時刻只允許一條執行緒對其執行lock操作,但lock操作可以被同一個執行緒重複執行(可重入鎖),
多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖
6.如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值
7.如果一個變數實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數
8.對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體(執行store和write操作)
volatile變數的特殊規則: --- 不能保證原子性
1.保證此變數對所有執行緒的可見性
2.禁止指令重排序優化
記憶體模型有哪些規則?
原子性、可見性、可排序性
10.Java反射與動態代理
Java反射,可以知道Java類的內部構造,就可以與它進行互動,包括建立新的物件和呼叫物件中的方法等。
這種互動方式與直接在原始碼中使用的效果是相同的,但是又額外提供了執行時刻的靈活性。使用反射的一個最大的弊端是效能比較差
每個類被載入進記憶體後,系統就會為該類生成一個對應的 java.lang.Class物件,通過該Class物件即可訪問到JVM中的這個類
載入完類之後,在堆記憶體中會產生一個Class型別的物件(一個類只有一個Class物件),這個物件包含了完整的類結構資訊,這個Class物件就像一面鏡子,可以看到類的結構
程式在執行狀態中,可以動態載入一個只有名稱的類(全路徑),對於任意一個已經載入的類,都能夠知道這個類的所有屬性和方法,
對於任意一個物件,都能呼叫它的任意一個方法和屬性
用法:
作用1:獲取類的內部結構
Java反射API可以獲取程式在執行時刻的內部結構,只需短短十幾行程式碼,就可遍歷出一個Java類的內部結構,包括:構造方法、宣告的域和定義的方法等
java.lang.Class類的物件,可以通過其中的方法來獲取到該類中的構造方法、域和方法(getConstructor、getFiled、getMethod)
這三個方法還有相應的getDeclaredXXX版本,區別在於getDeclaredXXX版本的方法只會獲取該類自身所宣告的元素,而不會考慮繼承下來的。
Constructor、Field和Method這三個類分別表示類中的構造方法、域和方法。這些類中的方法可以獲取到所對應結構的後設資料。
作用2:執行時刻,對一個Java物件進行操作
動態建立一個Java類的物件,獲取某個屬性的值和呼叫某個方法
在Java原始碼中編寫的對類和物件的操作,都可以在執行時刻通過反射API來實現
特別說明:
clazz.setAccessible(true) --- 可以獲取到類中的private屬性和private方法
Class物件的獲取
1.物件的 getClass()方法
2.類.class屬性 --- 最安全,效能最好
3.Class.forName(String classFullName) 動態載入類 --- classFullName是類的全限定名(包.類名) --- 最常用
從Class中獲取資訊
1.獲取資訊
構造器、包含方法、包含屬性、包含的Annotation、實現的介面,所在包,類名,簡稱,修飾符等
獲取內容 方法簽名
構造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法 Method getMethod(String name, Class<?>... parameterTypes)
包含的屬性 Field getField(String name)
包含的Annotation <A extends Annotation> A getAnnotation(Class<A> annotationClass)
內部類 Class<?>[] getDeclaredClasses()
外部類 Class<?> getDeclaringClass()
所實現的介面 Class<?>[] getInterfaces()
修飾符 int getModifiers()
所在包 Package getPackage()
類名 String getName()
簡稱 String getSimpleName()
2.判斷資訊
註解型別、是否是了指定註解、是否是陣列、列舉、介面等
判斷內容 方法簽名
註解型別? boolean isAnnotation()
使用了該Annotation修飾? boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名類? boolean isAnonymousClass()
陣列? boolean isArray()
列舉? boolean isEnum()
原始型別? boolean isPrimitive()
介面? boolean isInterface()
obj是否是該Class的例項 boolean isInstance(Object obj)
反射生成並操作物件
通過Method物件執行相應的方法
通過Constructor物件呼叫對應的構造器來建立例項
通過Field物件直接訪問和修改物件的成員變數值
反射建立物件
2種方式:
1.使用Class物件的newInstance()建立該Class物件對應類的例項(要求改Class對應類有預設構造器)
2.先使用Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法建立該Class物件對應類的例項
--- 該方式,可選擇指定的構造器來建立例項
1.Spring根據配置檔案資訊(類的全限定名),使用反射建立物件 ---- 方式一,預設構造器建立例項
eg:
模擬Spring,實現一個物件池,物件池根據檔案讀取key-value對,然後建立這些物件,並放入Map中,
物件池可以將id作為key,將物件例項作為value,可以通過id獲取物件例項
即:
ObjectPool pool = ObjectPool.init("config.json");
User user = (User) pool.getObject("id1");
System.out.println(user);
Bean bean = (Bean) pool.getObject("id2");
System.out.println(bean);
參考:
http://blog.csdn.net/zjf280441589/article/details/50453776
配置檔案
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User"
},
{
"id": "id2",
"class": "com.fq.domain.Bean"
}
]
}
ObjectPool物件池
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
private static JSONArray getObjects(String config) throws IOException {
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
// 根據指定的JSON配置檔案來初始化物件池
public static ObjectPool init(String config) {
try {
JSONArray objects = getObjects(config);
ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
if (objects != null && objects.size() != 0) {
for (int i = 0; i < objects.size(); ++i) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
pool.putObject(id, getInstance(className));
}
}
return pool;
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Object getObject(String id) {
return pool.get(id);
}
public void putObject(String id, Object object) {
pool.put(id, object);
}
public void clear() {
pool.clear();
}
}
例項類User和Bean
public class User {
private int id;
private String name;
private String password;
}
public class Bean {
private Boolean usefull;
private BigDecimal rate;
private String name;
}
反射呼叫方法
獲取到某個類對應的Class物件後,可通過該Class物件的getMethod()來獲取一個Method陣列或Method物件,
每個Method物件對應一個方法,在獲取Method物件後,可通過呼叫invoke()方法呼叫該Method物件對應的方法
eg:
通過動態呼叫物件方法 + 配置檔案,來給物件設定值 --- 根據屬性建立物件(呼叫setter方法,可設定屬性和依賴的物件)
1.json格式的配置檔案,用來定義物件、屬性值及其依賴關係 --- config.json
注意:
其中fields代表該Bean所包含的屬性, name為屬性名稱, value為屬性值(屬性型別為JSON支援的型別),
ref代表引用一個物件(也就是屬性型別為Object,但是一定要引用一個已經存在了的物件)
這裡定義了一個User物件,設定其3個屬性
定義一個ComplexBean物件,設定其name屬性為complex-bean-name,並設定其引用的物件是id2
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User",
"fields": [
{
"name": "id",
"value": 101
},
{
"name": "name",
"value": "feiqing"
},
{
"name": "password",
"value": "ICy5YqxZB1uWSwcVLSNLcA=="
}
]
},
{
"id": "id2",
"class": "com.fq.domain.Bean",
"fields": [
{
"name": "usefull",
"value": true
},
{
"name": "rate",
"value": 3.14
},
{
"name": "name",
"value": "bean-name"
}
]
},
{
"id": "id3",
"class": "com.fq.domain.ComplexBean",
"fields": [
{
"name": "name",
"value": "complex-bean-name"
},
{
"name": "refBean",
"ref": "id2"
}
]
}
]
}
2.定義物件池,用於建立物件,並呼叫其方法給物件賦值
參考:反射.ObjectPool 的實現
package com.jay.advanced.java.反射;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模擬Spring,建立一個物件池
* Created by hetiewei on 2017/2/17.
*/
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
//從指定檔案,讀取配置資訊,返回解析後json陣列
private static JSONArray getObjects(String config) throws IOException {
//獲取輸入流
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
//讀取輸入流內容,變成json陣列返回
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
//根據類名,獲取類的物件例項
private static Object getIntance(String className, JSONArray fields) throws ClassNotFoundException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//配置的Class
Class<?> clazz = Class.forName(className);
//目標Class的例項物件
Object targetObject = clazz.newInstance();
//遍歷屬性,賦值給例項物件 --- 注意區分,直接賦值,還是賦值引用物件
if (fields != null && fields.size() != 0) {
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
//需要設定的成員變數名
String filedName = field.getString("name");
//需要設定的成員變數的值
Object fieldValue;
//如果是8種基本型別或String型別,直接獲取value,得到屬性賦值
if (field.containsKey("value")) {
fieldValue = field.get("value");
} else if (field.containsKey("ref")) {
//如果是引用型別, 先獲得引用物件的id,然後根據id,從物件池中得到引用物件
String refBeanId = field.getString("ref");
fieldValue = OBJECTPOOL.getObject(refBeanId);
} else {
throw new RuntimeException("沒有value 或 ref 引用");
}
//構造setterXxx
String setterName = "set" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
//需要設定成員變數的setter方法
Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
//呼叫setter方法設定值
setterMethod.invoke(targetObject, fieldValue);
}
}
return targetObject;
}
private static ObjectPool OBJECTPOOL;
//建立一個物件池例項(儲存多執行緒安全)
private static void initSingletonPool() {
if (OBJECTPOOL == null) {
synchronized (ObjectPool.class) {
if (OBJECTPOOL == null) {
OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
}
}
}
}
//指定根據的JSON配置檔案來初始化物件池
public static ObjectPool init(String config) {
//初始化pool
initSingletonPool();
//解析Json配置檔案
try {
JSONArray objects = getObjects(config);
for (int i = 0; i < objects.size(); i++) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
//初始化Bean,並放入物件池中
OBJECTPOOL.putObject(id, getIntance(className, object.getJSONArray("fields")));
}
return OBJECTPOOL;
} catch (IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void putObject(String id, Object obj) {
pool.put(id, obj);
}
public Object getObject(String id) {
return pool.get(id);
}
public void clear() {
pool.clear();
}
}
3.客戶端使用物件池
//初始化物件池
ObjectPool pool = ObjectPool.init("config.json");
//從物件池中根據id獲取物件例項
User user = (User) pool.getObject("id1");
Bean bean = (Bean) pool.getObject("id2");
ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
4.ComplexBean類
public class ComplexBean {
private String name;
private Bean refBean;
}
反射訪問並操作成員變數
通過Class物件的getField()方法可獲取該類所包含的全部或指定的成員變數Field,
getDeclaredXxx方法可以獲取所有的成員變數,無論private/public;
Field提供了讀取和設定成員變數值的方法
getXxx(Object obj)
獲取obj物件的該成員變數的值,此處的Xxx對應8中基本型別,如果該成員變數的型別是引用型別, 則取消get後面的Xxx;
setXxx(Object obj, Xxx val)
將obj物件的該成員變數值設定成val值.此處的Xxx對應8種基本型別, 如果該成員型別是引用型別, 則取消set後面的Xxx;
eg:
通過反射,設定成員變數值
User user = new User();
//反射獲取物件的指定屬性
Field idField = User.class.getDeclaredFiled("id");
//設定該屬性即使為private也可被訪問
idField.setAccessible(true);
//將物件的常用變數,設定為指定值 --- 這裡將user物件的id屬性,設定為30
idField.setInt(user, 30);
private void setAccessible(AccessibleObject object) {
object.setAccessible(true);
}
反射獲取註解資訊
只需要獲取到Class Method Filed等這些實現了AnnotatedElement介面的類例項, 就可以獲取到我們想要的註解資訊
eg:
獲取client()方法上的註解資訊
Annotation[] annotations = this.getClass().getMethod("client").getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType().getName());
}
eg:
獲取某個註解中的後設資料,需要強轉成所需的註解型別,然通過註解物件的抽象方法來訪問這些後設資料
@Tag(name = "client")
public class Client {
@Test
public void client() throws NoSuchMethodException {
Annotation[] annotations = this.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Tag) {
Tag tag = (Tag) annotation;
System.out.println("name: " + tag.name());
System.out.println("description: " + tag.description());
}
}
}
}
反射獲取泛型資訊
為了通過反射操作泛型,Java新增了4種型別來代表不能歸一到Class了下,但又和原始型別同樣重要的型別
型別 含義
ParameterizedType 一種引數化型別, 比如Collection<String>
GenericArrayType 一種元素型別是引數化型別或者型別變數的陣列型別
TypeVariable 各種型別變數的公共介面
WildcardType 一種萬用字元型別表示式, 如? ? extends Number ? super Integer
動態代理:
代理模式:
代理物件和被代理物件一般實現相同的介面,呼叫者與代理物件進行互動。代理的存在對於呼叫者來說是透明的,呼叫者看到的只是介面。
代理物件則可以封裝一些內部的處理邏輯,如訪問控制、遠端通訊、日誌、快取等。比如一個物件訪問代理就可以在普通的訪問機制之上新增快取的支援,
傳統的代理模式的實現,需要在原始碼中新增一些附加的類。這些類一般是手寫或是通過工具來自動生成
動態代理:
JDK5引入了動態代理機制,允許開發人員在執行時刻動態的建立出代理類及其物件。
在執行時刻,可以動態建立出一個實現了多個介面的代理類,每個代理類的物件都會關聯一個表示內部處理邏輯的InvocationHandler介面的實現,
當使用者呼叫代理物件所代理的介面中的方法時,這個呼叫資訊被傳遞個InvocationHandler的invoke()方法,
在invoke()方法引數中可以獲取到代理物件、方法對應的Method物件和呼叫的實際引數,invoke()方法的返回值被返回給使用者。
相當於對方法呼叫進行了攔截 --- 這是一個不需要依賴AspectJ等AOP框架的一種AOP實現方式
11.常用的記憶體調節引數
-Xms 初始堆大小,預設是實體記憶體1/64(<1G)
-Xmx 最大堆大小
-Xmn 新生代的記憶體空間大小(eden+ 2 survivor space),
整個堆大小=新生代大小 + 老生代大小 + 永久代大小
在保證堆大小不變的情況下,增大新生代後,將會減小老生代大小。此值對系統效能影響較大,Sun官方推薦新生代配置為整個堆的3/8
-XX:SurvivorRation 新生代Eden區和Survivor區容量比,預設是8, 兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10
-Xss 每個執行緒的堆疊大小, 推薦 256K
-XX:PermSize 持久代(非堆記憶體)初始值, 預設是實體記憶體 1/64
-XX:MaxPermSize 持久代(非堆記憶體)最大值,預設實體記憶體1/4
-XX:+UseParallelGC 多核處理器,配置後提示GC效率
eg:
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=256M
-vmargs 說明後面是VM的引數,所以後面的其實都是JVM的引數了
-Xms128m JVM初始分配的堆記憶體
-Xmx512m JVM最大允許分配的堆記憶體,按需分配
-XX:PermSize=64M JVM初始分配的非堆記憶體
-XX:MaxPermSize=128M JVM最大允許分配的非堆記憶體,按需分配
說明:
Java 虛擬機器具有一個堆,堆是執行時資料區域,所有類例項和陣列的記憶體均從此處分配。堆是在 Java 虛擬機器啟動時建立的。”“在JVM中堆之外的記憶體稱為非堆記憶體(Non-heap memory)”。
JVM主要管理兩種型別的記憶體:堆和非堆。簡單來說堆就是Java程式碼可及的記憶體,是留給開發人員使用的;非堆就是JVM留給自己用的,
方法區,JVM記憶體處理貨優化所需的記憶體、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法的程式碼都在非堆記憶體中
記憶體分配方法:
1.堆上分配
2.棧上分配
12.JVM記憶體管理
程式計數器
方法區
堆
棧(JVM棧 + 本地方法棧)
說明:
GC主要發生在堆上,方法區也會發生GC, 棧與暫存器是執行緒私有的,不會GC
方法區:
存放內容:類資訊、類的static屬性、方法、常量池
已經載入的類的資訊(名稱、修飾符等)
類中的static變數
類中的field資訊
類中定義為final常量
類中的方法資訊
執行時常量池:編譯器生成的各種字面量和符號引用(編譯期)儲存在class檔案的常量池中,這部分內容會在類載入之後進入執行時常量池
使用例項: 反射
在程式中通過Class物件呼叫getName等方法獲取資訊資料時,這些資訊資料來自方法區
調節引數:
-XX:PermSize 指定方法區最小值,預設16M
-XX:MaxPermSize 指定方法區最大值,預設64M
所拋異常:
方法區使用記憶體超過最大值,丟擲 OutOfMemoryError
GC操作:
對類的解除安裝
針對常量池的回收
總結:
企業開發中, -XX:PermSize==-XX:MaxPermSize,都設定為256M
eg:
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
類中的static變數會在方法區分配記憶體,但是類中的例項變數不會(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧)
關於方法區的存放內容,可以這樣去想所有的通過Class物件可以反射獲取的都是從方法區獲取的(包括Class物件也是方法區的,Class是該類下所有其他資訊的訪問入口)
堆:
存放內容:
物件例項(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧)
陣列值
使用例項:
new建立的物件都在這裡分配
調節引數:
-Xmx:最大堆記憶體,預設為實體記憶體的1/4但小於1G
-Xms:最小堆記憶體,預設為實體記憶體的1/64但小於1G
-XX:MinHeapFreeRatio,預設當空餘堆記憶體小於最小堆記憶體的40%時,堆記憶體增大到-Xmx
-XX:MaxHeapFreeRatio,當空餘堆記憶體大於最大堆記憶體的70%時,堆記憶體減小到-Xms
注意:
在實際使用中,-Xmx與-Xms配置成相等的,這樣,堆記憶體就不會頻繁的進行調整了!!!
所拋錯誤:
OutOfMemoryError:Java heap space
堆記憶體劃分:
新生代:
Eden + from + to from 與 to 大小相等
-Xmn:整個新生代大小
-XX:SurvivorRation : 調整Eden:from(to)的比例,預設是 8:1 即: eden:from:to = 8:1:1
老年代:
新建物件直接分配到老年代的2種情況:
大物件:-XX:PretenureSizeThreshold(單位:位元組)引數來指定大物件的標準
大陣列:資料中的元素沒有引用任何外部的物件
總結:
企業開發中 -Xmx==-Xms
eg:
<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年輕代大小-Xmn==512m,這樣,年老代大小就是2048-512==1536m,這個比率值得記住,
在企業開發中,年輕代:年老代==1:3,而此時,我們配置的-XX:MaxTenuringThreshold=15(這也是預設值),年輕代物件經過15次的複製後進入到年老代
年輕代分為Eden和2個Survivor(from+to),預設 Eden:from:to==8:1:1
1.新產生的物件有效分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的物件會直接進入年老代)
2.當Eden區滿了或放不下時,其中存活的物件會複製到from區(注意:如果存活下來的物件from區放不下,則這些存活下來的物件全部進入老年代),之後Eden區的記憶體全部回收掉
注意:如果Eden區沒有滿,但來了一個小物件Eden區放不下,這時候Eden區存活物件複製到from區後,清空Eden區,之後剛才的小物件再進入Eden區
3.之後產生的物件繼續分配在Eden區,當Eden區滿時,會把Eden區和from區存活下來的物件複製到to(同理,如果存活下來的物件to區都放不下,則這些存活下來的物件全部進入年老代),之後回收掉Eden區和from區的所有記憶體;
4)如上這樣,會有很多物件會被複制很多次(每複製一次,物件的年齡就+1),預設情況下,當物件被複制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了
5)當年老代滿了或者存放不下將要進入年老代的存活物件的時候,就會發生一次Full GC(這個是我們最需要減少的,因為耗時很嚴重)
棧:
注意點:
每個執行緒都會分配一個棧,每個棧中有多個棧幀(每個方法對應一個棧幀) 每個方法在執行的同時都會建立一個棧幀,每個棧幀用於儲存當前方法的區域性變數表、運算元棧等,
每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程,說的更明白一點,就是方法執行時建立棧幀,方法結束時釋放棧幀所佔記憶體
存放內容:
區域性變數表: 8種基本資料型別、物件引用, 該空間在編譯期已經分配好,執行期不變
引數調節:
-Xss:設定棧大小,通常設定為1m就好
eg:
<jvm-arg>-Xss1m</jvm-arg>
所拋異常:
StackOverFlowError 執行緒請求的棧深度大於虛擬機器所允許的深度
eg:
沒有終止調節的遞迴(遞迴基於棧)
每個方法的棧深度在javac編譯之後就已經確定了
OutOfMemoryError: 虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠記憶體,則拋該異常
注意:
棧可以動態擴充套件,但棧中的區域性變數表不可以
C暫存器(程式計數器)
概念: 當前執行緒所執行的位元組碼的行號指示器,用於位元組碼直譯器對位元組碼指令的執行。
多執行緒:通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條執行緒中的指令,
為了執行緒切換後能恢復到正確的執行位置,每條執行緒都要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。
記憶體分配概念:
在類載入完成後,一個物件所需的記憶體大小就可以完全確定了,具體的情況檢視物件的記憶體佈局。
為物件分配空間,即把一塊兒確定大小(上述確定下來的物件記憶體大小)的記憶體從Java堆中劃分出來
Java的物件記憶體佈局 3 大塊
物件頭
儲存物件自身的執行時資料:Mark Word(在32位和64位JVM上長度分別為32bit和64bit),包含資訊如下:
物件hashCode
物件GC分代年齡
鎖狀態標識
執行緒持有的鎖
偏向鎖相關:偏向鎖、自旋鎖、輕量級鎖以及其他的一些鎖優化策略是JDK1.6加入的,這些優化使得Synchronized的效能與ReentrantLock的效能持平,
在Synchronized可以滿足要求的情況下,優先使用Synchronized,除非是使用一些ReentrantLock獨有的功能,例如指定時間等待等。
型別指標: 物件指向類後設資料的指標
JVM通過這個指標來確定這個物件是哪個類的例項(根據物件確定其Class的指標)
例項資料
物件真正儲存的有效資訊
對齊填充
JVM要求物件的大小必須是8的整數倍,若不夠,需要補位對齊
注意:
1.Mark Word是非固定的資料結構,以便在極小空間記憶體儲儘量多的資訊
2.如果物件是一個陣列,物件頭必須有一塊用來記錄陣列長度的資料,JVM可以通過Java物件的後設資料確定物件長度,但對於陣列不行
3.基本資料型別和物件包裝類所在的記憶體大小(位元組)
boolean 1位元組
byte 1位元組
short 2位元組
char 2位元組
int 4位元組
float 4位元組
long 8位元組
double 8位元組
引用型別 在32位和64位系統上長度分別為4bit和8bit
記憶體分配的 2 種方式:
1.指標碰撞
適用場合:
堆記憶體規整(即:沒有記憶體碎片,有大塊完整記憶體)的情況下
原理:
用過的記憶體全部整合到一邊,沒用過的記憶體放在另一邊,中間有個分界值指標,只需要向著沒有用過的記憶體方向將該指標移動新建立的物件記憶體大小位置即可
GC收集器:
Serial、ParNew
2.空閒列表
適用場合:
堆記憶體不規整情況
原理:
JVM虛擬機器會維護一個列表,該列表會記錄哪些記憶體塊是可用的,在分配時,找一塊足夠大的記憶體來劃分給new的物件例項,最後更新列表記錄
GC收集器:
CMS
注意:
1.2種記憶體分配方式,取決於Java堆記憶體是否規整
2.Java堆記憶體是否規整,取決於GC收集器的演算法是 "標記-清除" 還是 "標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製演算法記憶體也是規整的
建立一個真正物件的基本過程:5步
1.類載入機制檢查
JVM首先檢查一個new指定的引數是否能在常量池中定位到一個符號引用,並且檢查該符號應用代表的類是否已被載入、解析和初始化過
實際就是在檢查new的物件所屬的類是否已經執行過類載入機制,如果沒有,則先進行載入機制載入類
2.分配記憶體
把一塊確定大小的記憶體從堆中劃分出來
3.初始化零值
物件的例項欄位不需要賦初始值也可以直接通過其預設零值
每種型別的物件都有不同的預設零值
4.設定物件頭
5.執行<init>
為物件字元賦值(第3步只是初始化了零值,這裡會根據引數,給例項賦值)
13.JVM 記憶體回收GC
1.記憶體回收區域
堆:GC主要區域
方法區: 回收無用的類和廢氣的常量
注意:
棧和PC暫存器是執行緒私有,不會發生GC
2.判斷物件是否存活
1.引用計數法
原理:
給物件新增一個引用計數器,每當有一個地方使用它,計數器值+1,引用失效時,計數器值-1
缺點:
1.每次為物件賦值時,都要進行計數器值得加減,消耗較大
2.對於迴圈依賴無法處理
2.可達性分析(跟蹤收集)
原理:
從根集合(GC Root)開始向下掃描,根集合中的節點可以到達的節點就是存活節點,根集合中的節點到達不了的節點就是要被回收的的節點
GC Root節點: 全域性性的引用(常量和靜態屬性)和棧引用
1.Java棧中的物件引用
2.方法區中, 常量+靜態變數
3. 3 種引用型別
強引用: A a = new A();//a是常引用
強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題
軟引用: 記憶體不足時,是否弱引用所引用的物件,當記憶體足夠時,就是一個普通物件(強引用)
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a); //軟引用
弱引用: 弱引用物件只能存活到下一次垃圾會話之前,一旦發生垃圾回收,立刻被回收掉
4.GC回收演算法
1.標記-清楚演算法 --- 年老代
2.標記-整理演算法(標記-壓縮) --- 年老代
3.複製演算法 --- 年輕代
標記-清楚演算法 --- 年老代
原理:
從根集合點掃描,標記處所有的存活物件,最後掃描整個記憶體空間,並清除沒有標記的物件(即:死亡物件)
使用場合:
存活物件較多的情況下比較高效
適用於年老代
缺點:
容易產生記憶體碎片,再來一個比較大的物件時(典型情況:該物件的大小大於空閒表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
掃描了整個空間兩次(第一次:標記存活物件;第二次:清除沒有標記的物件)
注意:
在該情況下,記憶體不規整,物件的記憶體分配採用"空閒列表法"
標記-整理演算法(標記-壓縮) --- 年老代
原理:
從根集合節點進行掃描,標記出所有的存活物件,最後掃描整個記憶體空間並清除沒有標記的物件(即死亡物件)(可以發現前邊這些就是標記-清除演算法的原理),清除完之後,將所有的存活物件左移到一起。
適用場合:
用於年老代(即舊生代)
缺點:
需要移動物件,若物件非常多而且標記回收後的記憶體非常不完整,可能移動這個動作也會耗費一定時間
掃描了整個空間兩次(第一次:標記存活物件;第二次:清除沒有標記的物件)
優點:
不會產生記憶體碎片
注意:
在該情況下,記憶體規整,物件的記憶體分配採用"指標碰撞法"
複製演算法 --- 年輕代
原理:
從根集合節點進行掃描,標記出所有的存活物件,並將這些存活的物件複製到一塊兒新的記憶體(圖中下邊的那一塊兒記憶體)上去,之後將原來的那一塊兒記憶體(圖中上邊的那一塊兒記憶體)全部回收掉
適用場合:
存活物件較少的情況下比較高效
掃描了整個空間一次(標記存活物件並複製移動)
適用於年輕代(即新生代):基本上98%的物件是"朝生夕死"的,存活下來的會很少
缺點:
需要一塊兒空的記憶體空間
需要複製移動物件
注意:
在該情況下,記憶體規整,物件的記憶體分配採用"指標碰撞法",見《第二章 JVM記憶體分配》
以空間換時間:通過一塊兒空記憶體的使用,減少了一次掃描
14.關於Set --- HashSet、TreeSet、LinkedHashSet -- 都是去重的,都可用Iterator或foreach進行遍歷
參考:http://blog.csdn.net/speedme/article/details/22661671
HashSet --- 去重,無序, add()時會呼叫hashcode和equals,所以儲存在HashSet中的物件需要重寫這兩個方法,非同步的,元素只能放一個null
即:
HashSet:資料結構式雜湊表,執行緒非同步。保證元素唯一性的原理,判斷hashCode是否相同,如果相同,判斷元素的equals方法
TreeSet --- 去重,可按照某種順序排序, add()會將物件轉為Comparable,然後呼叫compareTo()方法,所以儲存在TreeSet中的物件必須實現Comparable,重寫compareTo()方法
底層資料結構是 二叉樹,保證元素唯一性的依據
支援2種排序:自然排序、定製排序
TreeSet判斷兩個物件不相等的方式是兩個物件通過equals方法返回false,或者通過CompareTo方法比較沒有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。
Java提供了一個Comparable介面,該介面裡定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該介面的物件就可以比較大小。
obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個物件相等,如果返回一個正數,則表明obj1大於obj2,如果是 負數,則表明obj1小於obj2。
如果我們將兩個物件的equals方法總是返回true,則這兩個物件的compareTo方法返回應該返回0
定製排序
自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator介面,實現 int compare(T o1,T o2)方法
2種排序方式比較:
方式一:讓集合中的元素自身具有比較性,這就讓加入到TreeSet集合中的物件必須實現comparable介面重寫compareTo(Object obj)方法
這種方式也成為元素的自然排序或預設排序。(但是如果排序的元素不是本人寫的,別人寫的沒有實現comparable介面時想排序使用第二種方式)
方式二:讓集合容器具有比較性,自定義一個比較器實現comparator介面,重寫compare(Object o1,Object o2)方法,在初始化TreeSet容器物件將這個
自定義的比較器作引數傳給容器的建構函式,使得集合容器具有比較性,使用這種方式的優先順序高於方式一,
LinkedHashSet --- HashSet的子類,去重,並保留儲存順序
HashSet 工作原理: 每次儲存物件時,呼叫物件的hashCode(),計算一個hash值,在集合中查詢是否包含hash值相同的元素
如果沒有hash值相同的元素,根據hashCode值,來決定該物件的儲存位置,直接存入,
如果有hash值相同的元素,逐個使用equals()方法比較,
比較結果全為false就存入.
如果比較結果有true則不存.
如何將自定義類物件存入HashSet進行去重複
* 類中必須重寫hashCode()方法和equals()方法
* equals()方法中比較所有屬性
* hashCode()方法要保證屬性相同的物件返回值相同, 屬性不同的物件儘量不同
TreeSet 工作原理:儲存物件時,add()內部會自動呼叫compareTo()方法進行比較,根據比較結果使用二叉樹形式進行儲存 --- 二叉樹實現儲存
參考:http://blog.csdn.net/jinhuoxingkong/article/details/51191106
TreeSet使用二叉樹原理,對新add()的物件安裝指定的順序排序(升序、降序),每增加一個物件都會進行排序,將物件插入二叉樹指定的位置
Integer和String都是按預設的TreeSet排序,自定義的物件,必須實現Comparable介面,重寫compareTo(),指定比較規則
在重寫compare()方法時,要返回相應的值才能使TreeSet按照一定規則來排序,升序是:比較此物件與指定物件的順序。如果該物件小於、等於或大於指定物件,則分別返回負整數、零或正整數。
如果想把自定義類的物件存入TreeSet進行排序, 那麼必須實現Comparable介面
* 在類上implement Comparable
* 重寫compareTo()方法
* 在方法內定義比較演算法, 根據大小關係, 返回正數負數或零
TreeSet實現排序的2種比較方式:
1.自定義類類 實現 Comparable介面,重寫其 compareTo()方法 ---- 類排序
2.給TreeSet定義一個實現Comparator介面的比較器,重寫其 compare()方法 ---- 比較器排序
* a.自然順序(Comparable)
* TreeSet類的add()方法中會把存入的物件提升為Comparable型別
* 呼叫物件的compareTo()方法和集合中的物件比較
* 根據compareTo()方法返回的結果進行儲存
* b.比較器順序(Comparator)
* 建立TreeSet的時候可以制定 一個Comparator
* 如果傳入了Comparator的子類物件, 那麼TreeSet就會按照比較器中的順序排序
* add()方法內部會自動呼叫Comparator介面中compare()方法排序
* 呼叫的物件是compare方法的第一個引數,集合中的物件是compare方法的第二個引數
* c.兩種方式的區別
* TreeSet建構函式什麼都不傳, 預設按照類中Comparable的順序(沒有就報錯ClassCastException)
* TreeSet如果傳入Comparator, 就優先按照Comparator
15.關於Map --- HashMap、TreeMap
原理:http://blog.csdn.net/chenssy/article/details/26668941
HashMap
按照key的hashCode實現,無序
TreeMap
基於紅黑樹實現,對映根據其key的自然順序進行排序,或根據建立對映時提供的Comparator進行排序,具體取決於使用的構造方法
TreeMap只能依據key來排序,不能根據value排序
如果想對value排序,可以把TreeMap的EntrySet轉換成list,然後使用Collections.sort排序 -- 參考:http://blog.csdn.net/liuxiao723846/article/details/50454622
http://blog.csdn.net/xiaoyu714543065/article/details/38519817
eg:value是String或引用型別的值,按照指定規則對value進行排序
public static Map sortTreeMapByValue(Map map){
List<Map.Entry> list = new ArrayList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry>() {
//升序排
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().toString().compareTo(o2.getValue().toString());
}
});
for (Map.Entry<String, String> e: list) {
System.out.println(e.getKey()+":"+e.getValue());
}
return map;
}
16.關於ThreadLocal
當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本
3個重要方法:
void set(T value)、T get()以及T initialValue()
使用場景:
多執行緒中,每個執行緒需要獨享這個變數,且每個執行緒用的變數最初都是一樣的,可以通過ThreadLocal處理該變數
原理:
ThreadLocal如何為每個執行緒維護變數的副本?
ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中key為執行緒物件,value為執行緒的變數副本
eg:
public class JavaTest {
// 建立一個Integer型的執行緒本地變數, 並通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];// 建立5個執行緒
for (int j = 0; j < 5; j++) {
threads[j] = new Thread(new Runnable() {
@Override
public void run() {
// 獲取當前執行緒的本地變數,然後累加5次
int num = local.get();// 返回當前執行緒的執行緒本地變數值,若對應的thread不存在,則會呼叫initialValue初始化
for (int i = 0; i < 5; i++) {
num++;
}
// 重新設定累加後的本地變數
local.set(num);
System.out.println(Thread.currentThread().getName() + " : "
+ local.get());
}
}, "Thread-" + j);
}
for (Thread thread : threads) {// 啟動執行緒
thread.start();
}
}
}
執行後結果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個執行緒累加後的結果都是5,各個執行緒處理自己的本地變數值,執行緒之間互不影響
17.自定義註解
元註解
Java提供了4種元註解,專門負責註解其他註解使用
@Retention 表示需要在什麼級別儲存該註釋資訊(生命週期)
可選引數:
RetentionPolicy.SOURCE: 停留在java原始檔,編譯器被丟掉
RetentionPolicy.CLASS:停留在class檔案中,但會被VM丟棄(預設)
RetentionPolicy.RUNTIME:記憶體中的位元組碼,VM將在執行時也保留註解,因此可以通過反射機制讀取註解的資訊 --- 最常用
@Target 表示該註解用於什麼地方
可選引數:
ElementType.CONSTRUCTOR: 構造器宣告
ElementType.FIELD: 成員變數、物件、屬性(包括enum例項)
ElementType.LOCAL_VARIABLE: 區域性變數宣告
ElementType.METHOD: 方法宣告
ElementType.PACKAGE: 包宣告
ElementType.PARAMETER: 引數宣告
ElementType.TYPE: 類、介面(包括註解型別)或enum宣告
@Documented 將註解包含在JavaDoc中
@Inheried 執行子型別繼承父類中的註解
自定義註解:
eg:
自定義註解 --- MyAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定義註解
* 作用於方法和類
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
//為註解新增屬性
String color();
String value() default "我是xxx";//為註解屬性提供預設值
int[] array() default {1,2,3};
Gender gender() default Gender.MAN; // 新增一個列舉
// 新增列舉屬性
MetaAnnotation metaAnnotation() default @MetaAnnotation(birthday = "我的出身日期為1988-2-18");
}
定義一個列舉類 --- Gender
public enum Gender{
MAN{
public String getName(){
return "男";
}
},
WOMEN{
public String getName(){
return "女";
}
};
}
定義註解類 --- MetaAnnotation
public @interface MetaAnnotation{
String birthday();
}
解析註解:
/**
* 呼叫註解並賦值
* Created by hetiewei on 2016/10/12.
*/
@MyAnnotation(metaAnnotation = @MetaAnnotation(birthday = "我的出身日期為1991-2-27"),
color = "red", array = {23, 26 })
public class Test {
public static void main(String args[]){
//檢查類Test中是否包含@MyAnnotation註解
if (Test.class.isAnnotationPresent(MyAnnotation.class)){
//若存在則獲取註解,並解析
MyAnnotation annotation = Test.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
//解析註解中的內容
//1.獲取註解屬性
System.out.println(annotation.color());
System.out.println(annotation.value());
//2.獲取屬性陣列
int[] arrs = annotation.array();
System.out.println(arrs.toString());
//3.獲取列舉
Gender gender = annotation.gender();
System.out.println("性別:"+gender);
//4.獲取註解屬性
MetaAnnotation meta = annotation.metaAnnotation();
System.out.println(meta.birthday());
}
}
}
18.關於列舉 列舉類是一種特殊的類,它一樣有自己的Field,方法,可以實現一個或者多個介面,也可以定義自己的構造器
列舉與普通類有如下簡單區別:
(1). 列舉類可以實現一個或者多個介面,使用enum定義的列舉類預設繼承了java.lang.Enum類,而不是繼承Object類。其中java.lang.Enum類實現了java.lang.Serializable和java.lang.Comparable介面。
(2). 使用enum定義,非抽象的列舉類預設會使用final修飾,因此列舉類不能派生子類。
(3). 列舉類的構造器只能使用private訪問控制符,如果省略了構造器的訪問控制符,則預設使用private修飾;如果強制指定訪問控制符,則只能指定private修飾符。
(4). 列舉類的所有例項必須在列舉類的第一行顯示列出,否則這個列舉類永遠不能產生例項。列出這些例項時,系統會自動新增public static final修飾,無須程式設計師顯示新增。
所有的列舉類都提供了一個values方法,該方法可以很方便的遍歷所有的列舉值
(5) 列舉常用方法
name() ,toString() --- 返回此列舉例項名稱,優先使用 toString()
ordinal() --- 返回列舉例項的索引位置,第一個列舉值索引為0
public static T valueOf(Class enumType, String name)
--- 返回指定列舉類中指定名稱的列舉值,名稱必須與在該列舉類中宣告列舉值時所用的識別符號完全匹配,不允許使用額外的空白字元
eg:
public enum SeasonEnum{
//列出4個列舉例項
SPRING,SUMMER,FALL,WINTER;
}
解析:
1.列舉值之間以英文逗號(,)隔開,列舉值列舉結束後以英文分號作為結束
2.列舉值代表了該列舉類的所有可能例項
3.使用列舉值 EnumClass.variable eg: SeasonEnum.SPRING
4.列舉可作為switch條件表示式
5.獲取所有列舉值 EnumClass[] enums = EnumClass.values();
列舉類的屬性Field、方法和構造器
1.列舉類也是一種類,只是它是一種比較特殊的類,因此它一樣可以定義Field,方法
19.幾種集合類解析
1.HashMap 底層 陣列+連結串列,計算出hash值後,得到元素在陣列中存放的位置索引,
若不同元素hash值相同,即:有相同的存放位置,則在相同位置建立連結串列,採用頭插入法依次儲存元素
工作原理:
陣列+連結串列 以 Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模陣列的大小得到陣列的下標
如果多個key佔有同一個下標(碰撞),則使用連結串列將相同的key串起來
通過hash方法,通過put和get儲存和獲取物件,儲存物件時,將K/V傳給put()方法時,它呼叫hashCode()計算hash值得到bucket位置,
進一步儲存,HashMap會根據當前bucket的佔用情況自動調整容量(超過載入因子,容量擴充套件為2倍)。
獲取物件時,通過K,呼叫hashCode()得到bucket位置,並進一步呼叫equals()方法確定鍵值對。
如果發生碰撞時,HashMap通過連結串列將產生碰撞的元素組織起來,在Java8中,如果一個bucket中碰撞的元素超過某個限制(,預設8個),
則使用紅黑樹來替換連結串列,從而提高速度
2.HashSet 底層 是HashMap實現, 優點:利用雜湊表提供查詢效率, 缺點:元素不能重複
由於HashSet不允許元素重複,故需要判斷元素是否相同,
用hash表判斷元素是否相同的方法,即需要hashCode和equals兩個方法,對於hashSet,先通過hashCode判斷元素在雜湊表中的位置是否相同,在通過equals方法判斷元素內容是否相同
雜湊表如何判斷元素是否相同?
1> 雜湊值是否相同 :判斷的其實是物件的hashCode()方法是否相同(看物件是否在雜湊表的同一個位置)
2>內容是否相同:用equals方法判斷物件是否相同。
規則:若hash值不同,不必判斷物件內容,返回false;若hash值相同,有必要判斷物件內容,若在相同,返回true,否則false。
3.TreeSet 使用元素的自然順序,物件集合中元素進行排序,新增的元素需要自己實現Comparable介面,以便預設排序時呼叫其CompareTo()進行比較
2中自定義排序方式
1.元素的類,實現Comparable介面,實現compareTo()
2.給ThreeSet傳遞一個實現Comparator介面的引數物件
20.關於執行緒池
參考:
http://www.codeceo.com/article/java-thread-pool-deep-learn.html
http://www.codeceo.com/article/java-threadpoolexecutor.html
1.核心類:
ThreadPoolExecutor extends AbstractExecutorService implement ExecutorService 提供4個構造器
構造器引數:
corePoolSize: 核心池大小
預設情況,建立執行緒池後,池中執行緒數為0,當有任務來時,建立一個執行緒去執行任務,當執行緒池中執行緒數目達到corePoolSize後,會把任務放入快取佇列、
maxPoolSize: 執行緒池最大執行緒數
keepAliveTime:表示執行緒沒有任務執行時最多保持多久會終止
預設情況下,只有當執行緒池中的執行緒數大於corePoolSize時,keepAliveTime才會起作用,直到執行緒池中的執行緒數不大於corePoolSize,即當執行緒池中的執行緒數大於corePoolSize時,如果一個執行緒空閒的時間達到keepAliveTime,則會終止,直到執行緒池中的執行緒數不超過corePoolSize
unit: 引數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
workQueue: 阻塞佇列,用來儲存等待執行的任務
可選的阻塞佇列:
ArrayBlockingQueue
LinkedBlockingQueue --- 預設,用的最多
SynchronousQueue
PriorityBlockingQueue
threadFactory: 執行緒工廠,主要用來建立執行緒
handler: 表示當拒絕處理任務時的策略,
4種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務
2.執行緒池原理:
1.執行緒池狀態:
ThreadPoolExecutor中定義了一個Volatile變數runState表示當前執行緒池的狀態,使用volatile來保證執行緒之間的可見性
執行緒池的4種狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
解析:
1.建立執行緒池後,初始時,執行緒池處於RUNNING狀態
2.如果呼叫了shutdown()方法,則執行緒池處於SHUTDOWN狀態,此時執行緒池不能接受新任務,但會等待所有任務執行完畢
3.如果呼叫了shutdownNow()方法,執行緒池處於STOP狀態,此時執行緒池不能接受新任務,並且會嘗試終止正在執行的任務
4.當執行緒池處於SHUTDOWN或STOP狀態,並且所有工作執行緒已經銷燬,任務快取佇列已經清空或執行結束後,執行緒池被設定為TERMINATED狀態
2.任務的執行:
執行緒任務儲存在BlockingQueue中,通過execute(Runnable )來呼叫執行,
21.Java的類載入器
類載入器 --- 一個用來載入類檔案的類
Java原始碼通過javac編譯成類檔案,然後JVM來執行類檔案中的位元組碼,類載入器負責載入檔案系統、網路或其他來源的類檔案
JVM中類載入器的樹狀層次結構
Bootstrap ClassLoader 引導類載入器, 載入Java的核心庫(jre/lib/rt.jar),用C++程式碼實現,不繼承子java.lang.ClassLoader
Extension ClassLoader 擴充套件類載入器, 載入Java的擴充套件庫(jre/ext/*.jar), Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類
System ClassLoader 系統類載入器, 根據Java應用的類路徑(classpath)來載入Java類,
Java應用的類都是由它來完成載入的,可通過ClassLoader.getSystemClassLoader()獲取系統類載入器
自定義類載入器 通過繼承java.lang.ClassLoader類,實現自己的類載入器
Java類載入器的3個機制:
委託機制:
將載入器的請求交給父載入器,如果父類載入器找不到或不能載入這個類,則當前類載入器再載入它
可見性機制:
子類的載入器可以看見所有父類載入器載入的類,而父類載入器看不到子類載入器載入的類
單一性機制:
類僅被載入一次, 由 委託機制 確保子類載入器不會再次載入父類載入器載入過的類
類載入過程 3個步驟:
裝載:
連結:(驗證、準備、解析)
初始化:
裝載:
查詢並載入類的二進位制資料
連結:
驗證:確保被載入類資訊符合JVM規範、沒有安全方面問題
準備:為類的靜態變數分配記憶體,並將其初始化為預設值
解析:把虛擬機器常量池中的符號引用轉換為直接引用
初始化:
為類的靜態變數賦予正確的初始值
說明:
1.JVM會為每個載入的類維護一個常量池
2.類的初始化步驟:
1.如果這個類沒被載入和連結,則先進行載入和連結
2.如果這個類存在父類,如果類未初始化,則先初始化其父類
3.如果類中存在static塊,一次執行這些初始化語句
java.lang.ClassLoader類
根據一個指定的類名稱,找到或生成其對於的位元組程式碼,然後從這些位元組碼中定義出一個Java類,即:java.lang.Class類的一個例項
ClassLoader中的方法:
getParent() 返回該類載入器的父類載入器
loadClass(name) 載入名稱為name的類,返回結果是java.lang.Class的例項
findClass(name) 查詢名稱為name的類,返回結果是java.lang.Class的例項
findLoadedClass(name) 查詢名稱為name的已被載入過的類,返回結果是java.lang.Class的例項
resolveClass(Class<?> c) 連結指定的Java類
Java中的類載入過程
載入(可自定義類載入器) 連線 ( 驗證 準備 解析 ) 初始化
載入:
獲取二進位制位元組流 --> 將位元組流靜態儲存結構轉換為方法區的執行時資料結構 --> 在堆中生成Class物件
連線:
驗證:
檔案格式驗證: 1.參照Class檔案格式規範驗證
2.此階段基於位元組流經過此驗證後,位元組流才會進入方法區,後面的驗證都依賴與方法區的驗證
後設資料驗證: Java語法驗證,eg:該類是否繼承了不該繼承的類
位元組碼驗證: 執行時安全性檢查
符號引用驗證: 確保類中引用的類,欄位,方法都是可訪問的
準備:
設定類變數初始值 --- static類變數 初始值 , 注意:final比較特別!!!
1.設定類變數 --- static變數
2.設定變數初始值 (注意:非程式碼中定義的值,8種基本資料型別都有初始值 eg: static int a = 10, 準備階段會把a初始值賦值為0,初始化時,再賦值為10 )
3.對於final的值,設為程式碼中的值(eg:final static int a = 10 , 準備階段直接把 a 賦值為10)
解析:
將符號引用轉換為直接引用
1.符號引用: 用符號來表示所引用的目標
2.直接引用: 一個指向記憶體中目標物件地址的控制程式碼
初始化:
1.根據程式碼實現初始化類變數及其他資源 (準備階段,static類變數還是初始值,這裡賦值為程式碼中指定的值)
2.執行子類初始化方法時,先執行父類的初始化方法(static變數賦值,static程式碼段執行,先父類後子類)
22.Java反射 增加 裝飾模式 的適用性
裝飾模式:在不必改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能,它是通過建立一個包裝物件來包裹真實的物件,比生產子類更加靈活,
使用Java的動態代理實現裝飾模式,會具有更強的靈活性和適用性
裝飾模式有什麼特點呢?
1、裝飾物件和真實物件有相同的介面。這樣呼叫者就能以和真實物件相同的方式和裝飾物件互動。
2、裝飾物件包含一個真實物件的引用(即上面例子中的Ability介面)。
3、裝飾物件接受所有來呼叫者的請求,並把這些請求轉發給真實的物件。
4、裝飾物件可以在呼叫者的方法以前或以後增加一些附加功能。這樣就確保了在執行時,不用修改給定物件的結構就可以在外部增加附加的功能。
什麼樣的地方使用裝飾模式呢?
1、需要動態擴充套件一個類的功能,或給一個類新增附加職責。
2、需要動態的給一個物件新增功能,這些功能可以再動態的撤銷。
3、需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
4、 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類。
23.JVM的執行時棧幀 --- JVM執行程式的過程!!! --- 方法的執行過程 !!!
1.每個方法的執行,在JVM中都是對應的棧幀在JVM棧中的入棧到出棧的過程!!!
2.每個在JVM中執行的程式,都是由許多的幀切換產生的結果
參考:
http://blog.csdn.net/column/details/14217.html
棧幀: --- 執行緒安全!!!每個執行緒的棧幀相互獨立 ---> 區域性變數在多執行緒環境下執行緒安全的原因!!!
存放方法的區域性變數表、運算元棧、動態連結,方法返回值和一些額外的附加資訊
當前棧:
一個方法的呼叫鏈可能很長,當呼叫一個方法時,可能會有很多方法處於執行狀態,但對於執行引擎,置於JVM棧頂的棧幀才是有效的,這個棧幀稱為 當前棧
當前棧所關聯的方法稱為當前方法,執行引擎的所有指令都是針對當前棧幀進行操作的
區域性變數表:
內容: 存放方法的區域性變數
eg:方法引數,方法內定義的區域性變數,物件引用,returnAddress型別
在Java程式被編譯為class檔案時,這個表的容量最大值已經確定
訪問:
虛擬機器利用索引編號的遞增來對區域性變數表中定義的變數進行一次訪問(從0開始),而對於例項方法(非static方法),其區域性變數表的第0個索引是this,
這是可以在例項方法中使用this.name ......的原因
動態連線:
參考:http://blog.csdn.net/eric_sunah/article/details/8014865
方法的呼叫過程:
在虛擬機器執行時,執行時常量池會儲存大量符號引用,這些符號引用可以看做每個方法的間接引用,
如果代表棧幀A的方法要呼叫代表棧幀B的方法,則這個虛擬機器的方法呼叫指令就會以B方法的符號引用作為引數,
但因為符號引用並不是直接指向代表B方法的記憶體位置,所有在呼叫之前還必須要將符號引用轉換為直接引用,然後通過直接引用訪問到真正的方法!
注意:
靜態解析:
如果符號引用在類載入階段或第一次使用時轉化為直接引用,則這種轉換成為靜態解析
動態連線:
如果在執行期間轉換為直接引用,這種轉換稱為動態連線
棧幀A 常量池 棧幀B
區域性變數表 區域性變數表
A方法的符號引用
運算元棧 運算元棧
B方法的符號引用
動態連線 動態連線
字串常量等
返回地址 返回地址
方法返回地址
1.正常退出:根據方法定義決定是否要返回值給上層呼叫者
2.異常退出:不會傳遞返回值給上層呼叫者
注意:
1. 不管那種方式結束,在退出當前方法時,都會跳轉到當前方法被呼叫的位置!!!
如果正常退出,則呼叫者的PC計數器的值可作為返回地址,
如果異常退出,則需要通過異常處理表來確定
2. 方法的一次呼叫對應著棧幀在虛擬機器中的一次入棧出棧操作!!!
方法退出時做的事情:
恢復上層方法的區域性變數表以及運算元棧,如果有返回值,就把返回值壓入到呼叫者棧幀的運算元棧中,
還會把PC計數器的值調整為方法呼叫入口的下一條指令
24.JVM的記憶體溢位分析和引數調優
1.JVM調優 http://blog.csdn.net/eric_sunah/article/details/7862114
1.JVM記憶體引數調優
-Xms 設定初始化堆的記憶體
-Xmx 設定堆最大使用記憶體
-Xss 設定每個執行緒的棧大小
-Xmn 設定年輕代大小
eg:
java -Xmx4096m -Xms4096m -Xmn2g -Xss128k
或
java -Xmx4g -Xms4g -Xmn2g -Xss128k
設定JVM堆最大可以記憶體為4096M,初始記憶體為4096M(-Xms設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配記憶體)
設定JVM年輕代大小為2G JVM堆記憶體 = 年輕代大小 + 年老代大小 + 持久代大小
持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統效能影響較大,推薦配置為整個堆的3/8
設定每個執行緒的棧大小為128k
JDK5+ 執行緒棧預設1M, 相同實體記憶體下,減小該值能生成更多執行緒,但作業系統對一個程式內的執行緒數有限制,最好不超過5000
如果方法遞迴太深,則可能耗盡執行緒棧,報出 StackOverflow !!! 執行緒棧記憶體溢位 <--- 方法呼叫太深
eg:設定堆記憶體中的記憶體分配比例
java -Xmx4g -Xms4g -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=64m -XX:MaxTenuringThreshold=0
-Xmn2g 設定年輕代大小為2G
-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所佔比值為1:4,年輕代佔整個堆的1/5
-XX:SurvivorRatio=4 設定年輕代中from和to區的比例,Eden區(from)與Survivor區(to)的大小比值為4:1:1,即 一個 Survivor 佔 年輕代的1/6
特別注意:
上面的比值 4 <=等價=> 1:4
-XX:MaxPermSize=64m 設定持久代大小為64M
-XX:MaxTenuringThreshold=0:設定垃圾最大年齡
小結:
1,整個堆包括年輕代,老年代和持久代。其中年輕代又包括一個Eden區和兩個Survivor區。
2,年輕代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
2,持久代:
-XX:PermSize
-XX:MaxPermSize
3,年輕代和老年代的比例:
-XX:NewRatio(年輕代和老年代的比值,年輕代多,除去持久代)
當設定了-XX:+UseConcMarkSweepGC後,會使-XX:NewRatio=4失效,此時需要使用-Xmn設定年輕代大小
4,Eden與Survivor的比例
-XX:SurvivorRatio(Eden區與兩個Survivor區的比值,Eden區多)
2.GC引數設定
並行收集器 --- 吞吐量優先,適合後臺處理
eg:
-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
解析:
-XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集
-XX:ParallelGCThreads=20:配置並行收集器的執行緒數,即:同時多少個執行緒一起進行垃圾回收。此值最好配置與處理器數目相等
-XX:+UseParallelOldGC:配置年老代垃圾收集方式為並行收集,JDK6.0支援對年老代並行收集
併發收集器 --- 響應時間優先,保證系統響應時間,減少垃圾收集時的停頓時間,適合應用伺服器和典型領域等
eg:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設定年老代為併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設定。
-XX:+UseParNewGC:設定年輕代為並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值。
3.常見配置彙總
堆設定
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設定年輕代大小
-XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:MaxPermSize=n:設定持久代大小
收集器設定
-XX:+UseSerialGC:設定序列收集器
-XX:+UseParallelGC:設定並行收集器
-XX:+UseParalledlOldGC:設定並行年老代收集器
-XX:+UseConcMarkSweepGC:設定併發收集器
垃圾回收統計資訊
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設定
-XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。
-XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間
-XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)
併發收集器設定
-XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。
4.調優總結
年輕代大小選擇
響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的物件。
吞吐量優先的應用:儘可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設定,一般要考慮併發會話率和會話持續時間等一些引數。
如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;
如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:
併發垃圾收集資訊
持久代併發收集次數
傳統GC資訊
花在年輕代和年老代回收上的時間比例
減少年輕代和年老代花費的時間,一般會提高應用的效率
吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期物件,
減少中期的物件,而年老代盡存放長期存活物件。
較小堆引起的碎片問題
因為年老代的併發收集器使用標記、清除演算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的物件。
但是,當堆空間較小時,執行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,
然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮。
-XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮
2.JVM記憶體溢位分析 --- OutOfMemoryError 不屬於Exception,繼承自Throwable
1.堆記憶體溢位 --- 不斷建立物件,並且不釋放,導致GC無法回收,堆記憶體移除 Java heap space
eg: 製造堆記憶體溢位的程式
1.降低修改虛擬機器堆記憶體大小 -Xms20m -Xmx20m
2.不斷建立強引用物件,方式GC回收
public static void main(String[] args) {
headOutOfMemory();
}
/*
* -verbose:gc -XX:+PrintGCDetails -verbose:gc
* -XX:+HeapDumpOnOutOfMemoryError
*
* -Xms20m -Xms20m
*
*/
static void headOutOfMemory() {
long count = 0;
try {
List<Object> objects = new ArrayList<Object>();
while (true) {
count++;
objects.add(new Object());
}
} catch (Throwable ex) {
System.out.println(count);
ex.printStackTrace();
}
}
}
異常資訊:
java.lang.OutOfMemoryError: Java heap space
2.棧記憶體溢位 --- 棧主要存放棧幀(區域性變數表(基本資料型別,物件引用,returnAddress型別),運算元棧,動態連結,方法出口資訊),
1.StackOverflowError ---- 當執行緒棧的空間大於虛擬機器所允許時,丟擲 StackOverflowError
執行緒棧,因遞迴或方法呼叫太深,導致超過執行緒棧設定時,拋 StackOverflowError
eg:
自定義執行緒棧溢位
1.降低執行緒棧大小 -Xss4k
2.方法迴圈遞迴
public class JVMStackSOF {
/**
* (1) 在hotspot虛擬機器中不區分虛擬機器棧(-Xss)和本地方法棧(-Xoss),且只有對Xss引數的設定,才對棧的分配有影響
*
* (2)
* 由於StackOverflowError->VirtualMachineError->Error
* ->Throwable,所以catch的時候如果用Exception的話將捕獲不到異常 Stack length 會隨著-Xss的減少而相應的變小
*/
private int stackNumber1 = 1;
public void stackLeck1() {
stackNumber1++;
stackLeck1();
}
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeck1();
} catch (Throwable ex) {
System.out.println("Stack length:" + jvmStackSOF.stackNumber1);
ex.printStackTrace();
}
}
}
異常資訊:
java.lang.StackOverflowError
2.OutOfMemoryError ---- 棧空間不足,丟擲OutOfMemoryError
JVM棧,整體記憶體不足時,拋OutOfMemoryError
eg:
自定義JVM棧溢位
1.JVM中除了堆和方法區,剩餘的記憶體基本都由棧佔用
2.每個執行緒都有獨立的棧空間(堆、方法區是執行緒公用)
3.如果-Xss調大每個執行緒的棧空間,可建立的執行緒數量必然減少
public class JVMStackOOM {
/**
* (1)不停的建立執行緒,因為OS提供給每個程式的記憶體是有限的,且虛擬機器棧+本地方法棧=(總記憶體-最大堆容量(X模型)-最大方法區容量(
* MaxPermSize)),於是可以推斷出,當每個執行緒的棧越大時,那麼可以分配的執行緒數量的就越少,當沒有足夠的記憶體來分配執行緒所需要的棧空間時,
* 就會丟擲OutOfMemoryException
* (2)由於在window平臺的虛擬機器中,java的執行緒是隱射到作業系統的核心執行緒上的,所以執行一下程式碼時,會導致作業系統假死(我就嚐到了血的代價)
*/
private static volatile int threadNumber = 0;
public void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
threadNumber++;
while (true) {
System.out.println(Thread.currentThread());
}
}
}.start();
}
}
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
try {
jvmStackOOM.stackLeakByThread();
} catch (Throwable ex) {
System.out.println(JVMStackOOM.threadNumber);
ex.printStackTrace();
}
}
}
異常資訊如下:
java.lang.OutOfMemoryError:unable to create new native thread
3.方法區溢位
方法區:存放JVM載入的類資訊,常量,執行時常量池,靜態變數,編譯期編譯後的程式碼等
異常資訊:
java.lang.OutOfMemoryError: PermGen space
eg:
自定義方法區溢位程式碼
1.通過不斷產生類資訊來佔用方法區記憶體
2.調整 -XX:PermSize=10m --XX:MaxPermSize=10m,來降低方法區記憶體大小
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 利用CGLib技術不斷的生成動態Class,這些Class的資訊會被存放在方法區中,如果方法區不是很大會造成方法去的OOM
*
*
* -XX:PermSize=10m -XX:MaxPermSize=10m
* */
public class MethodAreaOOM {
static class Test {}
public static void main(String[] args) {
try{
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(arg0, arg2);
}
});
System.out.println(enhancer.create());
}
}catch(Throwable th){
th.printStackTrace();
}
}
}
異常資訊:java.lang.OutOfMemoryError: PermGen space
25.JVM記憶體分配策略與回收
1.分配策略
1.物件優先在Eden區上分配
2.大物件直接分配在老年區
-XX:PretenureSizeThreshold 引數設定直接放入老年區的物件大小
3.長期存活的物件直接進入老年區
-XX:MaxTenuringThreshold 引數設定物件年齡,經歷幾次gc可以進入老年區
JVM為每個物件定義了年齡計數器,
如果物件在Eden出生並經過第一次Minor GC後任然存活,並能被Survivor容納,將被移到Survivor空間中,
並將物件年齡設為1,物件在Survivor區每熬過一次Minor GC,年齡+1,
當年齡達到一定程度(預設15,可引數-XX:MaxTenuringThreshold設定)時,進入到老年代中
2.記憶體回收
1.Minor GC 發生在年輕代的GC,當JVM無法為一個新物件分配空間時,觸發Minor GC,清理年輕代記憶體,大多數物件生命週期短,所以Minor GC 非常頻繁,而且速度較快
觸發條件:
Eden區滿時,觸發Minor GC
2.Full GC 發生在年老代的GC
觸發條件:
1.呼叫System.gc(),系統建議執行Full GC,但不一定執行
2.老年代空間不足
3.方法區空間不足
4.通過Minor GC後進入老年代的物件平均大小,大於老年代可用記憶體空間
5.由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小
26.AOP應用和實現
AOP優化改造:http://blog.csdn.net/xvshu/article/details/46288953
1.關於Aspectj
獲取目標方法資訊 --- JoinPoint Spring只支援方法執行的JoinPoint
JoinPoint裡包含了如下幾個常用的方法:
Object[] getArgs:返回目標方法的引數
Signature getSignature:返回目標方法的簽名
Object getTarget:返回被織入增強處理的目標物件
Object getThis:返回AOP框架為目標物件生成的代理物件
注意:當使用@Around處理時,我們需要將第一個引數定義為ProceedingJoinPoint型別,該類是JoinPoint的子類
2.5種增強處理
Before、 在某個連線點JoinPoint之前執行, 不能阻止連線點前的執行
Around、 包圍一個連線點的通知,可以在方法呼叫前後完成自定義的行為
AfterReturning、 在某連線點正常完成後執行的通知,不包括丟擲異常的情況
After、 某連線點退出時執行(不同正常返回還是異常退出,都會執行的通知,類似finally功能,可以用於釋放連線和資源)
AfterThrowing 在方法丟擲異常退出時執行的通知
--- 5種增強處理中,織入增強處理的目標方法、目標方法的引數和被織入增強處理的目標物件等
任何一種織入的增強處理中,都可以獲取目標方法的資訊
切點表示式:
1.execution()表示式
eg:
execution (* com.sample.service.impl..*. *(..))
1.execution() 表示式主體
2.第一個* : 表示返回型別, * 表示所有型別
3.包名: 要攔截的包,後面2個句點表示當前包和當前包的所有子包,即:com.sample.service.impl包、及其子孫包下所有類的方法
4.第二個* : 表示類名, * 表示所有類
5.第三個* : 表示方法名, * 表示所有方法, *(..) 中的2個句點表示方法引數, 2個句點表示任何引數
2.自定義註解
1.定義切點方法
//自定義註解方式
@Pointcut("annotation(com.jay.annotation.MyAnnotation)")
//@Pointcut("execution (* com.gcx.controller..*.*(..))") --- execution()切點表示式方式
public void controllerAspect(){}
2.在增強方法上,使用切點方法
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint){
}
eg:
使用5種增強處理
1.類上新增 @Aspect註解
2.方法上新增 4中增強處理註解
3.特別注意:
@Around中引數是 ProceedJoinPoint, 其他4類增強用 JoinPoint
@Aspect
public class AdviceTest {
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模擬許可權檢查...");
System.out.println("@Before:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:引數為:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被織入的目標物件為:" + point.getTarget());
}
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:執行目標方法之前...");
//訪問目標方法的引數:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改變後的引數1";
}
//用改變後的引數執行目標方法
Object returnValue = point.proceed(args);
System.out.println("@Around:執行目標方法之後...");
System.out.println("@Around:被織入的目標物件為:" + point.getTarget());
return "原返回值:" + returnValue + ",這是返回結果的字尾";
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模擬日誌記錄功能...");
System.out.println("@AfterReturning:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:引數為:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值為:" + returnValue);
System.out.println("@AfterReturning:被織入的目標物件為:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模擬釋放資源...");
System.out.println("@After:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:引數為:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被織入的目標物件為:" + point.getTarget());
}
//標註該方法體為異常通知,當目標方法出現異常時,執行該方法體
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")
public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){
...
}
}
3.動態代理實現
1.靜態代理 --- 代理模式
兩個類實現同一個介面,在代理類的介面方法中,呼叫被代理物件的介面方法,同時在代理類的介面方法中,呼叫被代理類方法前後新增邏輯
2.動態代理 --- JDK動態代理 和 cglib 動態代理(位元組碼增強技術,效率高)
參考:http://blog.csdn.net/zpf336/article/details/52086180
http://blog.csdn.net/wenbo20182/article/details/52021096
2種動態代理區別:
1.JDK動態代理要求被代理類要實現介面,而cglib不需要
2.cglib能根據記憶體中為其建立子類(代理物件)
1.JDK的動態代理 --- 通過建立一個實現InvocationHandler介面的中間物件,實現動態代理
優點:不必要求代理者和被代理者實現相同介面
缺點:僅支援介面代理, JDK動態代理需要實現類通過介面定義業務方法,對於沒有介面的類,無法代理
eg:
自定義的基於JDK的動態代理類
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用JDK動態代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy()
greeting.sayHello("Jack");
2.cglib動態代理 --- 位元組碼增強技術,效率高
原理:
通過位元組碼技術,為一個類建立子類,並在子類中採用方法攔截的技術,攔截所有父類方法的呼叫,順勢織入橫切邏輯
優點:位元組碼增強技術,CGLib建立的動態代理物件效能比JDK建立的動態代理物件的效能高不少,
但是CGLib在建立代理物件時所花費的時間卻比JDK多得多,所以對於單例的物件,因為無需頻繁建立物件,用CGLib合適,
反之,使用JDK方式要更為合適一些
缺點:CGLib由於是採用動態建立子類的方法,對於final方法,無法進行代理
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用cglib動態代理
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
27.關於JVM架構
參考:http://www.ityouknow.com/java/2017/03/01/jvm-overview.html
1.Java記憶體模型
記憶體模型:
描述程式中各個變數(例項域、靜態域、陣列元素)之間的關係,以及在計算機系統中將變數儲存到記憶體,從記憶體取出變數這樣的底層細節
記憶體模型規則:
原子性:
約定了:訪問儲存單元內任何型別欄位的值以及對其進行更新操作時,必須保證其是原子的
即:獲得或初始化某一些值時(),該值的原子性在JVM內部是必須得到保證的
可見性:
一個執行緒修改的狀態,對另一個執行緒是可見的,
即:一個執行緒修改的結果,另一個執行緒馬上就能看到
eg:
volatile修飾的變數具有可見性,不允許執行緒內部快取和重排序,但不保證原子性
可見性規則約束下,定義了一個執行緒在哪種情況下可以訪問或影響另外一個執行緒,以及從另外一個執行緒的可見區域讀取相關資料、將資料寫入到另外一個執行緒內
可排序性:
為了提高效能,編譯器和處理器可能會對指令做重排序
volatile修飾的變數不允許執行緒內部快取和重排序
Java記憶體模型: --- JMM Java Memory Model
JMM: 是控制Java執行緒之間、執行緒和主存直接通向的協議
JMM定義了執行緒和主存之間的抽象關係:
執行緒之間的共享變數儲存在主存(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體儲存了該執行緒以讀/寫共享變數的副本
2.JVM例項內部結構
子系統:
類載入器作為JVM的子系統,針對Class檔案進行檢測來載入對應的類
執行引擎:
負責程式碼的解釋和執行
記憶體區域:
儲存位元組碼、類資訊、物件、引數、變數等
方法區: --- 執行緒共享
儲存類的裝載資訊(類資訊),
靜態變數(類變數),
執行時常量池
堆: --- 執行緒共享
物件、陣列記憶體對分配:
當一個Java程式建立一個物件或者一個陣列時,JVM例項會針對該物件和陣列分配一個新的記憶體堆空間。在JVM例項內部,只存在一個記憶體堆的例項,所有的依賴該JVM的Java程式都共享該例項
程式記憶體堆分配:
多個Java程式啟動時,會得到JVM分配給自己的對空間,多個Java程式的堆空間時相互獨立的
JVM棧: --- 執行緒私有
對於執行緒記憶體棧分配:
當一個新執行緒啟動時,JVM為其建立獨立記憶體棧,
記憶體棧由棧幀構成,棧幀有2中操作:出棧和入棧
當前執行緒方法 --- 正在被執行緒執行的方法, 該方法的棧幀稱為 當前幀
對於方法:
當一個執行緒呼叫某個方法時,JVM建立並將一個新幀壓入到記憶體棧中,這個幀稱為當前棧幀,
當該方法執行時,JVM使用記憶體棧來儲存 引數引用、區域性引用變數、基本型別數值、返回值等相關資料
無論方法正常結束還是異常結束,JVM都彈出或丟棄該棧幀,上一幀方法成為當前幀
本地方法棧:
儲存了本地Java方法呼叫狀態,其狀態包括區域性變數、被呼叫的引數、它的返回值、以及中間計算結果
程式計數器: --- 執行緒私有
每個執行緒都有自己的PC暫存器,通過計數器來指示下一條指令執行
3.JVM記憶體分配策略
靜態儲存:
編譯時,能確定每個資料在執行時需要的儲存空間,因而在編譯時就給它們分配固定的記憶體空間
此分配策略,要求程式碼中不允許有可變資料結構存在,也不允許巢狀或遞迴結構出現(無法計算需要記憶體空間)
eg:
static final 全域性常量
棧式儲存:
動態儲存分配,由一個類似堆疊的執行棧來實現,按先進後出原則分配
程式對資料所需記憶體空間未知,只有到執行時才能知道佔用空間
堆式儲存:
專門負責在編譯時或執行時,無法確定儲存要求的資料結構的記憶體分配
eg:
可變字串和物件例項
4.物件分配規則:
1.物件優先分配在年輕代的Eden區,如果Eden區沒有足夠空間,JVM執行一次 Minor GC
2.大物件直接進入老年代(大物件:需要大量連續記憶體空間的物件, 可引數設定大物件大小),目的:避免在Eden區和2個Survivor區直接進行大量的記憶體拷貝 <--- 新生代採用複製演算法收集記憶體
3.長期存活的物件進入老年代, JVM為每個物件定義了一個年齡計數器,如果物件經過1次Minor GC,物件進入Survivor區,之後沒經過一次Minor GC,物件年齡+1, 直到達到閾值,才進入老年代 (可引數設定年齡計數器大小)
4.動態判斷物件年齡。 如果Survivor區中相同年齡的所有物件大小總和,大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入老年代
5.空間分配擔保。 每次進行Minor GC時, JVM會計算Survivor區移至老年代的物件的平均大小,如果這個值大於老年區剩餘值,則進行一個Full GC
5.GC演算法 垃圾回收
物件存活判斷:
引用計數 --- 每個物件有應用計數屬性,新增一個引用,計數+1, 應用釋放,計數-1,計數為0,可回收 ---- 無法解決物件迴圈引用問題!
可達性演算法(根搜尋演算法) --- 從GC Roots開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到 GC Roots沒有任何引用鏈可達時, 證明此物件不再用,可回收
可作為GC Roots的物件:
虛擬機器棧中引用的物件
方法區中的靜態屬性引用的物件
方法區中常量引用的物件
本地方法棧中JNI引用的物件
特別注意:
GC管理的主要是 Java 堆,一般情況下只針對堆,進行垃圾回收,
方法區、棧、本地方法區不被GC鎖管理,因而選擇這些區域內的物件作為GC Roots,
被GC Roots引用的物件不被GC回收
GC演算法: 3種基礎演算法
參考:http://blog.csdn.net/java2000_wl/article/details/8022293
標記-清除演算法
分"標記" 和 "清除" 兩個階段
首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件
缺點:
標記和清除過程效率低
標記清除後,產生大量不連續的記憶體碎片
複製演算法
將可用記憶體按容量分為相等的2塊,每次只使用其中的一塊,
當這塊記憶體用完了,就將還存活的物件複製到另外一塊上面,然後把已使用過的記憶體空間一次清理掉
缺點:
記憶體縮小為原來的一半
優點:
不會有記憶體碎片,
只需要移動堆的記憶體指標,按順序分配記憶體即可,實現簡單,執行高效
標記-壓縮(整理)演算法
標記與第一種演算法一樣類似,但後續操作不是直接清理物件,而是讓所有存活物件都向一端移動,
並更新引用其物件的指標,然後直接清理掉端邊界意外的記憶體
缺點:
在標記-清除的基礎上,需要進行物件的移動,成本相對較高
優點:
不會產生記憶體碎片
JVM分代收集演算法
JVM把堆記憶體分為新生代和老年代, 根據各個代的特點,採用合適的收集演算法
新生代 --- 複製演算法 !!!
每次垃圾收集都有大批物件死去,只有少量存活,適合複製演算法,只需付出少量存活物件的複製成本就可完成收集
老年代 --- 標記-整理演算法 !!!
老年代中物件存活率高,沒有額外空間對它進行分配擔保,必須使用 "標記-壓縮" 演算法進行回收
垃圾回收器:
Serial收集器 --- 序列收集器,最古老,最穩定,效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收
ParNew收集器 --- Serial收集器的多執行緒版本
Parallel收集器 --- 類似ParNew,更關注系統的吞吐量
Parallel Old收集器 --- 使用多執行緒 和 標記-整理 演算法
CMS收集器 --- 是一種以獲取最短回收停頓時間為目標的收集器
G1收集器 --- 面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵
28.雜湊表 和 HashMap原理
雜湊表和雜湊演算法原理:
http://blog.csdn.net/duan19920101/article/details/51579136
http://blog.csdn.net/tanggao1314/article/details/51457585
一種根據關鍵字直接訪問記憶體儲存位置的資料結構
雜湊函式: 通過雜湊表,資料元素的存放位置和資料元素關鍵字之間建立起某種對應關係, 關鍵字到記憶體的對映
hash表以及HashMap原理:
http://www.code123.cc/258.html
HashMap原理:
HashMap基於雜湊函式,使用put(key,value)儲存物件,使用get(key)獲取物件,
put傳遞鍵值對,先對鍵呼叫hashCode()方法,返回hashCode()用於找到bucket位置來儲存Entry物件,
HashMap是在bucket中儲存鍵物件和值物件,作為 Map.Entry
bucket儲存LinkedList,而LinkedList中儲存的是key-value物件 --> Map.Entry
HashMap在Map.Entry靜態內部類中實現儲存key-value對,
HashMap使用雜湊演算法,在put和get方法中,使用hashCode()和equals()方法,
通過傳遞key-value對,呼叫put方法時,HashMap呼叫鍵物件(key)的hashCode()計算hashCode,然後bucket位置來儲存物件(根據key查詢value存放地址),
如果多個key的hashCode相同,則是發生碰撞,物件將儲存在同一個bucket位置的LinkedList的下一個節點中(鍵值對物件 <==> Map.Entry,Entry儲存在LinkedList中)
兩個物件的hashCode相同會發生什麼?
1.兩個物件的hashCode相同,但它們可能並不相等 hashCode() 和 equals()
2.HashMap的key-value抽象成Map.Entry, Entry儲存在LinkedList中, 每個bucket位置對應一個LinkedList,用於解決雜湊碰撞
3.如果hashCode相同,則它們的bucket位置相同, 儲存時會發生碰撞,
如果傳送碰撞,後一個新增的物件,會存放在bucket位置對應的LinkedList的下個節點中
如果兩個鍵的hashCode相同,如何獲取值物件?
1.get()根據key的hashCode去查詢bucket位置,然後獲取對應的value物件
2.如果兩個key的hashCode相同,則兩個key對應的Entry都儲存在同一個bucket位置的LinkedList中
3.HashMap的LinkedList中儲存的是鍵值對Map.Entry物件
4.根據key的hashCode找到bucket位置後,呼叫key.equals()方法找到LinkedList中正確的節點,最終找到要查的value值物件
5.注意:
hashCode()用於直接定位bucket位置
equals() 用於獲取值物件時使用
如果HashMap大小超過了負載因子定義的容量,怎麼辦?
1.預設負載因子是0.75,即:一個map填滿75%的bucket時,將會建立原來HashMap大小的2倍的bucket陣列,來重新調整map的大小,
並將原來的物件放入新的的bucket陣列中 --- 這個過程稱為 rehashing --- 因為它呼叫hash方法找到新的bucket位置
重新調整HashMap大小時,存在什麼問題?
多執行緒下,可能產生條件競爭
eg:
如果2個執行緒都發現HashMap需要重新調整大小,它們會同時嘗試調整大小。
在調整大小過程中,儲存在LinkedList中的元素的次序將會反過來。
因為移動到新的bucket位置時,HashMap並不會將元素放到LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷,
如果條件競爭發生了,就死迴圈了
多執行緒環境下,使用HashTable或ConcurrentHashMap替代HashMap
注意:
1.String、Integer適合作為HashMap的key
原因:
String是不可變的,final的,已經重寫了equals()和hashCode()方法
如果兩個不相等的物件返回不同的hashCode,碰撞的機率就會小,進而提高HashMap的效能
2.只要遵循equals()和hashCode()的定義規則,並且當物件插入到Map中之後不會再改變了,可以自定義物件作為key,
3.使用ConcurrentHashMap替代HashTable
HashTable是同步的,執行緒安全的,但ConcurrentHashMap同步效能更好,其採用了分段鎖
4.HashTable和ConcurrentHashMap的區別
1.HashTable每次同步執行是,都要鎖住整個結構
2.ConcurrentHashMap將hash表分為16個bucket桶(預設值),採用分段鎖,get、put、remove等操作只鎖當前需要用到的桶
eg:
原先只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,讀執行緒不受限制),提升了併發性
總結:
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取物件。當我們將鍵值對傳遞給put()方法時,它呼叫鍵物件的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值物件。當獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。HashMap使用連結串列來解決碰撞問題,當發生碰撞了,物件將會儲存在連結串列的下一個節點中。 HashMap在每個連結串列節點中儲存鍵值對物件。
當兩個不同的鍵物件的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的連結串列中。鍵物件的equals()方法用來找到鍵值對
HashTable實現原理:
實現原理與HashMap類似,但為了執行緒安全,HashTable中幾乎所有的public方法都用synchronized做了同步處理,有些方法也是在內部通過 synchronized 程式碼塊來實現
Hashtable 與 HashMap 的簡單比較
HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵對映到相應值的類的抽象父類,而 AbstractMap 是基於 Map 介面的實現,它以最大限度地減少實現此介面所需的工作。
HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時候,呼叫 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap則不是。我們可以看一下原始碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 程式碼塊來實現。
所以有人一般都建議如果是涉及到多執行緒同步時採用 HashTable,沒有涉及就採用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法建立了一個執行緒安全的 Map 物件,並把它作為一個封裝的物件來返回。
不考慮效能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap)來替換HashMap,這兩種方式基本都是對整個 hash 表結構做鎖定操作的
ConcurrentHashMap實現原理: --- 依賴於Java記憶體模型
參考:http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
ConcurrentHashMap結果中包含Segment的陣列,預設併發基本,建立包含16個Segment物件的陣列,
每個Segment又包含若干個雜湊表的桶,每個桶是由HashEntry連線起來的一個連結串列,
如果key能夠均勻雜湊,每個Segment大約守護整個雜湊表桶總數的1/16
併發讀些操作:
執行 put 方法的時候,會需要加鎖來完成,但加鎖操作是針對的hash值對應的Segment,而不是整個ConcurrentHashMap,因為put操作只是在某個Segment中完成,並不需要對整個ConcurrentHashMap加鎖,
此時,其他執行緒可以對另外的Segment進行put操作,雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖
同時,讀執行緒並不會因為本執行緒的加鎖而阻塞
在理想狀態下,ConcurrentHashMap 可以支援 16 個執行緒執行併發寫操作(如果併發級別設定為 16),及任意數量執行緒的讀操作
總結:
雜湊表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 物件的不變性和用 volatile 型變數協調執行緒間的記憶體可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發效能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個併發雜湊對映表的實現,它允許完全併發的讀取,並且支援給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。
在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全域性的鎖來同步不同執行緒間的併發訪問。同一時間點,只能有一個執行緒持有鎖,也就是說在同一時間點,只能有一個執行緒能訪問容器。這雖然保證多執行緒間的安全併發訪問,但同時也導致對容器的訪問變成序列化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
用分離鎖實現多個執行緒間的更深層次的共享訪問。
用 HashEntery 物件的不變性來降低執行讀操作的執行緒在遍歷連結串列期間對加鎖的需求。
通過對同一個 Volatile 變數的寫 / 讀訪問,協調不同執行緒間讀 / 寫操作的記憶體可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 物件的不變性及對同一個 Volatile 變數的讀 / 寫來協調記憶體可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於雜湊對映表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。
通過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高
29.關於Comparable和Comparator介面
1.Comparable和Comparator介面被用來對物件集合或者陣列進行排序
2.內部排序,物件類實現Comparable介面,重寫 CompareTo()方法
2.外部集合排序,呼叫Collections.sort(collection, Comparator<T>)對集合元素排序
eg:
Collections.sort(list,new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
if (user1.getName().equals(user2.getName())) {
return user1.getAge() - user2.getAge();
} else {
return user1.getName().compareTo(user2.getName());
}
}
});
30.OAuth認證流程
參考:http://www.code123.cc/1671.html
31.Java NIO
參考:http://blog.csdn.net/hxpjava1/article/details/56282385
概念:
專門為提高I/O吞吐量而設計,NIO通過Reactor模式的事件驅動機制來達到 Non Blocking
Reactor -- 反應器
將事件註冊到Reactor中,當有相應的事件發生時,Reactor告訴我們哪些事情發生了,我們根據具體的事件去做相應的處理
通道和緩衝區: 標準IO基於位元組流和字元流進行操作,而NIO基於通道Channel和緩衝區Buffer進行操作,資料總是從通道讀取到緩衝區,或從緩衝區寫入到通道中
非同步IO:NIO可以非同步的使用IO,當執行緒從通道讀取資料到緩衝區時,執行緒還可進行其他事情,當資料被寫入到緩衝區時,執行緒可以繼續處理它。從緩衝區寫入通道也類似
Selectors選擇器:選擇器用於監聽多個通道的事件(eg:連線開啟,資料到達等),因此,單執行緒可以監聽多個資料通道
eg:
Selector執行單執行緒處理多個Channel,如果應用開啟了多個連線(通道),但每個連線流量都很低,使用Selector就會很方便, 如:在一個聊天伺服器中
優點:
舊IO對檔案操作只能一個位元組一個位元組或一行一行的讀,對Socket IO會阻塞,可以為每個Socket建立一個Thread,但開銷太大
NIO 對Socket IO可以實現非阻塞,可用單執行緒管理多個通道,
NIO使用緩衝區,File IO和Socket IO都是和Buffer緩衝區互動讀取
NIO將通道資料讀到緩衝區中再進行操作,避免逐位元組或逐行讀取的效能開銷
從Channel讀取到Buffer -->
Channel Buffer
<--從Buffer寫入到Channel
NIO和IO如何影響程式的設計?
對NIO和IO類的API呼叫
資料處理
用來處理資料的執行緒數
使用場景:
1.聊天伺服器 需要管理同時開啟的成千上萬個連線,但這些連線每次只傳送少量的資料
2.P2P網路中, 需要維持許多開啟的連線到其他計算機上,使用一個單獨的執行緒來管理所有出戰連線
3.其他流量小, 連線多的場景
不適合場景:
1.少量連線佔用高頻寬,一次傳送大量資料 --- 檔案伺服器(適合IO實現,一個連線通過一個執行緒處理)
NIO核心模組:
Selector(選擇器):
1.Selector允許單執行緒處理多個Channel pk 舊IO的多執行緒處理, 效能更高
一個單執行緒選擇器可監控多個通道的多個事件(eg:連線開啟,資料到達等事件)
2.使用Selector,需要向它註冊一個Channel,然後輪詢呼叫它的select()方法,該方法將阻塞,
當註冊的某個通道準備好要進行IO操作時,返回已選擇鍵的個數,
此時通過selectedKeys獲得已選擇的鍵,就可進行相關的IO操作,
選擇鍵SelectionKey 是用來連線 Selector 和 Channel
直到這裡註冊中的Channels中有一個準備好的事件,
一旦這個方法返回,這個執行緒將會執行這些事件
事件的例項是進來的連線,接收到的資料等等
3. 要使用Selector,得向Selector註冊Channel,然後呼叫它的select()方法,
該方法會一直阻塞到某個註冊的通道有事件就緒,一旦這個方法返回,執行緒就可以處理這些事件(新連線進來,資料接收等)
---> Channel
Thread ---> Selector ---> Channel
---> Channel
4.NIO的選擇器允許一個單獨的執行緒來監視多個輸入通道,可以註冊多個通道使用一個選擇器,然後使用一個單獨的執行緒來 "選擇" 通道,
這些通道里已經有可以處理的輸入,或選擇已準備寫入的通道, 這種機制,使得一個單獨執行緒很容易來管理多個通道
Channel(通道):
1.4種Channel
FileChannel 檔案IO
ServerSocketChannel TCP IO
SocketChannel TCP IO
DatagramChannel UDP IO
Buffer(緩衝區):
緩衝區 --- 記憶體中預留指定位元組數的記憶體空間,用來對輸入、輸出的資料作臨時儲存, IO操作中資料的中轉站
緩衝區直接為通道Channel服務,寫入資料到通道,或從通道讀取資料
8種緩衝區類:
對應boolean之外的7基本資料型別 + MappedByteBuffer(專門用於記憶體對映的ByteBuffer)
ByteBuffer <--繼承 -- MappedByteBuffer 用於表示記憶體對映檔案
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer讀寫資料的4個步驟:
寫入資料到Buffer
呼叫flip()方法
從Buffer中讀取資料
呼叫clear()或 compact()方法
說明:
1.當向Buffer寫入資料時,buffer會記錄下寫了多少資料,
一旦要讀取資料,呼叫flip()方法,將Buffer從寫模式切換到讀模式,在讀模式下,可以讀取之前寫入到Buffer的所有資料
2.一旦讀完所有資料,就要清空緩衝區,讓它可再次被寫入,2種清空緩衝區方式:
clear() --- 清空整個緩衝區
compact() --- 只清除已讀過的資料,任何未讀的資料都將被移到緩衝區的起始處,新寫入的資料,將放到緩衝區未讀資料的後面
緩衝區的 4 個屬性: capacity>=limit>=position>=mark>=0
capacity
可容納的最大數量,緩衝區建立時被設定,不能改變
limit
上界,緩衝區中當前資料量
讀模式下:
表示最多能讀到多少資料,當切換Buffer到讀模式時,limit被設定為寫模式下的position值,
即:能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)
寫模式下:
表示最多能往Buffer裡寫多少資料,limit等於Buffer的capacity
position
位置,下一個要被讀、寫的元素的索引
寫資料到Buffer時,position表示當前的位置,初始位置為0,當資料寫入到Buffer後,position向前移動到下一個可插入資料的Buffer單元
讀資料時,同某個特定位置讀,當將Buffer從寫模式切換到讀模式,position會被重置為0,當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置
初始:為 0,最大:capacity-1
mark
標記,呼叫mark()來設定mark=position,再呼叫reset()可以讓position回覆到標記的位置,即:position=mark
初始:為 -1
建立緩衝區
緩衝區類都是抽象的,不能new方式例項化,每個緩衝區類都有一個靜態工廠方法,用於建立相應緩衝區物件
格式:
XxBuffer buf = XxBuffer.allocate(capacity)
eg:
//建立一個容量為10的byte緩衝區和char緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(10);
CharBuffer buf2 = CharBuffer.allocate(10);
如果想用一個指定大小的陣列,作為緩衝區的資料儲存器,可用wrap()方法建立緩衝區
eg:
//使用一個指定陣列,作為緩衝區儲存器
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes);
解析:
1.緩衝區資料會存在bytes陣列中,bytes陣列或buf緩衝區任何一方中資料的改動都會影響另一方
2.還可建立指定初始位置(position)和上界(limit)的緩衝區
//使用指定陣列作為緩衝區儲存器,並建立一個position=3,limit=8,capacity=10的緩衝區
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes, 3, 8);
操作緩衝區
1.存取
get() 從緩衝區取資料
put(xx) 向緩衝區存資料
channel.read(buf) 從Channel寫到Buffer
channel.write(buf) 從Buffer讀取資料到Channel
Buffer.rewind() 將position設回0, 可以重讀Buffer中的所有資料,limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
eg:
ByteBuffer buf = ByteBuffer.allocate(10);
//存3個資料
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'D');
//反轉緩衝區,即從寫模式切換到讀模式,從頭開始讀,最多讀取已寫入個數的資料
buf.flip();
//讀取2次資料
System.out.println((char)buf.get());
System.out.println((char)buf.get());
//返回當前位置到上界的資料元素數量
System.out.println(buf.remaining());
//從當前位置到上界是否還有資料元素
System.out.println(buf.hasRemaining());
解析:
1.呼叫put()或get()時,每呼叫一次,position值 +1,指示下次存或取開始的位置
2.buf.flip() 從寫模式切換到讀模式,可以從頭開始讀取最多已存入個數的資料
3.Buffer.remaining():返回從當前位置到上界的資料元素數量;
Buffer.hasRemaining():告訴我們從當前位置到上界是否有資料元素;
2.反轉 flip()
將一個處於存資料狀態的緩衝區變為一個處於準備讀取資料的狀態
反轉緩衝區,
即:將緩衝位元組陣列的指標設定為陣列的開始序列,即:陣列下標0,這樣才能從buffer開頭,對buffer進行遍歷(讀取)
即:呼叫flip()後,讀寫指標知道緩衝區頭部,並且設定了最多隻能讀取已已寫入的資料長度
如果不呼叫flip(),就會從檔案最好開始讀取
特別注意:
buffer.flip() 作用:將Buffer從寫模式切換到讀模式,呼叫flip()方法設定這個position的值為0,以及設定這個limit的值為剛才position的值,
換句話說,position現在標記了讀的位置,limit標記了有多少個Buffer的位元組,字元等等被寫入到buffer。限制有多少個位元組,字元可以去讀取的。
flip原始碼:
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;
}
3.清空資料
1.清空緩衝區內所有資料 --- clear()
2.清空已讀取的資料 --- compact()
在buffer中仍然有未讀取的資料,並且你想稍後讀取,呼叫compact(),
compact()方法拷貝所有未讀取的資料到buffer的開頭,然後設定position值為最後未讀取資料元素的後面,
再寫資料時,從已有資料後面繼續寫
4.標記 --- mark()
記住當前位置,之後可以將位置恢復到標記處(使用reset()方法恢復)
通過呼叫Buffer.mark(),可以標記Buffer中的一個特定的position,
之後可通過呼叫Buffer.reset() 恢復到這個position
eg:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
5.比較2個緩衝區是否相等
6.批量移動緩衝區的資料
public static void batchMove(){
byte[] bytes = "hello nio".getBytes();
/**這裡,可以直接把資料交給陣列來儲存
ByteBuffer buf = ByteBuffer.wrap(bytes);
*/
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
//將byte資料寫入緩衝區 <=等價=> buf.put(bytes);
buf.put(bytes, 0, bytes.length);
//反轉緩衝區,變為讀模式
buf.flip();
//輪詢判斷是否有資料,有則將緩衝區資料批量讀到array中
byte[] array = new byte[bytes.length];
while (buf.hasRemaining()){
buf.get(array, 0, buf.remaining());
}
//輸出緩衝區讀出的資料
System.out.println(new String(array));
}
7.複製緩衝區
通道之間的資料傳輸:
NIO中,如果2個通道中有一個是FileChannel, 則可以直接將資料從一個Channel傳輸到另外一個Channel
transferFrom() --- 對目標Channel呼叫
FileChannel的transferFrom()方法可將資料從源通道傳入到FileChannel中
兩種方式:
toChannel.transferFrom(fromChannel, position, count)
toChannel.transferFrom(position, count, fromChannel)
transferTo() --- 對源Channel呼叫
將資料從FileChannel傳輸到其他Channel中
eg:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Selector選擇器:
NIO能檢測到一到多個通道,並能知道通道的各類事件,一個單獨的執行緒可以管理多個Channel,從而管理多個網路連線
1.Selector建立
Selector selector = Selector.open();
2.向Selector中註冊通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ)
注意:
1.與Selector一起使用時,Channel必須處於非阻塞模式下,所以:不能將FileChannel於Selector一起使用,因為FileChannel不能切換到非阻塞模式
2.Channel通過register()方法註冊到Selector,並標明它感興趣的事件,之後通過Selector的select()判斷是否有感興趣的事件發生,如果有,通過selectedKeys()獲得興趣事件的集合
3.register(selector, Key)中,Key 表示通過Selector監聽Channel時,對什麼事件感興趣
4種不同型別事件:
Connect
Accept
Read
Write
3.SelectionKey
向Selector註冊Channel時,register()方法返回一個SelectionKey物件,這個物件包含感興趣的屬性:
interest集合
ready集合
Channel
Selector
附加的物件(可選)
4.通過Selector選擇通道
select() 方法 返回 "感興趣事件(連線,接受,讀,寫)" 已經就緒的那些通道
NIO聊天室參考:
http://blog.csdn.net/kindz_cn/article/details/51512672
http://blog.csdn.net/abc_key/article/details/29029879
32.Java中執行緒池原理和實現
JDK 執行緒池
Executors -建立-> 4種執行緒池 -實現-> ExecutorService(介面) -繼承-> Executor(介面)
4種執行緒池
newSingleThreadExecutor
建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
--- 單執行緒執行,按提交的執行緒順序依次執行
newFixedThreadPool
建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待
--- 推薦方式,超過執行緒池最大容量,執行緒等待被呼叫,
--- 定長執行緒池的大小最好根據系統資源進行設定。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
建立一個定長執行緒池,支援定時及週期性任務執行,延遲執行
eg:
/**
* 使用執行緒池定時延遲排程
*/
private static void test2(){
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延遲3秒後執行
scheduledExecutorService.schedule(getThread(),3, TimeUnit.SECONDS);
//延遲1秒後,每3秒執行一次
scheduledExecutorService.scheduleAtFixedRate(getThread(),1,3, TimeUnit.SECONDS);
}
newCachedThreadPool
建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒(空閒執行緒預設60s會被回收),若無可回收,則新建執行緒
--- 不推薦,如果一次提交過多執行緒,而且每個執行緒比較耗時耗記憶體,可能瞬間擠爆JVM記憶體!!!
1.建立
ExecutorService executorService = Executors.newXxThreadExecutor()
2.呼叫
1.不要返回值
void executorService(Runnable)
2.需要返回值
Future<T> submit(Callable<T>)
3.如果需要 定時或 延遲 執行執行緒, 使用 ScheduledExecutorService的schedule()和scheduleAtFixedRate() 呼叫執行緒
3.ThreadPoolExecutor是Executors類的底層實現,
ThreadPoolExecutor的構造器:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所儲存的執行緒數,包括空閒執行緒。
maximumPoolSize-池中允許的最大執行緒數。
keepAliveTime - 當執行緒數大於核心時,此為終止前多餘的空閒執行緒等待新任務的最長時間。
unit - keepAliveTime 引數的時間單位。
workQueue - 執行前用於保持任務的佇列。此佇列僅保持由 execute方法提交的 Runnable任務。
threadFactory - 執行程式建立新執行緒時使用的工廠。
handler - 由於超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理程式。
4.關於執行緒等待佇列
1.newFixedThreadPool和newSingleThreadExecutor 執行緒等待佇列是 LinkedBlockingQueue --- 無界阻塞佇列
2.newCachedThreadPool 執行緒等待佇列是 SynchronousQueue --- 同步阻塞佇列 --- 每個插入操作必須等待另一個執行緒的移除操作,同樣任何一個移除操作都等待另一個執行緒的插入操作。因此此佇列內部其 實沒有任何一個元素,或者說容量是0
3.ScheduledThreadPoolExecutor 執行緒等待佇列是 DelayedWorkQueue --- 延遲阻塞佇列
如何選擇執行緒阻塞佇列?
1.直接提交的無界佇列 SynchronousQueue
2.無界佇列 LinkedBlockingQueue
3.有界佇列 ArrayBlockingQueue
關於SynchronousQueue?
1.該Queue,在某次新增元素後,必須等待其他執行緒取走後才能繼續新增 (類似,進棧的元素,必須出棧,才能有新的元素被放入)
2.可避免在處理具有內部依賴性的請求集時出現鎖
eg:
如果你的任務A1,A2有內部關聯,A1需要先執行,那麼先提交A1,再提交A2,當使用SynchronousQueue我們可以保證,
A1必定先被執行,在A1麼有被執行前,A2不可能新增入queue中
5.執行緒池的4個組成:
1.執行緒池管理器(ThreadPool) --- 建立並管理執行緒池,包括:建立執行緒池,銷燬執行緒池,新增新任務
2.工作執行緒(PoolWorker) --- 執行緒池中執行緒,沒有任務時處於等待狀態,可以迴圈執行任務
3.任務介面(Task) --- 每個任務必須實現的介面,以便工作執行緒排程 --- Runnable
4.任務佇列(taskQueue) --- 存放沒有處理的任務,提供一種緩衝機制
6.JDK執行緒池原始碼分析:http://www.cnblogs.com/exe19/p/5359885.html
33.JDK動態代理和cglib位元組碼技術代理的區別?
1.JDK動態代理:
1.靜態代理 --- 代理物件和目標物件實現了相同的介面,目標物件作為代理物件的一個屬性,
具體介面實現中,可以呼叫目標物件相應方法前後加上其他業務處理邏輯
2.JDK動態代理只能針對實現了介面的類生成代理
2.CGLIB代理 --- 通過位元組碼技術,為目標物件生成一個與其功能一樣的子類
1.針對類實現代理
2.主要是對指定的類生產一個子類,覆蓋其中的所有方法
3.被代理類或方法不能宣告為final
3.區別:
1.JDK動態代理只能對實現了介面的類生成代理, 動態代理只能對於介面進行代理
2.cglib針對類實現代理,主要是對指定的類生成一個子類,覆蓋中的方法,因為是繼承,所以該類或方法最好不要宣告成final ,final可以阻止繼承和多型
3.Spring實現中,如果有介面,預設使用JDK動態代理,如果目標物件沒有實現介面,使用cglib代理,
如果目標物件實現了介面,可以強制使用CGLIB實現代理(新增CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
4.動態代理的應用
AOP(Aspect-OrientedProgramming,面向切面程式設計),AOP包括切面(aspect)、通知(advice)、連線點(joinpoint),實現方式就是通過對目標物件的代理在連線點前後加入通知,完成統一的切面操作。
實現AOP的技術,主要分為兩大類:
一是採用動態代理技術,利用擷取訊息的方式,對該訊息進行裝飾,以取代原有物件行為的執行;
二是採用靜態織入的方式,引入特定的語法建立“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的程式碼。
Spring提供了兩種方式來生成代理物件: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport物件的配置來決定。
預設的策略是如果目標類是介面,則使用JDK動態代理技術,如果目標物件沒有實現介面,則預設會採用CGLIB代理。
如果目標物件實現了介面,可以強制使用CGLIB實現代理(新增CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
5.參考:http://www.cnblogs.com/linghu-java/p/5714769.html
34.Spring的事務傳播級別
Spring中定義了7種傳播行為:
參考:https://yq.aliyun.com/articles/71303?spm=5176.8067842.tagmain.14.VE7RJr
35.關於Spring 宣告式事務的原理
參考:http://yemengying.com/2016/11/14/something-about-spring-transaction/
Spring的宣告式事務:
1.JavaConfig方法 --- 在需要管理事務的類或方法上新增 @Transactional註解,然後在配置類上新增 @EnableTransactionManagement註解
2.Xml方式 --- 新增 <tx:annotation-driven />
Spring會利用Aop在相關方法呼叫的前後進行事務管理
問題:
public class JayServiceImpl implements JayService {
public void A(List<Giraffe> giraffes) {
for (Giraffe giraffe : giraffes) {
B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
說明:
Service中A方法呼叫B方法,方法A沒有事務管理,方法B採用宣告式事務,通過在方法上宣告 @Transactional註解來做事務管理
問題:
Junit 測試方法 A 的時候發現方法 B 的事務並沒有開啟, 而直接呼叫方法 B 事務是正常開啟的???
// 沒有開啟事務
@Test
public void testA() {
giraffeService.A();
}
// 正常開啟事務
@Test
public void testB() {
giraffeService.B();
}
}
原理分析:
Spring在載入目標Bean時,會為宣告瞭@Transactional的Bean建立一個代理類,而目標類本身並不能感知到代理類的存在,
呼叫通過Spring上下文注入的Bean的方法,而不是直接呼叫目標類的方法
即:
先呼叫代理類的方法,代理類再呼叫目標類的方法
Calling Code
--call--> Proxy --->foo()
---> Pojo --> pojo.foo()
對於加了@Transactional註解的方法,在呼叫代理類方法時,會先通過攔截器 TransactionInterceptor開啟事務,
然後再呼叫目標類的方法,最後在呼叫結束後, TransactionInterceptor會提交或回滾事務
問題解析:
對於第一段的程式碼,我在方法 A 中呼叫方法 B,實際上是通過“this”的引用,也就是直接呼叫了目標類的方法,而非通過 Spring 上下文獲得的代理類,所以。。。事務是不會開啟滴
解決方法:
通過實現ApplicationContextAware介面獲得 Spring 的上下文,(或自動注入Context物件),然後獲得目標類的代理類,通過代理類的物件,呼叫方法 B,即可
public class GiraffeServiceImpl implements GiraffeService,ApplicationContextAware{
@Setter
private ApplicationContext applicationContext;
public void A(List<Giraffe> giraffes) {
GiraffeService service = applicationContext.getBean(GiraffeService.class);
for (Giraffe giraffe : giraffes) {
service.B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
36.Java類載入機制
裝載 ---> 連結(驗證 --> 準備 --> 解析) ---> 初始化
1.JVM類載入機制:
裝載:
1.找到該型別的class檔案,產生一個該型別的class檔案二進位制資料流(ClassLoader需要實現的loadClassData()方法)
2.解析該二進位制資料流為方法區內的資料結構
3.建立一個該型別的java.lang.Class例項
最終:通過defineClass()建立一個Java型別物件(Class物件)
找到二進位制位元組碼,並載入到JVM中
JVM通過類全限定名(包名.類名) + 類載入器 完成類的載入,生成類對應的Class物件
連結:
驗證:
負責對二進位制位元組碼進行校驗、類資訊是否符合JVM規範,有沒有安全問題、對class檔案長度和型別進行檢查
參考:http://www.importnew.com/17105.html
準備:
初始化類中靜態變數、並將其初始化為預設值 --- 只初始化靜態變數預設值 !!!,給其類變數賦值發生在初始化階段!!!
對於final型別的變數,準備階段直接賦初始值
該記憶體分配發生在方法區
解析:
解析類中呼叫的介面、類、欄位、方法的符號引用,把虛擬機器常量池中的符號引用轉換為直接引用
初始化:
1.對static類變數指定初始值!!!(2種方式:一種是通過類變數的初始化語句,一種是靜態初始化語句)
2.一個類的初始化需要先初始化其父類,並遞迴初始化其祖先類
2.JVM必須在每個類或介面主動使用時進行初始化:
主動使用的情況:
1.建立類的例項(無論是new、還是反射、克隆、序列化建立的)
2.使用某個類的靜態方法
3.訪問某個類或即可的靜態欄位
4.呼叫Java API中的某些反射方法
5.初始化某個類的子類(先初始化其父類)
6.啟動某個標明為啟動類的類(含main()方法)
主動使用會導致類的初始化,其超類均將在該類的初始化之前被初始化,但通過子類訪問父類的靜態欄位或方法時,對於子類(或子介面、介面的實現類)來說,這種訪問就是被動訪問,或者說訪問了該類(介面)中的不在該類(介面)中宣告的靜態成員
3.建立物件時,類中各成員的執行順序:
父靜態塊 <-- 子靜態塊 <-- 父普通程式碼塊 <-- 父構造器 <-- 子普通程式碼塊 <-- 子構造器
1.父類靜態成員和靜態初始化快,按在程式碼中出現的順序依次執行。
2.子類靜態成員和靜態初始化塊,按在程式碼中出現的順序依次執行。
3. 父類的例項成員和例項初始化塊,按在程式碼中出現的順序依次執行。
4.執行父類的構造方法。
5.子類例項成員和例項初始化塊,按在程式碼中出現的順序依次執行。
6.執行子類的構造方法。
eg:
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}
class Parent{
{
System.out.println("parent中的初始化塊");
}
static{
System.out.println("parent中static初始化塊");
}
public Parent(){
System.out.println("parent構造方法");
}
}
class Son extends Parent{
{
System.out.println("son中的初始化塊");
}
static{
System.out.println("son中的static初始化塊");
}
public Son(){
System.out.println("son構造方法");
}
}
結果:
parent中static初始化塊
son中的static初始化塊
parent中的初始化塊
parent構造方法
son中的初始化塊
son構造方法
37.MySQL效能優化
參考:
MySQL效能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.儲存引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支援事務處理,為每個表建立3個檔案,分別儲存不同內容
支援表級鎖,表的寫操作會阻塞其他使用者對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他使用者對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他使用者對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是序列的
eg:
tb_Demo表,那麼就會生成以下三個檔案:
1.tb_demo.frm,儲存表定義;
2.tb_demo.MYD,儲存資料;
3.tb_demo.MYI,儲存索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量資料時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入資料,適合管理:郵件或Web伺服器日誌資料
總結:
1.適合做count的計算 (注意:不含where條件的統計,因為MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 預設引擎
支援事務處理
引入了行級鎖(併發高)和外來鍵約束
不支援全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外來鍵約束 --- MySQL支援外來鍵的儲存引擎只有InnoDB
5.支援自動增加列 Auto_INCREMNET屬性
6.InnoDB是為處理巨大資料量時的最大效能設計
總結:
可靠性要求高,需要事務支援,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多效能開銷,而它的強項在於多執行緒的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定資料引擎的建立
細節和具體實現的差別:
1.InnoDB不支援FULLTEXT型別的索引。
2.InnoDB 中不儲存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT型別的欄位,InnoDB中必須包含只有該欄位的索引,但是在MyISAM表中,可以和其他欄位一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,匯入資料後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外來鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務型別來選擇合適的表型別,才能最大的發揮MySQL的效能優勢。
儲存引擎選擇依據?
是否需要支援事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外來鍵支援;
是否需要全文索引
經常使用什麼樣的查詢模式
資料量大小
eg:
需要事務和外來鍵約束 -- InnoDB
需要全文索引 -- MyISAM
資料量大,傾向於InnoDB,因為它支援事務處理和故障恢復,InnoDB可以利用事務日誌進行資料恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
運算元據表的習慣,也會影響效能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因為InnoDB不儲存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致效能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB效能的方法:
InnoDB支援事務,儲存過程,檢視和行級鎖,在高併發下,表現比MyISAM強很多
影響效能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設定為1的話,那麼每次插入資料的時候都會自動提交,導致效能急劇下降,應該是跟重新整理日誌有關係,設定為0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設定達到好的效能
設定innodb_buffer_pool_size能夠提升InnoDB的效能
設定查詢快取
2.配置檔案my.ini引數優化
1.max_connections --- 最大併發連線數,允許的同時客戶連線數, 預設100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢快取,用於快取select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設定query_cache_size大於0,可以極大改善查詢效率。而如果表資料頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 快取的最大執行緒數
5.sort_buffer --- 每個需要進行排序的執行緒分配該大小的一個緩衝區
6.wait_timeout --- 預設是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連線在8小時內沒有活動,就會自動斷開該連線。 不要設定太長,建議 7200
7.default-storage-engine=INNODB # 建立新表時將使用的預設儲存引擎
配置示例,2G記憶體,針對站多,抗壓型的設定,最佳:
table_cache=1024 實體記憶體越大,設定就越大.預設為2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 預設為2M
innodb_flush_log_at_trx_commit=1
(設定為0就是等到innodb_log_buffer_size列隊滿後再統一儲存,預設為1)
innodb_log_buffer_size=2M 預設為1M
innodb_thread_concurrency=8 你的伺服器CPU有幾個就設定為幾,建議用預設一般為8
key_buffer_size=256M 預設為218 調到128最佳
tmp_table_size=64M 預設為16M 調到64-256最掛
read_buffer_size=4M 預設為64K
read_rnd_buffer_size=16M 預設為256K
sort_buffer_size=32M 預設為256K
max_connections=1024 預設為1210
thread_cache_size=120 預設為60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 檢視執行效率,定位優化物件的效能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連線代替子查詢
7.當只要一行資料時,使用limit 1
8.為搜尋欄位建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啟查詢快取,併為查詢快取優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢快取,
2.像NOW()和RAND()或是其它的諸如此類的SQL函式都不會開啟查詢快取,因為這些函式的返回是會不定的易變的。所以,你所需要的就是用一個變數來代替MySQL的函式,從而開啟快取
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變為一個不衣變的值
38.JVM效能優化
1.
2.Java程式碼效能優化
1.沒必要儘量不要使用靜態變數
2.充分利用單例機制減少對資源的載入,縮短執行的時間,提高系統效率
單例適用場景:
1. 控制資源的使用,通過執行緒同步來控制資源的併發訪問;
2. 控制例項的產生,以達到節約資源的目的
3.減少物件建立,最大限度的重用物件
儘量避免在經常呼叫的方法中迴圈使用new物件 --- 享元模式(可減少物件多次建立)
4.使用final修飾符
5.儘量使用區域性變數
呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在分配給改方法的棧(Stack)中,速度較快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢
6.學會用StringBuilder和StringBuffer,並儘量確定其容量
單執行緒使用StringBuilder,多執行緒情況下使用StringBuffer,這樣效能會有很大提升
7.儘量使用基本資料型別代替物件 eg:字串建立
8.使用HashMa、ArrayList,HashTable、Vector等使用在多執行緒的場合,內部使用了同步機制,這個會降低程式的效能
9.深入理解HashMap原理
當你要建立一個比較大的hashMap時,充分利用另一個建構函式
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次進行了hash重構,擴容是一件很耗費效能的事,在預設initialCapacity只有16,
而 loadFactor是 0.75,需要多大的容量,你最好能準確的估計你所需要的最佳大小,同樣的Hashtable,Vectors也是一樣的道理
10.儘量在finally塊中釋放資源
11.儘早釋放無用物件的引用
12.儘量避免使用split,split由於支援正規表示式,所以效率比較低,考慮使用apache的 StringUtils.split(string,char),頻繁split的可以快取結果
13.儘量使用System.arraycopy ()代替通過來迴圈複製陣列
System.arraycopy()要比通過迴圈來複制陣列快的多
14..儘量快取經常使用的物件 推薦:redis快取
15.儘量避免非常大的記憶體分配
參考:http://developer.51cto.com/art/201511/496263.htm
61.MySQL效能優化
參考:
MySQL效能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.儲存引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支援事務處理,為每個表建立3個檔案,分別儲存不同內容
支援表級鎖,表的寫操作會阻塞其他使用者對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他使用者對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他使用者對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是序列的
eg:
tb_Demo表,那麼就會生成以下三個檔案:
1.tb_demo.frm,儲存表定義;
2.tb_demo.MYD,儲存資料;
3.tb_demo.MYI,儲存索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量資料時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入資料,適合管理:郵件或Web伺服器日誌資料
總結:
1.適合做count的計算 (注意:不含where條件的統計,因為MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 預設引擎
支援事務處理
引入了行級鎖(併發高)和外來鍵約束
不支援全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外來鍵約束 --- MySQL支援外來鍵的儲存引擎只有InnoDB
5.支援自動增加列 Auto_INCREMNET屬性
6.InnoDB是為處理巨大資料量時的最大效能設計
總結:
可靠性要求高,需要事務支援,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多效能開銷,而它的強項在於多執行緒的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定資料引擎的建立
細節和具體實現的差別:
1.InnoDB不支援FULLTEXT型別的索引。
2.InnoDB 中不儲存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT型別的欄位,InnoDB中必須包含只有該欄位的索引,但是在MyISAM表中,可以和其他欄位一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,匯入資料後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外來鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務型別來選擇合適的表型別,才能最大的發揮MySQL的效能優勢。
儲存引擎選擇依據?
是否需要支援事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外來鍵支援;
是否需要全文索引
經常使用什麼樣的查詢模式
資料量大小
eg:
需要事務和外來鍵約束 -- InnoDB
需要全文索引 -- MyISAM
資料量大,傾向於InnoDB,因為它支援事務處理和故障恢復,InnoDB可以利用事務日誌進行資料恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
運算元據表的習慣,也會影響效能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因為InnoDB不儲存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致效能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB效能的方法:
InnoDB支援事務,儲存過程,檢視和行級鎖,在高併發下,表現比MyISAM強很多
影響效能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設定為1的話,那麼每次插入資料的時候都會自動提交,導致效能急劇下降,應該是跟重新整理日誌有關係,設定為0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設定達到好的效能
設定innodb_buffer_pool_size能夠提升InnoDB的效能
設定查詢快取
2.配置檔案my.ini引數優化
1.max_connections --- 最大併發連線數,允許的同時客戶連線數, 預設100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢快取,用於快取select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設定query_cache_size大於0,可以極大改善查詢效率。而如果表資料頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 快取的最大執行緒數
5.sort_buffer --- 每個需要進行排序的執行緒分配該大小的一個緩衝區
6.wait_timeout --- 預設是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連線在8小時內沒有活動,就會自動斷開該連線。 不要設定太長,建議 7200
7.default-storage-engine=INNODB # 建立新表時將使用的預設儲存引擎
配置示例,2G記憶體,針對站多,抗壓型的設定,最佳:
table_cache=1024 實體記憶體越大,設定就越大.預設為2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 預設為2M
innodb_flush_log_at_trx_commit=1
(設定為0就是等到innodb_log_buffer_size列隊滿後再統一儲存,預設為1)
innodb_log_buffer_size=2M 預設為1M
innodb_thread_concurrency=8 你的伺服器CPU有幾個就設定為幾,建議用預設一般為8
key_buffer_size=256M 預設為218 調到128最佳
tmp_table_size=64M 預設為16M 調到64-256最掛
read_buffer_size=4M 預設為64K
read_rnd_buffer_size=16M 預設為256K
sort_buffer_size=32M 預設為256K
max_connections=1024 預設為1210
thread_cache_size=120 預設為60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 檢視執行效率,定位優化物件的效能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連線代替子查詢
7.當只要一行資料時,使用limit 1
8.為搜尋欄位建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啟查詢快取,併為查詢快取優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢快取,
2.像NOW()和RAND()或是其它的諸如此類的SQL函式都不會開啟查詢快取,因為這些函式的返回是會不定的易變的。所以,你所需要的就是用一個變數來代替MySQL的函式,從而開啟快取
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變為一個不衣變的值
62.Java常見的鎖型別有哪些?請簡述其特點。
1、synchronized物件同步鎖:synchronized是對物件加鎖,可作用於物件、方法(相當於對this物件加鎖)、靜態方法(相當於對Class例項物件加鎖,鎖住的該類的所有物件)以保證併發環境的執行緒安全。同一時刻只有一個執行緒可以獲得鎖。
其底層實現是通過使用物件監視器Monitor,每個物件都有一個監視器,當執行緒試圖獲取Synchronized鎖定的物件時,就會去請求物件監視器(Monitor.Enter()方法),如果監視器空閒,則請求成功,會獲取執行鎖定程式碼的權利;如果監視器已被其他執行緒持有,執行緒進入同步佇列等待。
2、Lock同步鎖:與synchronized功能類似,可從Lock與synchronized區別進行分析:
1、Lock可以通過tryLock()方法非阻塞地獲取鎖而。如果獲取了鎖即立刻返回true,否則立刻返回false。這個方法還有加上定時等待的過載方法tryLock(long time, TimeUnit unit)方法,在定時期間內,如果獲取了鎖立刻返回true,否則在定時結束後返回false。在定時等待期間可以被中斷,丟擲InterruptException異常。而Synchronized在獲得鎖的過程中是不可被中斷的。
2、Lock可以通過lockInterrupt()方法可中斷的獲取鎖,與lock()方法不同的是等待時可以響應中斷,丟擲InterruptException異常。
3、Synchronized是隱式的加鎖解鎖,而Lock必須顯示的加鎖解鎖,而且解鎖應放到finnally中,保證一定會被解鎖,而Synchronized在出現異常時也會自動解鎖。但也因為這樣,Lock更加靈活。
4、Synchronized是JVM層面上的設計,對物件加鎖,基於物件監視器。Lock是程式碼實現的。
3、可重入鎖:ReentrantLock與Synchronized都是可重入鎖。可重入意味著,獲得鎖的執行緒可遞迴的再次獲取鎖。當所有鎖釋放後,其他執行緒才可以獲取鎖。
4、公平鎖與非公平鎖:“公平性”是指是否等待最久的執行緒就會獲得資源。如果獲得鎖的順序是順序的,那麼就是公平的。不公平鎖一般效率高於公平鎖。ReentrantLock可以通過建構函式引數控制鎖是否公平。
5、ReentrantReadWriteLock讀寫鎖:是一種非排它鎖, 一般的鎖都是排他鎖,就是同一時刻只有一個執行緒可以訪問,比如Synchronized和Lock。讀寫鎖就多個執行緒可以同時獲取讀鎖讀資源,當有寫操作的時候,獲取寫鎖,寫操作之後的讀寫操作都將被阻塞,直到寫鎖釋放。讀寫鎖適合寫操作較多的場景,效率較高。
6、樂觀鎖與悲觀鎖:在Java中的實際應用類並不多,大多用在資料庫鎖上,可參看:http://blog.csdn.net/sdyy321/article/details/6183412
7、死鎖:是當兩個執行緒互相等待獲取對方的物件監視器時就會發生死鎖。一旦出現死鎖,整個程式既不會出現異常也不會有提示,但所有執行緒都處於阻塞狀態。死鎖一般出現於多個同步監視器的情況。
63.volatile與automicInteger是什麼?如何使用?
在併發環境中有三個因素需要慎重考量,原子性、可見性、有序性。
voatile 保證了有序性(防止指令衝排序)和變數的記憶體可見性(每次都強制取主存資料),每次取到volatile變數一定是最新的
volatile主要用於解決可見性,它修飾變數,相當於對當前語句前後加上了“記憶體柵欄”。使當前程式碼之前的程式碼不會被重排到當前程式碼之後,當前程式碼之後的指令不會被重排到當前程式碼之前,一定程度保證了有序性。而volatile最主要的作用是使修改volatile修飾的變數值時會使所有執行緒中的快取失效,並強制寫入公共主存,保證了各個執行緒的一致。可以看做是輕量級的Synchronized。詳情可參看:http://www.cnblogs.com/dolphin0520/p/3920373.html。
automicXXX主要用於解決原子性,有一個很經典的問題:i++是原子性的操作碼?答案是不是,它其實是兩步操作,一步是取i的值,一步是++。在取值之後如果有另外的執行緒去修改這個值,那麼當前執行緒的i值就是舊資料,會影響最後的運算結果。使用automicXXX就可以非阻塞、保證原子性的對資料進行增減操作。詳情可參看:http://ifeve.com/java-atomic/
注:在此列舉的只是Java多執行緒最基礎的知識,也是面試官最常問到的,先打牢基礎,再去探討底層原理或者高階用法,除了這十個問題,在此再推薦一些其他的資料:
JVM底層又是如何實現synchronized的:http://www.open-open.com/lib/view/open1352431526366.html
Java執行緒池詳解:http://blog.csdn.net/zhangliangzi/article/details/52389766
Java執行緒池深度解析:http://www.cnblogs.com/dolphin0520/p/3932921.html
ConcurrentHashMap原理分析:http://www.cnblogs.com/ITtangtang/p/3948786.html
Java阻塞佇列詳解:http://ifeve.com/java-blocking-queue/
64.MyBatis中timestamp時間型別,在Spring MVC出參時無法轉為正確的時間型別?
在xml中配置: javaType為 java.sql.Timestamp
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" javaType="java.sql.Timestamp"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" javaType="java.sql.Timestamp"/>
65.Datatable自定義搜尋
//自定義搜尋,每次只能根據一個維度進行搜尋(按渠道或產品型別)
$("#channel_select,#brand_select").change(function(){
var tsval = $(this).val()
table.search(tsval, false, false).draw();
});
66.synchronized和Lock的底層實現原理?
參考:http://www.open-open.com/lib/view/open1352431526366.html
鎖原理:http://blog.csdn.net/Luxia_24/article/details/52403033
synchronized 在軟體層面依賴JVM
Lock 在硬體層面依賴特殊的CPU指令 --- CAS + JNI呼叫CPU指令來實現
synchronized 可以吧任何一個非 null 物件作為 "鎖",
作用於方法上時,鎖住的是物件例項this,
作用於靜態方法,鎖住的是物件對應的Class例項,因為Class資料儲存在永久帶,因此靜態方法鎖相當於該類的全域性鎖,
作用於某個物件例項,鎖住的是對應的程式碼塊
HotSpot JVM中,鎖 --- 物件監視器(物件來監視執行緒的互斥) --- synchronized的實現原理
物件監視器,設定幾種狀態來區分請求的執行緒:
Contention Set: 所有請求鎖的執行緒,被首先放置到該競爭佇列 --- 先進後出的虛擬佇列,會被執行緒併發訪問
Entry Set: 等待獲取鎖的執行緒(來自Contention Set)排隊佇列 --- 等待獲取物件鎖執行
Wait Set: 獲取鎖後,呼叫wait()方法,被阻塞的執行緒佇列 --- 等待再次獲取物件鎖
OnDeck: 任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck
Owner: 獲得鎖的執行緒稱為Owner
!Owner: 釋放鎖的執行緒
說明:
1.Entry Set 和Contention Set 同屬等待佇列,
2.Contention Set會被執行緒併發訪問,為了降低對Contention Set隊尾的爭用(為了減少加入與取出兩個執行緒對於contentionList的競爭),而建立Entry Set,如果Entry Set為空,則從Contention Set隊尾取出節點
3.Owner執行緒在unlock時,會從Contention Set中遷移執行緒到Entry Set,並會指定Entry Set中的某個執行緒(一般為Head)為Read(OnDeck)執行緒
4.Owner執行緒並不是把鎖傳遞給OnDeck執行緒,只是把競爭鎖的權利交給OnDeck,OnDeck執行緒需要重新競爭鎖
5.OnDeck執行緒獲得鎖喉變為Owner執行緒,無法獲得鎖的執行緒依然留在Entry Set中
6.如果Owner執行緒被wait()方法阻塞,則轉移後WaitSet中,如果某個時刻被notify/notifyAll喚醒,則再次轉移到EntrySet
執行緒的互斥,其實是執行緒對同一個物件的監視器monitor的操作:
每個物件都有一個監視器(monitor)!,當monitor被佔就會處於鎖定狀態,執行緒執行monitorentry指令時嘗試獲取monitor的所有權
1.如果monitor的進入數為0,則執行緒進入monitor,然後將進入數設定為1,執行緒即為monitor的所有者
2.如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數 +1 --- 鎖可重入 --- ReentrantLock 和synchronized 都是 可重入鎖
3.其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,知道monitor的進入數為0,再嘗試獲取monitor的所有權
4.執行緒呼叫一次unlock()釋放鎖,monitor的進入數就 -1 (只有monitor進入數為0,才能被其他執行緒搶佔)
5.一個執行緒獲取多少次鎖,就必須釋放多少次鎖,對於synchronized內建鎖 ,每一次進入和離開synchronized方法(程式碼塊),就是一個完整的鎖獲取和釋放
sleep()不會釋放鎖,等待指定時間後繼續執行
wait()會釋放鎖,進入物件監視器的 Wait Set佇列,等待被喚醒,被喚醒後,需要重新獲取鎖
wait()和notify/notifyAll必須成對出現,而且必須放在synchronized中,
yield() 不會釋放鎖, 只是讓當前執行緒讓出CPU佔用權
Synchronized底層優化 --- 偏向鎖、輕量級鎖
參考:http://www.cnblogs.com/paddix/p/5405678.html
http://www.jianshu.com/p/5dbb07c8d5d5
Synchronized效率低的原因?
Synchronized是通過物件內部的物件監視器鎖(monitor)來實現的,monitor本質是依賴於底層的作業系統的Mutex Lock(互斥鎖)來實現,
作業系統實現執行緒間切換需要從使用者態轉到核心態(JVM轉到作業系統核心),這個成本非常高,狀態轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因
Synchronized底層優化:
1.鎖的4種狀態:
無鎖狀態、偏向鎖、輕量級鎖、重量級鎖
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖(鎖升級只能從低到高升級,不會出現鎖的降級)
JDK1.6預設開啟偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖
鎖的狀態儲存在物件的標頭檔案中
重量級鎖 --- 依賴於作業系統Mutex Lock所實現的鎖, 需要從JVM轉到作業系統核心,進行互斥操作
輕量級鎖 --- 並不是用來代替重量級鎖,本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗
輕量級鎖目的:
為了線上程交替執行同步塊時提高效能 !!!
輕量級鎖適用場景:
執行緒交替執行同步塊的情況 ---- 鎖競爭不激烈的情況!
如果存在同一時間訪問同一個鎖,就會導致輕量級鎖升級為重量級鎖
偏向鎖 --- 為了在無多執行緒競爭的情況下,儘量減少不必要的輕量級鎖執行路徑
一旦執行緒第一次獲得了監視物件,之後讓監視物件 "偏向"這個執行緒,在該執行緒重複獲取鎖時,避免CAS操作
即:
設定一個變數,如果發現是true,無需再走加鎖、解鎖的流程!
偏向鎖目的:
解決無競爭下的鎖效能問題,在只有一個執行緒執行同步塊時,進一步提高效能
總結:
ynchronized的底層實現主要依靠Lock-Free的佇列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量 !!!
67.Spring如何解決Bean的迴圈依賴? --- 只支援Singleton作用域的, setter方式的迴圈依賴!!!
參考:https://my.oschina.net/yibuliushen/blog/737640
http://blog.csdn.net/caomiao2006/article/details/46511123
Spring容器迴圈依賴包括構造器迴圈依賴和setter迴圈依賴
如果是構造器迴圈依賴,Spring容器將無法啟動,報迴圈依賴異常BeanCurrentlInCreationException
解決方式:
將構造器注入方式改為屬性注入方式 --- setter
Spring 支援setter方法注入屬性方式的迴圈依賴
Spring中將迴圈依賴的處理分3中情況:
1.構造器迴圈依賴 --- 原理, Spring 不支援構造器方式的迴圈依賴
通過構造器注入構成的迴圈依賴是無法解決的,只能在容器啟動時丟擲BeanCurrentlInCreationException異常 --- 表示迴圈依賴
Spring容器將每一個正在建立的Bean的識別符號(id)放到 "當前建立bean池" 中,bean識別符號在建立過程中將一直保持在這個池中,
如果在建立bean過程中,發現自己已經在 "當前建立bean池"裡時,將丟擲 BeanCurrentlInCreationException異常,表示迴圈依賴,
而對於建立完畢的bean將從 "當前建立bean池"中清除掉
eg:
如在建立TestA類時,構造器需要TestB類,那將去建立TestB,在建立TestB類時又發現需要TestC類,則又去建立TestC,
最終在建立TestC時發現又需要TestA,從而形成一個環,沒辦法建立 --- 迴圈依賴,丟擲 BeanCurrentlInCreationException異常
配置檔案;
<bean id="testA" class="com.bean.TestA">
<constructor-arg index="0" ref="testB"/>
</bean>
<bean id="testB" class="com.bean.TestB">
<constructor-arg index="0" ref="testC"/>
</bean>
<bean id="testC" class="com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>
測試用例:
@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test.xml");
} catch (Exception e) {
//因為要在建立testC時丟擲;
Throwable ee1 = e.getCause().getCause().getCause();
throw e1;
}
}
分析:
Spring容器建立"testA"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testB",並將"testA"識別符號放到"當前建立bean池"。
Spring容器建立"testB"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testC",並將"testB"識別符號放到"當前建立bean池"。
Spring容器建立"testC"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testA",並將"testC"識別符號放到"當前建立Bean池"。
到此為止Spring容器要去建立"testA"bean,發現該bean識別符號在"當前建立bean池"中,因為表示迴圈依賴,丟擲BeanCurrentlyInCreationException。
說明:
Spring中bean預設是單例的,對於singleton作用於的Bean,可通過setAllowCircularReferences(false)來禁用迴圈引用
2.Setter迴圈依賴 --- 原理, Spring支援setter方式注入屬性的迴圈依賴!
setter注入方式構成的迴圈依賴,通過Spring容器提前暴露剛完成構造器注入但未完成其他步驟(eg:setter注入)的bean來完成的,
而且只能解決Singleton單例作用域的bean迴圈依賴,通過提前暴露一個單例工廠方法,從而使其他bean能引用到該bean(注意:此時僅僅只是生了一個bean,該bean還未呼叫其他方法,如setter注入)
對單例Bean迴圈依賴的處理:通過遞迴方法,找出當前Bean的所有依賴Bean,然後提前快取起來
原理:
建立Bean A時,先通過無參構造器建立一個A例項,此時屬性都是空的,但物件引用已經建立建立出來,然後把Bean A的引用提前暴露出來,
然後setter B屬性時,建立B物件,此時同樣通過無參構造器,構造一個B物件的引用,並將B物件引用暴露出來。
接著B執行setter方法,去池中找到A(因為此時,A已經暴露出來,有指向該物件的引用了),這樣依賴B就構造完成,也初始化完成,然後A接著初始化完成,
迴圈依賴就這麼解決了!!!
總結:
先建立物件引用,再通過setter()方式,給屬性賦值,層層建立物件 !!!
Bean A初始化時,先對其依賴B進行初始化,同時,通過預設無參構造器,生成自己的引用,而不呼叫其setter()方法,
當B物件建立時,如果還依賴C,則也通過無參構造器,生成B的引用,
C物件建立時,如果引用了A,則去物件池中查到A的引用,然後呼叫setter()方式,注入A,完成C物件的建立
C建立完成後,B使用setter()方式,注入C,完成B物件建立,
B物件場景完成後,A使用setter()方式,注入B,完成A物件建立,
最終,完成setter()方式的迴圈依賴!
如果迴圈依賴的都是單例物件(都是通過setter方式注入屬性的),那麼這個肯定沒問題,放心使用即可!!!
如果一個是單例,一個是原型,那麼一定要保證單例物件能提前暴露出來,才可以正常注入屬性!!!
3.prototype範圍的依賴處理
對於"prototype"作用域bean,Spring容器無法完成依賴注入,因為Spring容器不進行快取"prototype"作用域的bean,因此無法提前暴露一個建立中的bean
這個spring也無能為力,因為是原型物件,A建立的時候不會提前暴露出來,所以,每次都是要建立,建立的時候,發現有相同的物件正在建立,同樣報錯,迴圈依賴錯誤
4.Spring建立Bean的原始碼解釋:
1.建立Bean的入口
AbstractBeanFactory-->doGetBean()
Object sharedInstance = getSingleton(beanName); //從快取中查詢,或者如果當前建立池中有並且已經暴露出來了,就返回這個物件
2.建立單例Bean方法
DefaultSingletonBeanRegistry-->getSingleton(String beanName, ObjectFactory<?> singletonFactory)
3.建立真正物件
AbstractAutowireCapableBeanFactory-->doCreateBean
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
} 注意這一步很關鍵,是呼叫構造方法建立一個例項物件,如果這個構造方法有引數,而且就是迴圈依賴的引數,那麼這個物件就無法建立了,
因為到這裡物件沒有建立,也沒有暴露當前物件,如果是無參的構造方法,那麼就可以,先建立一個物件,儘管所有的屬性都為空
68.Spring事務管理的原理?
參考:http://www.codeceo.com/article/spring-transactions.html
宣告式事務管理,在Service之上或Service的方法之上,新增 @Transactional註解
@Transactional如何工作?
Spring在啟動時,會去解析生成相關的Bean,這是會檢視擁有相關注解的類和方法,
並且為這些類和方法生成代理,並根據 @Transactional的相關引數進行相關配置注入,
這樣就在代理中把相關的事務處理掉了(開啟正常提交事務,異常回滾事務)
真正的資料庫層,事務提交和回滾是通過binlog和redo log實現的
Spring事務管理機制實現原理:
參考:
http://www.jianshu.com/p/4312162b1458
http://www.cnblogs.com/duanxz/p/3750845.html
http://www.92to.com/bangong/2016/11-05/12533010.html
在呼叫一個需要事務的元件時,管理器首先判斷當前呼叫(即:當前執行緒)有沒有事務,如果沒有事務則啟動一個事務,並把事務與當前執行緒繫結,
Spring使用TransactionSynchronizationManager的bindResource方法將當前執行緒與一個事務繫結,採用的方式就是ThreadLocal,
參考:DataSourceTransactionManager的啟動事務用的程式碼 doBegin()
通過動態代理或AOP方式,對所有需要事務管理的Bean進行載入,生成代理物件,並根據配置在invoke()方法中對當前呼叫的方法名進行判定,
並在method.invoke()方法前後為其加上合適的事務管理程式碼,根據method.invoke()執行結果,正常提交事務,異常回滾事務
實現了EntityManager介面的持久化上下文代理,包含3個組成部分:
1.EntityManager Proxy本身
2.事務的切面
3.事務管理器
遇到過的問題:
參考:59,為何宣告式事務沒有生效?
69.Spring如何處理高併發?高併發下,如何保證效能?
1.單例模式 + ThreadLocal
單例模式大大節省了物件的建立和銷燬,有利於效能提高,ThreadLocal用來保證執行緒安全性
Spring單例模式下,用ThreadLocal來切換不同執行緒直接的引數,用ThreadLocal是為了保證執行緒安全,實際上,ThreadLocal的key就是當前執行緒的Thread例項
單例模式下,Spring把每個執行緒可能存線上程安全問題的引數值放進了ThreadLocal,雖然是一個例項,但在不同執行緒下的資料是相互隔離的,
因為執行時建立和銷燬的bean大大減少了,所以大多數場景下,這種方式對記憶體資源的消耗較少,並且併發越高,優勢越明顯
2.ThreadLocal
相比同步機制,ThreadLocal則從另一個角度來解決多執行緒的併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突。
因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,
在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal !!!
3.Spring MVC在併發訪問時,是否會存線上程安全問題?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
http://blog.csdn.net/a236209186/article/details/61460211
Struts2是基於類的攔截(每次處理請求,都會例項化一個物件Action,不會有執行緒安全問題)
Spring MVC 是基於方法的攔截,粒度更細,而Spring的Controller預設是Singleton的,即:每個request請求,系統都會用同一個Controller去處理,
Spring MVC和Servlet都是方法級別的執行緒安全,如果單例的Controller或Servlet中存在例項變數,都是執行緒不安全的,而Struts2確實是執行緒安全的
優點:
不用每次建立Controller,減少了物件建立和銷燬
缺點:
Controller是單例的,Controller裡面的變數執行緒不安全
解決方案:
1.在Controller中使用ThreadLocal變數,把不安全的變數封裝進ThreadLocal,使用ThreadLocal來儲存類變數,將類變數儲存線上程的變數域中,讓不同的請求隔離開來
2.宣告Controller為原型 scope="prototype",每個請求都建立新的Controller
3.Controller中不使用例項變數
Spring MVC 如何保證request物件執行緒安全?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
InvocationHandler介面:這是springmvc保證request物件執行緒安全的核心。
通過實現該介面,開發者能夠在Java物件方法執行時進行干預,搭配Threadlocal就能夠實現執行緒安全
問題:判斷一下程式是否執行緒安全?
@Controller
public class UserController{
@Autowired
private HttpSession session
@RequestMapping(xxxxxxx)
public void getUser{
session.get ...
session.set...
....
}
}
結論:
該程式是執行緒安全的
解析:
專案啟動和執行時,Controller物件中的HttpSession並不是HttpSession例項,而是一個代理,
是org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler代理了HttpSession ,可通過這個程式碼求證:System.out.println(Proxy.getInvocationHandler(session));
只要當你真正呼叫HttpSession中的非java.lang.Object方法時才會真真去呼叫被代理的HttpSession裡面的方法
說一下session.get ...過程:首先從物件工廠從Threadlocal中取得HttpSession例項,然後通過反射呼叫該例項的set方法
特別注意:
1.Spring 應該是在請求進來的時候ThreadLocal.set(Session),然後在請求的生命週期中都是一個Thread ,執行完後ThreadLocal.remove(Session)
2.一個請求使用一個ThreadLocal,繫結對應的HttpSession,所以是執行緒安全的
3.對於 "注入" 到Controller中的單例物件, 都是由Spring統一管理的,Spring對注入Controller的物件使用了ThreadLocal + 代理機制,保證了執行緒安全
4.但是,對於在Controller中直接定義的例項變數,是執行緒不安全的!!!
eg:
@RestController
@RequestMapping("/test1")
public class ControllerTest1 {
private int i = 0;
@GetMapping("/count")
public void test1(){
System.out.println(i++);
}
//驗證Controller中的例項變數,執行緒不安全
@GetMapping("/t1")
public void test3(){
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i=0;i<1000;i++) {
service.execute(new Runnable() {
@Override
public void run() {
HttpUtils.sendGet("http://localhost:8080/test1/count", null);
}
});
}
}
}
呼叫:http://localhost:8080/test1/t1 方法,使用多執行緒對i進行操作,發現i的結果不是999,證明Controller中的例項變數是執行緒不安全的!
結論:
1.對於單例的Controller,Service中定義的例項變數,都不是執行緒安全的!!!
2.儘量避免在Controller和Service中定義多執行緒共享的例項變數
3.Spring使用ThreadLocal + InvocationHandler(動態代理)提供了高併發訪問的效能
4.對於Controller和Service中的例項變數,多執行緒訪問時,需要加鎖處理 或 設定 scope = "prototype"為每個請求創一個物件
5.對於 @Autowire注入的HttpServletRequest和HttpSession,Spring進行了特殊處理,不會有執行緒安全問題
70.ConcurrentLinkedQueue與BlockingQueue
2類執行緒安全的佇列:
1.阻塞佇列 --- 阻塞演算法 --- 佇列使用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式實現
2.同步佇列 --- 非阻塞演算法 --- 使用迴圈CAS方式實現
LinkedBlockingQueue 執行緒安全的阻塞佇列,實現了BlockingQueue介面,BlockingQueue繼承自java.util.Queue介面,
並在介面基礎上增加了take()和put()方法, 這2個方法正式佇列操作的阻塞版本
先進先出,可以指定容量,預設最大是Integer.MAX_VALUE;其中主要用到put和take方法,put方法在佇列滿的時候會阻塞直到有佇列成員被消費,take方法在佇列空的時候會阻塞,直到有佇列成員被放進來!!!
put() 向佇列中放資料 take() 從佇列中取資料
ConcurrentLinkedQueue 是Queue的一個安全實現.Queue中元素按FIFO原則進行排序.採用CAS操作,來保證元素的一致性。
非阻塞方式實現的無界執行緒安全佇列 !!!
offer()新增元素, poll()獲取元素 isEmpty()判斷佇列是否為空 (特別注意,不用size(),效率低,會遍歷佇列,儘量要避免用size而改用isEmpty())
採用CAS操作,允許多個執行緒併發執行,並不會因為你加鎖而阻塞執行緒,使得併發效能更好!!!
使用:http://www.cnblogs.com/dmir/p/4907515.html
http://blog.csdn.net/sunxianghuang/article/details/52046150
ConcurrentLinkedQueue原始碼解析:http://ifeve.com/concurrentlinkedqueue/
總結:
多數生產消費模型的首選資料結構就是佇列(先進先出)。Java提供的執行緒安全的Queue可以分為阻塞佇列和非阻塞佇列,
其中阻塞佇列的典型例子是BlockingQueue,非阻塞佇列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞佇列或者非阻塞佇列
Java中的7種阻塞佇列
阻塞佇列: --- 阻塞的是執行緒操作(拿和取元素)
常用於生產者和消費者場景,是生產者用來存放元素、消費者用來獲取元素的容器
put()阻塞:佇列滿時,阻塞插入元素的執行緒,直到佇列不滿
take()阻塞:佇列空時,阻塞獲取元素的執行緒,直到佇列不空
ArrayBlockingQueue:一個由陣列結構組成的有界阻塞佇列。
LinkedBlockingQueue:一個由連結串列結構組成的有界阻塞佇列。
PriorityBlockingQueue:一個支援優先順序排序的無界阻塞佇列。
DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。
SynchronousQueue:一個不儲存元素的阻塞佇列。 生產者和消費者直接傳遞資料,不對資料作快取,生產者和消費者通過在佇列裡排隊的方式來阻塞和喚醒 --- 速度快
執行緒數少時,使用SynchronousQueue 速度更快!!!
LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的區別和使用場景?
區別:
1.三者都是執行緒安全的
2.2個BlockingQueue是阻塞的,ConcurrentLinkedQueue是併發的
3.2個BlockingQueue使用鎖機制實現阻塞和執行緒安全(通過ReentrantLock + Condition阻塞容量為空時的取操作和容量滿時的寫操作),
ConcurrentLinkedQueue使用cas演算法保證執行緒安全
4.ArrayBlockingQueue使用一個鎖(lock + 2個Condition),而LinkedBlockingQueue使用2個鎖(鎖分離,取用takeLock + Condition,寫用putLock+Condition),所以LinkedBlockingQueue的吞吐量大,併發效能比Array高
LinkedBlockingQueue,對頭和尾採用不同的鎖,提高了吞吐量,適合 "消費者生產者" 模式
ArrayBlockingQueue, 陣列實現,使用一把全域性鎖並行對queue的讀寫操作,同時使用2個Condition阻塞容量為空時的讀操作和容量滿時的寫操作
5.正因為LinkedBlockingQueue使用兩個獨立的鎖控制資料同步,所以可以使存取兩種操作並行執行,從而提高併發效率。
而ArrayBlockingQueue使用一把鎖,造成在存取兩種操作爭搶一把鎖,而使得效能相對低下。LinkedBlockingQueue可以不設定佇列容量,預設為Integer.MAX_VALUE.其容易造成記憶體溢位,一般要設定其值
使用場景:
阻塞佇列優點:
多執行緒操作不需要同步,
佇列會自動平衡負載,即:生產和消費兩邊,處理快了會被阻塞,減少兩邊的處理速度差距,
自動平衡負載特性,造成它能被用於多生產者佇列,佇列滿了就要阻塞等著,直到消費者使佇列不滿才能繼續生產
ConcurrentLinkedQueue:
允許多執行緒共享訪問一個集合,多用於訊息佇列!!!
多消費者消費同一個 用 ConcurrentLinkedQueue:
BlockingQueueue:
多執行緒共享時阻塞,多用於任務佇列!!!
單消費者用 BlockingQueueue:
總結: 單個消費者用LinkedBlockignQueue, 多消費者用ConcurrentLinkedQueue !!!
單生產者,單消費者 用 LinkedBlockingqueue
多生產者,單消費者 用 LinkedBlockingqueue
單生產者 ,多消費者 用 ConcurrentLinkedQueue
多生產者 ,多消費者 用 ConcurrentLinkedQueue
71.Java多執行緒同步機制:3種型別
volatile 變數:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。僅提供記憶體可見性保證,不提供原子性。 --- 只保證可見性,不保證原子性,不絕對執行緒安全!!!
CAS 原子指令:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。它同時提供記憶體可見性和原子化更新保證。
內部鎖(synchronized)和顯式鎖(各種Lock):重量級多執行緒同步機制,可能會引起上下文切換和執行緒排程,它同時提供記憶體可見性和原子性。
參考:
非阻塞演算法在併發容器中的實現:ConcurrentLinkedQueue https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html
72.CAS在JDK中的實現
參考:http://blog.csdn.net/canot/article/details/50759424
1.Synchronized鎖機制存在的問題:
(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。
(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。
(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險
優化:
偏向鎖、輕量級鎖、減小鎖粒度
2.鎖分類
悲觀鎖 --- 獨佔鎖 --- synchronized是獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖
樂觀鎖 --- 每次不加鎖,而是假設沒有衝突,而去完成某項操作,如果因為衝突失敗就重試,直到成功為止
3.CAS原理 --- 實現樂觀鎖
CAS操作:
CAS有3個運算元:
V 記憶體值
A 舊的預期值
B 要修改的新值
當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做!!!
非阻塞演算法: 一個執行緒的失敗或掛起,不應該影響其他執行緒的失敗或掛起的演算法
CAS的硬體基礎和實現原理:
現代的CPU提供了特殊指令,可以自動更新共享資料,而且能夠檢測到其他執行緒的干擾,
而compareAndSet()就用這些代替了鎖定, compareAndSet利用JNI,藉助呼叫的C語言來完成CPU指令的操作
eg:
AtomicInteger 如何實現無鎖下的執行緒安全?
//在沒有鎖的機制下可能需要藉助volatile原語,保證執行緒間的資料是可見的(共享的)。這樣才獲取變數的值的時候才能直接讀取。
private volatile int value;
public final int get(){
return value;
}
//i++操作, 每次從記憶體中讀取資料,然後將此資料和 +1 後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止
public final int incrementAndGet(){
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
39.Redis效能優化
40.MongoDB效能優化
41.MQ效能優化和對比
42.一次搞定Java多執行緒併發程式設計
參考:http://blog.csdn.net/liuguangqiang/article/details/52137188
Java併發程式設計:CountDownLatch、CyclicBarrier和Semaphore
http://blog.csdn.net/zheng0518/article/details/42297259
http://www.cnblogs.com/dongguacai/p/6023028.html
執行緒安全:
1.Synchronized 執行緒同步
2.Lock + ReentrantLock 執行緒安全
執行緒通訊與協作:
0.wait()、notify()、notifyAll() 每個物件都有的3個方法,通道Monitor、waitSet、enterSet用來監聽鎖,存放執行緒佇列
1.ReentrantLock + Condition 併發控制多路複用
每個ReentrantLock可以建立多個Condition,每個Condition都可以通過控制一個物件鎖,來實現多個執行緒通訊
Condition方法:
執行緒1中呼叫await()後,執行緒1將釋放鎖,等待被喚醒
執行緒2獲取到鎖後,執行完畢,呼叫signal()方法,喚醒執行緒1
eg:
列印1到9這9個數字,A執行緒列印1,2,3,然後B執行緒列印4,5,6,,然後再A執行緒列印 7,8,9
2.ReadWriteLock 讀寫鎖
3.CountDownLatch
4.CyclicBarrier
5.Semaphore
效率提升:
6.執行緒池 + 阻塞佇列
不能根據hashCode值判斷兩個物件是否相等,但可以直接根據hashCode值判斷兩個物件不相等。
如果兩個物件的hashCode值不等,一定是不同的物件,要判斷兩個物件是否真正相等,必須通過equals()方法
如果呼叫equals()方法得到的結果為true,則兩個物件的hashCode值一定相等
如果equals()方法得到的結果為false,則兩個物件的hashCode值不一定不同
如果兩個物件的hashcode值不等,則equals方法得到的結果必定為false;
如果兩個物件的hashcode值相等,則equals方法得到的結果未知。
在重寫equals()方法時,必須重寫hashCode方法,讓equals()方法和hashCode()方法始終在邏輯上保持一致性
hashCode方法的主要作用是為了配合基於雜湊的集合一起正常執行,這樣的雜湊集合包括HashSet、HashMap以及HashTable
Java裡靜態語句塊是優先物件存在,也就是優先於構造方法存在,我們通常用來做只建立一次物件使用,類似於單列模式而且執行的順序是:
父類靜態語句塊 -> 子類靜態語句塊 -> 父類非靜態程式碼塊、父類構造方法 -> 子類非靜態程式碼塊構造方法
靜態語句塊可以看作在類載入的時候執行,且只執行一次
Java物件初始化順序:
靜態程式碼塊內容先執行(父>子),接著執行父類非靜態程式碼塊和構造方法,然後執行子類非靜態程式碼塊和構造方法
首先執行父類靜態的內容,父類靜態的內容執行完畢後,接著去執行子類的靜態的內容,當子類的靜態內容執行完畢之後,再去看父類有沒有非靜態程式碼塊,如果有就執行父類的非靜態程式碼塊,
父類的非靜態程式碼塊執行完畢,接著執行父類的構造方法;父類的構造方法執行完畢之後,它接著去看子類有沒有非靜態程式碼塊,如果有就執行子類的非靜態程式碼塊。子類的非靜態程式碼塊執行完畢再去執行子類的構造方法
2.JVM記憶體模型
程式計數器
用來指示 執行哪條指令的
如果執行緒執行的是非native方法,則程式計數器保持的是當前需要執行的指令的地址
如果執行緒執行的是native方法,程式計數器中的值是undefined
Java棧
存放一個個的棧幀,每個棧幀對應一個被呼叫的方法。
棧幀中包括:區域性變數表、運算元棧、指向當前方法所屬的類的執行時常量池的引用、方法返回地址和一些額外的附加資訊
當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓入棧,當方法執行完後,便會將棧幀出棧
執行緒當前執行的方法所對應的棧幀位於Java棧的頂部
棧幀
區域性變數表
運算元棧
指向執行時常量池的引用
方法返回地址
額外的附加資訊
堆
儲存物件本身以及陣列
方法區 --- 執行緒共享區域
1、儲存了每個類的資訊(包括:類名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯後的程式碼等
2、常量池
本地方法棧
3.Hash表
雜湊表,能快速定位到想要查詢的記錄,而不是與表中存在的記錄的關鍵字比較來進行查詢。
Hash表的設計,採用了函式對映思想,將記錄的儲存位置與記錄的關鍵字關聯起來,從而能夠很快速地進行查詢
eg:
張三 13980593357
李四 15828662334
王五 13409821234
張帥 13890583472
Hash表能通過"李四"這個資訊直接獲取到該記錄在表中的位置,複雜度為O(1)
原理:
Hash表採用一個對映函式
f:key --> address
將關鍵字key對映到該記錄在表中的儲存位置
說明:
1.對映關係 稱為 Hash函式
2.通過Hash函式和關鍵字計算出來的儲存位置(hash表中的儲存位置,不是實際的實體地址) 稱為 Hash地址
聯絡人資訊採用Hash表儲存時,當想找 "李四" 的資訊時,直接根據 "李四" 和 Hash函式,計算出Hash地址即可
4.Java執行緒安全的本質:執行緒中並沒有存放任何物件資料,而是在執行時,去主記憶體(堆)中去同步資料,所有的物件資料都存在JVM的堆中,因此需要對資源進行共享鎖!!!
堆 --- JVM的核心資料儲存區 --- 執行緒共享的主記憶體
堆中為JVM的所有物件分配了記憶體空間用以儲存和維護變數值等
棧 --- 執行緒私有的記憶體區,由棧幀(執行緒棧)組成,存放8中基本資料型別和物件引用
每個執行緒都會生成一個自有的執行緒棧,執行緒棧中用儲存了該執行緒的基本資料常量,變數值,以及物件長變數的引用
每個執行緒執行時,根據程式碼順序,壓棧 棧幀(棧記憶體)
物件變數線上程執行時的過程:!!! --- 由JVM記憶體模型決定
1.執行緒根據棧中的引用去堆上同步該物件資料下來,然後線上程自己的記憶體中進行操作
2.操作之後再將執行緒棧撒花姑娘的運算結果同步到堆(主記憶體)中
3.多執行緒時,因為每個執行緒都操作自己從主記憶體(JVM堆)中同步過來的資料,如果不加鎖,會導致執行緒安全問題(資料提交到主記憶體時不一致)
5.堆 --- JVM中所有物件的記憶體空間 分為: Young Gen, Old Gen
Young Gen 又分為:Eden區和兩個大小相同的Survivor區(from 和 to)
Eden和Survivor預設比例 8:1 由 -XX:SurvivorRation設定
堆大小 -Xmx -Xms 設定
Young Gen -Xmn 設定
-XX:NewSize和-XX:MaxNewSize
用於設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。
Minor GC --- 發生在新生代的垃圾回收,Java 物件大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,
年輕代的GC使用複製演算法(將記憶體分為兩塊,每次只用其中一塊,當這塊記憶體用完,就將還活著的物件複製到另外一塊上面,複製演算法不會產生記憶體碎片)
Full GC --- 發生在年老代的GC, Full GC比較慢,儘量避免
新建立物件都會被分配到Eden區(一些大物件特殊處理),當Eden區滿則進行Minor GC,
這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區, 物件在Survivor區中每熬過一次Minor GC,年齡增加1歲,
當年齡增加到一定程度後(預設15歲),會被移動到年老代中,
當年老代滿時,經常Full GC
執行緒Stack 每個執行緒獨有的運算元棧區域性變數表方法入口 -Xss 設定
方法區 -XX:PermSize和-XX:MaxPermSize設定
6.JVM class類載入載入機制 --- JVM把描述類的資料從class載入到記憶體,並對資料進行校驗,轉化解析和初始化,最終得到可被虛擬機器直接使用的Java型別
類裝載器: 尋找類的位元組碼檔案, 並構造出類在JVM內部表示的物件元件
JVM類載入器把一個類裝入JVM過程:
(1) 裝載:查詢和匯入Class檔案;
(2) 連結:把類的二進位制資料合併到JRE中;
(a)校驗:檢查載入Class檔案資料的正確性,確保被載入類資訊符合JVM規範;
(b)準備:給類的靜態變數分配儲存空間,並將其初始化為預設值;
(c)解析:將虛擬機器常量池中符號引用轉成直接引用;
(3) 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作,為類的靜態變數賦初始值
說明:
1.類載入的雙親委託機制
某個特定的類載入器在接到載入類的請求時,首先將載入任務交給父類載入器,父類載入器又將載入任務向上委託,直到最高層的父類載入器,
如果最高層的父類載入器可以完成類載入任務,就成功返回,否則向下傳遞載入任務,由其子類載入器進行載入
2.初始化步驟
1.如果類沒有被載入和連結,則先進行載入和連結
2.如果類存在直接父類,並且父類未被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類
3.如果存在static塊,一次執行這些初始化語句
4.static塊,會在類第一次被使用時執行(呼叫類的靜態變數,初始化物件,Class.forName等)
7.正規表示式總計:
^ 和$表示以字串開始和以字串結尾。例:^abc 表示必須以abc開始(如:abcd,abcefd),abc$ 表示必須以abc結尾(如:);^abc$ 只能是abc(abc是個整體,abcabc不匹配) ;abc 表示包含abc的字串
* 和 + 和 ? 分別表示出現0次或多次,1次或多次,0次或1次。例:abc*表示有0個或多個abc,其他兩個同理
上面的*+?完全可以用範圍代替,abc{2}表示ab後面有至少兩個c,如abcc,dfdabccccc都是符合的;abc{2}$ 只有以abcc結尾的符合,如343abcc
abc{1,2} 表示ab後面跟著1或2個c;
abc{3,} 表示ab後面跟著至少3個c; {,3}這種是不正確的
| 或運算 ab|cd 表示字串裡有ab或者cd;
. 可以替換任意字元
下面是幾種是需要記住的
"[ab]":表示一個字串有一個"a"或"b"(相當於"a|b");
"[a-d]":表示一個字串包含小寫的'a'到'd'中的一個(相當於"a|b|c|d"或者"[abcd]");
"^[a-zA-Z]":表示一個以字母開頭的字串;
"[0-9]%":表示一個百分號前有一位的數字;
",[a-zA-Z0-9]$":表示一個字串以一個逗號後面跟著一個字母或數字結束。
下面看看具體的例項,比如我今天做的:一個輸入框,可以輸入數字,也可以輸入多個數字用逗號隔開,或者兩個數字用~分隔。
我寫的正規表示式 : ((^[0-9]+[~]?)?|^([0-9]+[,])+)[0-9]+$
8.JVM中的GC垃圾回收和記憶體分配策略 ---- JVM高階 參考:
http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
http://www.cnblogs.com/zhguang/tag/Java/
http://blog.csdn.net/column/details/javavirtualmachine.html
http://blog.csdn.net/eric_sunah/article/details/7870906
1.判斷回收物件
1.引用計數演算法
給物件中新增一個引用計數器,每當有一個地方引用它,計數器值+1,當引用失效,計數器值-1,任何時刻計數器為0的物件就不可能再被使用
缺點:
無法解決物件的迴圈依賴問題!!!
2.可達性演算法 --- JVM使用的GC判斷演算法
通過一系列稱為 GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鏈,
當一個物件到 GC Roots 沒有任何引用鏈相連時(即:GC Roots到這個物件不可達),則此物件是不可用的
Java中可作為 GC Roots的物件包括以下幾種:
1.虛擬機器棧(棧幀的本地變數表)中引用的物件
2.方法區中類靜態屬性引用的物件
3.方法區中常量引用的物件
4.本地方法棧中JNI(Native方法)引用的物件
2.JVM的垃圾回收機制
1.幾個概念:
記憶體分代 新生代(Eden + 2 Survivor )、老年代、永久代
新建立的物件分配在Eden區,經歷一次Minor GC後被移到 Survivor1區,再經歷一次Minor GC被移到Survivor2區,直到升至老年代
大物件(長字串或大陣列)可能直接存放到老年代
物件建立都在堆上,類資訊、常量、靜態變數儲存在方法區,堆和方法區是線性共享的
GC由守護執行緒執行,在從記憶體回收一個物件之前,會呼叫物件的finalize()方法
GC的觸發由JVM堆記憶體大小決定,System.gc()和Runtime.gc()會向JVM傳送執行GC的請求,但JVM不保證一定會執行GC
堆中沒有記憶體建立新物件時,會丟擲 OutOfMemoryError
2.GC回收什麼物件?
可達性演算法 --- 根搜尋法
--- 通過一系列稱為GC Roots的物件作為起始點,從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,證明此物件不可用,可以被回收了
程式把所有的引用關係看做一張圖,從一個節點 GC Roots開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢後,剩餘的節點被認為是沒有被引用的節點,可被回收
--- 可被作為GC Roots的物件
1.虛擬機器棧中引用的物件(本地變數表)
2.方法靜態屬性引用的物件
3.方法區中常量引用的物件
4.本地方法棧用引用的物件(Native物件)
引用計數法
--- 可能有迴圈依賴問題,導致計數器不為0,物件永遠無法被回收
多次掃描,而都不可達的物件,經歷幾次掃描後會被回收
3.垃圾回收演算法: 參考:http://www.cnblogs.com/sunniest/p/4575144.html
任何垃圾回收演算法,只做2件基本事情:
1.發現無用可被回收物件 (可達性演算法)
2.回收被無用物件佔用的空間,使得該空間可被再次使用
1.標記-清除演算法:
從GC Roots根集合進行掃描,對存活的物件進行標記,標記完成後,在掃描整個空間中未被標記的物件,進行回收
優點:不必移動物件,在存活物件比較多的情況下極為高效
缺點:GC時會暫停整個應用,容易造成不連續的記憶體碎片
2.複製演算法:
為了解決記憶體碎片問題而產生的一種演算法。它的整個過程可以描述為:標記所有的存活物件;通過重新調整存活物件位置來縮並物件圖;更新指向被移動了位置的物件的指標
將記憶體劃分為大小相等的2塊,每次只是用其中一塊,當一塊用完後,將還存活的物件複製到另一塊記憶體上,然後把已使用過的記憶體空間一次清理掉,
這樣使得每次都是對其中的一塊進行記憶體回收,不會產生碎片等情況,只要移動堆訂的指標,按順序分配記憶體即可,實現簡單,執行高效
優點:不會造成記憶體碎片
缺點:記憶體縮小為原來的一半,需要移動物件
3.標記-整理演算法:
先進行 標記-清除演算法,然後在清理完無用物件後,將所有存活的物件移動到一端,並更新引用其物件的指標
4.分代演算法:
基於物件生命週期分析得出的垃圾回收演算法。把物件分為年輕代、年老代、持久代,對不同的生命週期使用不同的演算法
年輕代: --- Minor GC, 物件生命週期短 使用: Serial、PraNew、Parallel Scavenge 收集器
1.所有新生成物件,目標是儘快收集掉生命週期短的物件
2.新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分物件在Eden區中生成。回收時先將eden區存活物件複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活物件複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
Eden : Survivor0:Survivor1 = 8:1:1
3.當survivor1區不足以存放 eden和survivor0的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)
老年代: --- Full GC 物件生命週期長 使用:Serial Old、Parallel Old、CMS收集器
1.在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。
2.記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。
永久代: --- 用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響
4.GC實現 --- 垃圾收集器
Serial --- 複製演算法
新生代單執行緒收集器,標記和清理都是單執行緒,優點:簡單高效
Serial Old --- 標記-整理演算法
老年代單執行緒收集器
ParNew --- 停止複製演算法
Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現
Parallel Scavenge ---
並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間)。適合後臺應用等對互動相應要求不高的場景
Parallel Old收集器(停止-複製演算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS --- 標記清理演算法
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
5.GC執行機制
Minor GC: 新物件在年輕代的Eden區申請記憶體失敗時,觸發Minor GC --- 年輕代
--- 只對年輕代的Eden去進行,不影響老年代
--- 頻繁進行
--- 選用速度快、效率高的演算法,使Eden儘快空閒
Full GC: 對整個堆進行整理,包括 年輕代、老年代、永久代
--- 對整個堆進行回收,速度比 Minor GC 慢
--- 儘量減少 Full GC次數
--- 物件JVM調優過程,大部分是針對Full GC的調節
可能導致Full GC的原因:
1.老年代滿
2.持久代滿
3.System.gc()被顯示呼叫
4.上一次GC之後Heap的各域分配策略動態變化
6.容易出現洩露的地方:
1.靜態集合類(HashMap、Vector),這些靜態變數生命週期和應用程式一樣,所有的物件也不能被釋放
2.各種連線、資料了連線、網路連線、IO連線沒被顯示呼叫close(),不被GC回收導致記憶體洩露
3.監聽器,在釋放物件的同時沒有相應刪除監聽器時可能導致記憶體洩露
9.JVM記憶體模型 http://www.infoq.com/cn/articles/java-memory-model-1
記憶體模型:描述了程式中各個變數(例項域、靜態域和陣列元素)直接的關係,以及在實際計算機系統中將變數儲存到記憶體、從記憶體中取出變數這樣的底層細節
JVM中存在一個主存,Java中所有物件成員變數都儲存在主存中,對於所有執行緒是共享的,每條執行緒都有自己的工作記憶體,
工作記憶體中儲存的是主存中某些物件成員變數的拷貝,執行緒對所有成員變數的操作都是在工作記憶體中進行,然後同步到主存,執行緒之間無法相互直接訪問,變數傳遞都需要通過主存
JMM(Java記憶體模型,Java Memory Model簡稱)是控制Java執行緒之間、執行緒和主存之間通訊的協議。
JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體(local memory),
本地記憶體中儲存了該執行緒以讀/寫共享變數的副本,執行緒在本地私有記憶體中操作完後,要將資料同步到主記憶體
Java記憶體模型中規定了:
所有變數都儲存在主記憶體中,每個執行緒都有自己的工作記憶體,執行緒的工作記憶體中使用到的變數,是主記憶體的副本拷貝,
執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,操作完後,要將資料同步到主記憶體
不同的執行緒之間工作記憶體是獨立的,執行緒間變數值的傳遞均需要在主記憶體來完成
特別注意:
(根據Java虛擬機器規範的規定,volatile變數依然有共享記憶體的拷貝,但是由於它特殊的操作順序性規定——
從工作記憶體中讀寫資料前,必須先將主記憶體中的資料同步到工作記憶體中,所有看起來如同直接在主記憶體中讀寫訪問一般,因此這裡的描述對於volatile也不例外)。
不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值得傳遞均需要通過主記憶體來完成
8種記憶體間的互動操作:
Java記憶體模型定義了8種操作來完成主記憶體與工作記憶體之間互動的實現細節
1.lock(鎖定)
作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態
2.unlock(解鎖)
作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定
3.read(讀取)
作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
4.load(載入)
作用於主記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中
5.use(使用)
作用於工作記憶體的變數,它把工作記憶體中的一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值得位元組碼指令時,將會執行這個操作
6.assign(賦值)
作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作
7.store(儲存)
作用於工作記憶體的變數,它把工作記憶體中的一個變數的值傳遞到主記憶體中,一般隨後的write操作使用
8.write(寫入)
作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數值放入主記憶體的變數中
8種基本操作必須滿足的規則:
1.不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行(即:read和load直接、store和write之間是可插入其他指令的)
2.不允許一個執行緒丟棄它的最近的assign操作,即:變數在工作記憶體中改變了之後,必須把該變化同步回主記憶體
3.不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從執行緒的工作記憶體同步回主記憶體中
4.一個新的變數只能從主記憶體中"誕生",不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數,
即:對一個變數實施use和store操作之前,必須先執行過了assign河load操作
5.一個變數在同一時刻只允許一條執行緒對其執行lock操作,但lock操作可以被同一個執行緒重複執行(可重入鎖),
多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖
6.如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值
7.如果一個變數實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數
8.對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體(執行store和write操作)
volatile變數的特殊規則: --- 不能保證原子性
1.保證此變數對所有執行緒的可見性
2.禁止指令重排序優化
記憶體模型有哪些規則?
原子性、可見性、可排序性
10.Java反射與動態代理
Java反射,可以知道Java類的內部構造,就可以與它進行互動,包括建立新的物件和呼叫物件中的方法等。
這種互動方式與直接在原始碼中使用的效果是相同的,但是又額外提供了執行時刻的靈活性。使用反射的一個最大的弊端是效能比較差
每個類被載入進記憶體後,系統就會為該類生成一個對應的 java.lang.Class物件,通過該Class物件即可訪問到JVM中的這個類
載入完類之後,在堆記憶體中會產生一個Class型別的物件(一個類只有一個Class物件),這個物件包含了完整的類結構資訊,這個Class物件就像一面鏡子,可以看到類的結構
程式在執行狀態中,可以動態載入一個只有名稱的類(全路徑),對於任意一個已經載入的類,都能夠知道這個類的所有屬性和方法,
對於任意一個物件,都能呼叫它的任意一個方法和屬性
用法:
作用1:獲取類的內部結構
Java反射API可以獲取程式在執行時刻的內部結構,只需短短十幾行程式碼,就可遍歷出一個Java類的內部結構,包括:構造方法、宣告的域和定義的方法等
java.lang.Class類的物件,可以通過其中的方法來獲取到該類中的構造方法、域和方法(getConstructor、getFiled、getMethod)
這三個方法還有相應的getDeclaredXXX版本,區別在於getDeclaredXXX版本的方法只會獲取該類自身所宣告的元素,而不會考慮繼承下來的。
Constructor、Field和Method這三個類分別表示類中的構造方法、域和方法。這些類中的方法可以獲取到所對應結構的後設資料。
作用2:執行時刻,對一個Java物件進行操作
動態建立一個Java類的物件,獲取某個屬性的值和呼叫某個方法
在Java原始碼中編寫的對類和物件的操作,都可以在執行時刻通過反射API來實現
特別說明:
clazz.setAccessible(true) --- 可以獲取到類中的private屬性和private方法
Class物件的獲取
1.物件的 getClass()方法
2.類.class屬性 --- 最安全,效能最好
3.Class.forName(String classFullName) 動態載入類 --- classFullName是類的全限定名(包.類名) --- 最常用
從Class中獲取資訊
1.獲取資訊
構造器、包含方法、包含屬性、包含的Annotation、實現的介面,所在包,類名,簡稱,修飾符等
獲取內容 方法簽名
構造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法 Method getMethod(String name, Class<?>... parameterTypes)
包含的屬性 Field getField(String name)
包含的Annotation <A extends Annotation> A getAnnotation(Class<A> annotationClass)
內部類 Class<?>[] getDeclaredClasses()
外部類 Class<?> getDeclaringClass()
所實現的介面 Class<?>[] getInterfaces()
修飾符 int getModifiers()
所在包 Package getPackage()
類名 String getName()
簡稱 String getSimpleName()
2.判斷資訊
註解型別、是否是了指定註解、是否是陣列、列舉、介面等
判斷內容 方法簽名
註解型別? boolean isAnnotation()
使用了該Annotation修飾? boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名類? boolean isAnonymousClass()
陣列? boolean isArray()
列舉? boolean isEnum()
原始型別? boolean isPrimitive()
介面? boolean isInterface()
obj是否是該Class的例項 boolean isInstance(Object obj)
反射生成並操作物件
通過Method物件執行相應的方法
通過Constructor物件呼叫對應的構造器來建立例項
通過Field物件直接訪問和修改物件的成員變數值
反射建立物件
2種方式:
1.使用Class物件的newInstance()建立該Class物件對應類的例項(要求改Class對應類有預設構造器)
2.先使用Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法建立該Class物件對應類的例項
--- 該方式,可選擇指定的構造器來建立例項
1.Spring根據配置檔案資訊(類的全限定名),使用反射建立物件 ---- 方式一,預設構造器建立例項
eg:
模擬Spring,實現一個物件池,物件池根據檔案讀取key-value對,然後建立這些物件,並放入Map中,
物件池可以將id作為key,將物件例項作為value,可以通過id獲取物件例項
即:
ObjectPool pool = ObjectPool.init("config.json");
User user = (User) pool.getObject("id1");
System.out.println(user);
Bean bean = (Bean) pool.getObject("id2");
System.out.println(bean);
參考:
http://blog.csdn.net/zjf280441589/article/details/50453776
配置檔案
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User"
},
{
"id": "id2",
"class": "com.fq.domain.Bean"
}
]
}
ObjectPool物件池
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
private static JSONArray getObjects(String config) throws IOException {
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
// 根據指定的JSON配置檔案來初始化物件池
public static ObjectPool init(String config) {
try {
JSONArray objects = getObjects(config);
ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
if (objects != null && objects.size() != 0) {
for (int i = 0; i < objects.size(); ++i) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
pool.putObject(id, getInstance(className));
}
}
return pool;
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Object getObject(String id) {
return pool.get(id);
}
public void putObject(String id, Object object) {
pool.put(id, object);
}
public void clear() {
pool.clear();
}
}
例項類User和Bean
public class User {
private int id;
private String name;
private String password;
}
public class Bean {
private Boolean usefull;
private BigDecimal rate;
private String name;
}
反射呼叫方法
獲取到某個類對應的Class物件後,可通過該Class物件的getMethod()來獲取一個Method陣列或Method物件,
每個Method物件對應一個方法,在獲取Method物件後,可通過呼叫invoke()方法呼叫該Method物件對應的方法
eg:
通過動態呼叫物件方法 + 配置檔案,來給物件設定值 --- 根據屬性建立物件(呼叫setter方法,可設定屬性和依賴的物件)
1.json格式的配置檔案,用來定義物件、屬性值及其依賴關係 --- config.json
注意:
其中fields代表該Bean所包含的屬性, name為屬性名稱, value為屬性值(屬性型別為JSON支援的型別),
ref代表引用一個物件(也就是屬性型別為Object,但是一定要引用一個已經存在了的物件)
這裡定義了一個User物件,設定其3個屬性
定義一個ComplexBean物件,設定其name屬性為complex-bean-name,並設定其引用的物件是id2
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User",
"fields": [
{
"name": "id",
"value": 101
},
{
"name": "name",
"value": "feiqing"
},
{
"name": "password",
"value": "ICy5YqxZB1uWSwcVLSNLcA=="
}
]
},
{
"id": "id2",
"class": "com.fq.domain.Bean",
"fields": [
{
"name": "usefull",
"value": true
},
{
"name": "rate",
"value": 3.14
},
{
"name": "name",
"value": "bean-name"
}
]
},
{
"id": "id3",
"class": "com.fq.domain.ComplexBean",
"fields": [
{
"name": "name",
"value": "complex-bean-name"
},
{
"name": "refBean",
"ref": "id2"
}
]
}
]
}
2.定義物件池,用於建立物件,並呼叫其方法給物件賦值
參考:反射.ObjectPool 的實現
package com.jay.advanced.java.反射;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模擬Spring,建立一個物件池
* Created by hetiewei on 2017/2/17.
*/
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
//從指定檔案,讀取配置資訊,返回解析後json陣列
private static JSONArray getObjects(String config) throws IOException {
//獲取輸入流
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
//讀取輸入流內容,變成json陣列返回
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
//根據類名,獲取類的物件例項
private static Object getIntance(String className, JSONArray fields) throws ClassNotFoundException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//配置的Class
Class<?> clazz = Class.forName(className);
//目標Class的例項物件
Object targetObject = clazz.newInstance();
//遍歷屬性,賦值給例項物件 --- 注意區分,直接賦值,還是賦值引用物件
if (fields != null && fields.size() != 0) {
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
//需要設定的成員變數名
String filedName = field.getString("name");
//需要設定的成員變數的值
Object fieldValue;
//如果是8種基本型別或String型別,直接獲取value,得到屬性賦值
if (field.containsKey("value")) {
fieldValue = field.get("value");
} else if (field.containsKey("ref")) {
//如果是引用型別, 先獲得引用物件的id,然後根據id,從物件池中得到引用物件
String refBeanId = field.getString("ref");
fieldValue = OBJECTPOOL.getObject(refBeanId);
} else {
throw new RuntimeException("沒有value 或 ref 引用");
}
//構造setterXxx
String setterName = "set" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
//需要設定成員變數的setter方法
Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
//呼叫setter方法設定值
setterMethod.invoke(targetObject, fieldValue);
}
}
return targetObject;
}
private static ObjectPool OBJECTPOOL;
//建立一個物件池例項(儲存多執行緒安全)
private static void initSingletonPool() {
if (OBJECTPOOL == null) {
synchronized (ObjectPool.class) {
if (OBJECTPOOL == null) {
OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
}
}
}
}
//指定根據的JSON配置檔案來初始化物件池
public static ObjectPool init(String config) {
//初始化pool
initSingletonPool();
//解析Json配置檔案
try {
JSONArray objects = getObjects(config);
for (int i = 0; i < objects.size(); i++) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
//初始化Bean,並放入物件池中
OBJECTPOOL.putObject(id, getIntance(className, object.getJSONArray("fields")));
}
return OBJECTPOOL;
} catch (IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void putObject(String id, Object obj) {
pool.put(id, obj);
}
public Object getObject(String id) {
return pool.get(id);
}
public void clear() {
pool.clear();
}
}
3.客戶端使用物件池
//初始化物件池
ObjectPool pool = ObjectPool.init("config.json");
//從物件池中根據id獲取物件例項
User user = (User) pool.getObject("id1");
Bean bean = (Bean) pool.getObject("id2");
ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
4.ComplexBean類
public class ComplexBean {
private String name;
private Bean refBean;
}
反射訪問並操作成員變數
通過Class物件的getField()方法可獲取該類所包含的全部或指定的成員變數Field,
getDeclaredXxx方法可以獲取所有的成員變數,無論private/public;
Field提供了讀取和設定成員變數值的方法
getXxx(Object obj)
獲取obj物件的該成員變數的值,此處的Xxx對應8中基本型別,如果該成員變數的型別是引用型別, 則取消get後面的Xxx;
setXxx(Object obj, Xxx val)
將obj物件的該成員變數值設定成val值.此處的Xxx對應8種基本型別, 如果該成員型別是引用型別, 則取消set後面的Xxx;
eg:
通過反射,設定成員變數值
User user = new User();
//反射獲取物件的指定屬性
Field idField = User.class.getDeclaredFiled("id");
//設定該屬性即使為private也可被訪問
idField.setAccessible(true);
//將物件的常用變數,設定為指定值 --- 這裡將user物件的id屬性,設定為30
idField.setInt(user, 30);
private void setAccessible(AccessibleObject object) {
object.setAccessible(true);
}
反射獲取註解資訊
只需要獲取到Class Method Filed等這些實現了AnnotatedElement介面的類例項, 就可以獲取到我們想要的註解資訊
eg:
獲取client()方法上的註解資訊
Annotation[] annotations = this.getClass().getMethod("client").getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType().getName());
}
eg:
獲取某個註解中的後設資料,需要強轉成所需的註解型別,然通過註解物件的抽象方法來訪問這些後設資料
@Tag(name = "client")
public class Client {
@Test
public void client() throws NoSuchMethodException {
Annotation[] annotations = this.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Tag) {
Tag tag = (Tag) annotation;
System.out.println("name: " + tag.name());
System.out.println("description: " + tag.description());
}
}
}
}
反射獲取泛型資訊
為了通過反射操作泛型,Java新增了4種型別來代表不能歸一到Class了下,但又和原始型別同樣重要的型別
型別 含義
ParameterizedType 一種引數化型別, 比如Collection<String>
GenericArrayType 一種元素型別是引數化型別或者型別變數的陣列型別
TypeVariable 各種型別變數的公共介面
WildcardType 一種萬用字元型別表示式, 如? ? extends Number ? super Integer
動態代理:
代理模式:
代理物件和被代理物件一般實現相同的介面,呼叫者與代理物件進行互動。代理的存在對於呼叫者來說是透明的,呼叫者看到的只是介面。
代理物件則可以封裝一些內部的處理邏輯,如訪問控制、遠端通訊、日誌、快取等。比如一個物件訪問代理就可以在普通的訪問機制之上新增快取的支援,
傳統的代理模式的實現,需要在原始碼中新增一些附加的類。這些類一般是手寫或是通過工具來自動生成
動態代理:
JDK5引入了動態代理機制,允許開發人員在執行時刻動態的建立出代理類及其物件。
在執行時刻,可以動態建立出一個實現了多個介面的代理類,每個代理類的物件都會關聯一個表示內部處理邏輯的InvocationHandler介面的實現,
當使用者呼叫代理物件所代理的介面中的方法時,這個呼叫資訊被傳遞個InvocationHandler的invoke()方法,
在invoke()方法引數中可以獲取到代理物件、方法對應的Method物件和呼叫的實際引數,invoke()方法的返回值被返回給使用者。
相當於對方法呼叫進行了攔截 --- 這是一個不需要依賴AspectJ等AOP框架的一種AOP實現方式
11.常用的記憶體調節引數
-Xms 初始堆大小,預設是實體記憶體1/64(<1G)
-Xmx 最大堆大小
-Xmn 新生代的記憶體空間大小(eden+ 2 survivor space),
整個堆大小=新生代大小 + 老生代大小 + 永久代大小
在保證堆大小不變的情況下,增大新生代後,將會減小老生代大小。此值對系統效能影響較大,Sun官方推薦新生代配置為整個堆的3/8
-XX:SurvivorRation 新生代Eden區和Survivor區容量比,預設是8, 兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10
-Xss 每個執行緒的堆疊大小, 推薦 256K
-XX:PermSize 持久代(非堆記憶體)初始值, 預設是實體記憶體 1/64
-XX:MaxPermSize 持久代(非堆記憶體)最大值,預設實體記憶體1/4
-XX:+UseParallelGC 多核處理器,配置後提示GC效率
eg:
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=256M
-vmargs 說明後面是VM的引數,所以後面的其實都是JVM的引數了
-Xms128m JVM初始分配的堆記憶體
-Xmx512m JVM最大允許分配的堆記憶體,按需分配
-XX:PermSize=64M JVM初始分配的非堆記憶體
-XX:MaxPermSize=128M JVM最大允許分配的非堆記憶體,按需分配
說明:
Java 虛擬機器具有一個堆,堆是執行時資料區域,所有類例項和陣列的記憶體均從此處分配。堆是在 Java 虛擬機器啟動時建立的。”“在JVM中堆之外的記憶體稱為非堆記憶體(Non-heap memory)”。
JVM主要管理兩種型別的記憶體:堆和非堆。簡單來說堆就是Java程式碼可及的記憶體,是留給開發人員使用的;非堆就是JVM留給自己用的,
方法區,JVM記憶體處理貨優化所需的記憶體、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法的程式碼都在非堆記憶體中
記憶體分配方法:
1.堆上分配
2.棧上分配
12.JVM記憶體管理
程式計數器
方法區
堆
棧(JVM棧 + 本地方法棧)
說明:
GC主要發生在堆上,方法區也會發生GC, 棧與暫存器是執行緒私有的,不會GC
方法區:
存放內容:類資訊、類的static屬性、方法、常量池
已經載入的類的資訊(名稱、修飾符等)
類中的static變數
類中的field資訊
類中定義為final常量
類中的方法資訊
執行時常量池:編譯器生成的各種字面量和符號引用(編譯期)儲存在class檔案的常量池中,這部分內容會在類載入之後進入執行時常量池
使用例項: 反射
在程式中通過Class物件呼叫getName等方法獲取資訊資料時,這些資訊資料來自方法區
調節引數:
-XX:PermSize 指定方法區最小值,預設16M
-XX:MaxPermSize 指定方法區最大值,預設64M
所拋異常:
方法區使用記憶體超過最大值,丟擲 OutOfMemoryError
GC操作:
對類的解除安裝
針對常量池的回收
總結:
企業開發中, -XX:PermSize==-XX:MaxPermSize,都設定為256M
eg:
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
類中的static變數會在方法區分配記憶體,但是類中的例項變數不會(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧)
關於方法區的存放內容,可以這樣去想所有的通過Class物件可以反射獲取的都是從方法區獲取的(包括Class物件也是方法區的,Class是該類下所有其他資訊的訪問入口)
堆:
存放內容:
物件例項(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧)
陣列值
使用例項:
new建立的物件都在這裡分配
調節引數:
-Xmx:最大堆記憶體,預設為實體記憶體的1/4但小於1G
-Xms:最小堆記憶體,預設為實體記憶體的1/64但小於1G
-XX:MinHeapFreeRatio,預設當空餘堆記憶體小於最小堆記憶體的40%時,堆記憶體增大到-Xmx
-XX:MaxHeapFreeRatio,當空餘堆記憶體大於最大堆記憶體的70%時,堆記憶體減小到-Xms
注意:
在實際使用中,-Xmx與-Xms配置成相等的,這樣,堆記憶體就不會頻繁的進行調整了!!!
所拋錯誤:
OutOfMemoryError:Java heap space
堆記憶體劃分:
新生代:
Eden + from + to from 與 to 大小相等
-Xmn:整個新生代大小
-XX:SurvivorRation : 調整Eden:from(to)的比例,預設是 8:1 即: eden:from:to = 8:1:1
老年代:
新建物件直接分配到老年代的2種情況:
大物件:-XX:PretenureSizeThreshold(單位:位元組)引數來指定大物件的標準
大陣列:資料中的元素沒有引用任何外部的物件
總結:
企業開發中 -Xmx==-Xms
eg:
<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年輕代大小-Xmn==512m,這樣,年老代大小就是2048-512==1536m,這個比率值得記住,
在企業開發中,年輕代:年老代==1:3,而此時,我們配置的-XX:MaxTenuringThreshold=15(這也是預設值),年輕代物件經過15次的複製後進入到年老代
年輕代分為Eden和2個Survivor(from+to),預設 Eden:from:to==8:1:1
1.新產生的物件有效分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的物件會直接進入年老代)
2.當Eden區滿了或放不下時,其中存活的物件會複製到from區(注意:如果存活下來的物件from區放不下,則這些存活下來的物件全部進入老年代),之後Eden區的記憶體全部回收掉
注意:如果Eden區沒有滿,但來了一個小物件Eden區放不下,這時候Eden區存活物件複製到from區後,清空Eden區,之後剛才的小物件再進入Eden區
3.之後產生的物件繼續分配在Eden區,當Eden區滿時,會把Eden區和from區存活下來的物件複製到to(同理,如果存活下來的物件to區都放不下,則這些存活下來的物件全部進入年老代),之後回收掉Eden區和from區的所有記憶體;
4)如上這樣,會有很多物件會被複制很多次(每複製一次,物件的年齡就+1),預設情況下,當物件被複制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了
5)當年老代滿了或者存放不下將要進入年老代的存活物件的時候,就會發生一次Full GC(這個是我們最需要減少的,因為耗時很嚴重)
棧:
注意點:
每個執行緒都會分配一個棧,每個棧中有多個棧幀(每個方法對應一個棧幀) 每個方法在執行的同時都會建立一個棧幀,每個棧幀用於儲存當前方法的區域性變數表、運算元棧等,
每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程,說的更明白一點,就是方法執行時建立棧幀,方法結束時釋放棧幀所佔記憶體
存放內容:
區域性變數表: 8種基本資料型別、物件引用, 該空間在編譯期已經分配好,執行期不變
引數調節:
-Xss:設定棧大小,通常設定為1m就好
eg:
<jvm-arg>-Xss1m</jvm-arg>
所拋異常:
StackOverFlowError 執行緒請求的棧深度大於虛擬機器所允許的深度
eg:
沒有終止調節的遞迴(遞迴基於棧)
每個方法的棧深度在javac編譯之後就已經確定了
OutOfMemoryError: 虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠記憶體,則拋該異常
注意:
棧可以動態擴充套件,但棧中的區域性變數表不可以
C暫存器(程式計數器)
概念: 當前執行緒所執行的位元組碼的行號指示器,用於位元組碼直譯器對位元組碼指令的執行。
多執行緒:通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條執行緒中的指令,
為了執行緒切換後能恢復到正確的執行位置,每條執行緒都要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。
記憶體分配概念:
在類載入完成後,一個物件所需的記憶體大小就可以完全確定了,具體的情況檢視物件的記憶體佈局。
為物件分配空間,即把一塊兒確定大小(上述確定下來的物件記憶體大小)的記憶體從Java堆中劃分出來
Java的物件記憶體佈局 3 大塊
物件頭
儲存物件自身的執行時資料:Mark Word(在32位和64位JVM上長度分別為32bit和64bit),包含資訊如下:
物件hashCode
物件GC分代年齡
鎖狀態標識
執行緒持有的鎖
偏向鎖相關:偏向鎖、自旋鎖、輕量級鎖以及其他的一些鎖優化策略是JDK1.6加入的,這些優化使得Synchronized的效能與ReentrantLock的效能持平,
在Synchronized可以滿足要求的情況下,優先使用Synchronized,除非是使用一些ReentrantLock獨有的功能,例如指定時間等待等。
型別指標: 物件指向類後設資料的指標
JVM通過這個指標來確定這個物件是哪個類的例項(根據物件確定其Class的指標)
例項資料
物件真正儲存的有效資訊
對齊填充
JVM要求物件的大小必須是8的整數倍,若不夠,需要補位對齊
注意:
1.Mark Word是非固定的資料結構,以便在極小空間記憶體儲儘量多的資訊
2.如果物件是一個陣列,物件頭必須有一塊用來記錄陣列長度的資料,JVM可以通過Java物件的後設資料確定物件長度,但對於陣列不行
3.基本資料型別和物件包裝類所在的記憶體大小(位元組)
boolean 1位元組
byte 1位元組
short 2位元組
char 2位元組
int 4位元組
float 4位元組
long 8位元組
double 8位元組
引用型別 在32位和64位系統上長度分別為4bit和8bit
記憶體分配的 2 種方式:
1.指標碰撞
適用場合:
堆記憶體規整(即:沒有記憶體碎片,有大塊完整記憶體)的情況下
原理:
用過的記憶體全部整合到一邊,沒用過的記憶體放在另一邊,中間有個分界值指標,只需要向著沒有用過的記憶體方向將該指標移動新建立的物件記憶體大小位置即可
GC收集器:
Serial、ParNew
2.空閒列表
適用場合:
堆記憶體不規整情況
原理:
JVM虛擬機器會維護一個列表,該列表會記錄哪些記憶體塊是可用的,在分配時,找一塊足夠大的記憶體來劃分給new的物件例項,最後更新列表記錄
GC收集器:
CMS
注意:
1.2種記憶體分配方式,取決於Java堆記憶體是否規整
2.Java堆記憶體是否規整,取決於GC收集器的演算法是 "標記-清除" 還是 "標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製演算法記憶體也是規整的
建立一個真正物件的基本過程:5步
1.類載入機制檢查
JVM首先檢查一個new指定的引數是否能在常量池中定位到一個符號引用,並且檢查該符號應用代表的類是否已被載入、解析和初始化過
實際就是在檢查new的物件所屬的類是否已經執行過類載入機制,如果沒有,則先進行載入機制載入類
2.分配記憶體
把一塊確定大小的記憶體從堆中劃分出來
3.初始化零值
物件的例項欄位不需要賦初始值也可以直接通過其預設零值
每種型別的物件都有不同的預設零值
4.設定物件頭
5.執行<init>
為物件字元賦值(第3步只是初始化了零值,這裡會根據引數,給例項賦值)
13.JVM 記憶體回收GC
1.記憶體回收區域
堆:GC主要區域
方法區: 回收無用的類和廢氣的常量
注意:
棧和PC暫存器是執行緒私有,不會發生GC
2.判斷物件是否存活
1.引用計數法
原理:
給物件新增一個引用計數器,每當有一個地方使用它,計數器值+1,引用失效時,計數器值-1
缺點:
1.每次為物件賦值時,都要進行計數器值得加減,消耗較大
2.對於迴圈依賴無法處理
2.可達性分析(跟蹤收集)
原理:
從根集合(GC Root)開始向下掃描,根集合中的節點可以到達的節點就是存活節點,根集合中的節點到達不了的節點就是要被回收的的節點
GC Root節點: 全域性性的引用(常量和靜態屬性)和棧引用
1.Java棧中的物件引用
2.方法區中, 常量+靜態變數
3. 3 種引用型別
強引用: A a = new A();//a是常引用
強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題
軟引用: 記憶體不足時,是否弱引用所引用的物件,當記憶體足夠時,就是一個普通物件(強引用)
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a); //軟引用
弱引用: 弱引用物件只能存活到下一次垃圾會話之前,一旦發生垃圾回收,立刻被回收掉
4.GC回收演算法
1.標記-清楚演算法 --- 年老代
2.標記-整理演算法(標記-壓縮) --- 年老代
3.複製演算法 --- 年輕代
標記-清楚演算法 --- 年老代
原理:
從根集合點掃描,標記處所有的存活物件,最後掃描整個記憶體空間,並清除沒有標記的物件(即:死亡物件)
使用場合:
存活物件較多的情況下比較高效
適用於年老代
缺點:
容易產生記憶體碎片,再來一個比較大的物件時(典型情況:該物件的大小大於空閒表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
掃描了整個空間兩次(第一次:標記存活物件;第二次:清除沒有標記的物件)
注意:
在該情況下,記憶體不規整,物件的記憶體分配採用"空閒列表法"
標記-整理演算法(標記-壓縮) --- 年老代
原理:
從根集合節點進行掃描,標記出所有的存活物件,最後掃描整個記憶體空間並清除沒有標記的物件(即死亡物件)(可以發現前邊這些就是標記-清除演算法的原理),清除完之後,將所有的存活物件左移到一起。
適用場合:
用於年老代(即舊生代)
缺點:
需要移動物件,若物件非常多而且標記回收後的記憶體非常不完整,可能移動這個動作也會耗費一定時間
掃描了整個空間兩次(第一次:標記存活物件;第二次:清除沒有標記的物件)
優點:
不會產生記憶體碎片
注意:
在該情況下,記憶體規整,物件的記憶體分配採用"指標碰撞法"
複製演算法 --- 年輕代
原理:
從根集合節點進行掃描,標記出所有的存活物件,並將這些存活的物件複製到一塊兒新的記憶體(圖中下邊的那一塊兒記憶體)上去,之後將原來的那一塊兒記憶體(圖中上邊的那一塊兒記憶體)全部回收掉
適用場合:
存活物件較少的情況下比較高效
掃描了整個空間一次(標記存活物件並複製移動)
適用於年輕代(即新生代):基本上98%的物件是"朝生夕死"的,存活下來的會很少
缺點:
需要一塊兒空的記憶體空間
需要複製移動物件
注意:
在該情況下,記憶體規整,物件的記憶體分配採用"指標碰撞法",見《第二章 JVM記憶體分配》
以空間換時間:通過一塊兒空記憶體的使用,減少了一次掃描
14.關於Set --- HashSet、TreeSet、LinkedHashSet -- 都是去重的,都可用Iterator或foreach進行遍歷
參考:http://blog.csdn.net/speedme/article/details/22661671
HashSet --- 去重,無序, add()時會呼叫hashcode和equals,所以儲存在HashSet中的物件需要重寫這兩個方法,非同步的,元素只能放一個null
即:
HashSet:資料結構式雜湊表,執行緒非同步。保證元素唯一性的原理,判斷hashCode是否相同,如果相同,判斷元素的equals方法
TreeSet --- 去重,可按照某種順序排序, add()會將物件轉為Comparable,然後呼叫compareTo()方法,所以儲存在TreeSet中的物件必須實現Comparable,重寫compareTo()方法
底層資料結構是 二叉樹,保證元素唯一性的依據
支援2種排序:自然排序、定製排序
TreeSet判斷兩個物件不相等的方式是兩個物件通過equals方法返回false,或者通過CompareTo方法比較沒有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。
Java提供了一個Comparable介面,該介面裡定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該介面的物件就可以比較大小。
obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個物件相等,如果返回一個正數,則表明obj1大於obj2,如果是 負數,則表明obj1小於obj2。
如果我們將兩個物件的equals方法總是返回true,則這兩個物件的compareTo方法返回應該返回0
定製排序
自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator介面,實現 int compare(T o1,T o2)方法
2種排序方式比較:
方式一:讓集合中的元素自身具有比較性,這就讓加入到TreeSet集合中的物件必須實現comparable介面重寫compareTo(Object obj)方法
這種方式也成為元素的自然排序或預設排序。(但是如果排序的元素不是本人寫的,別人寫的沒有實現comparable介面時想排序使用第二種方式)
方式二:讓集合容器具有比較性,自定義一個比較器實現comparator介面,重寫compare(Object o1,Object o2)方法,在初始化TreeSet容器物件將這個
自定義的比較器作引數傳給容器的建構函式,使得集合容器具有比較性,使用這種方式的優先順序高於方式一,
LinkedHashSet --- HashSet的子類,去重,並保留儲存順序
HashSet 工作原理: 每次儲存物件時,呼叫物件的hashCode(),計算一個hash值,在集合中查詢是否包含hash值相同的元素
如果沒有hash值相同的元素,根據hashCode值,來決定該物件的儲存位置,直接存入,
如果有hash值相同的元素,逐個使用equals()方法比較,
比較結果全為false就存入.
如果比較結果有true則不存.
如何將自定義類物件存入HashSet進行去重複
* 類中必須重寫hashCode()方法和equals()方法
* equals()方法中比較所有屬性
* hashCode()方法要保證屬性相同的物件返回值相同, 屬性不同的物件儘量不同
TreeSet 工作原理:儲存物件時,add()內部會自動呼叫compareTo()方法進行比較,根據比較結果使用二叉樹形式進行儲存 --- 二叉樹實現儲存
參考:http://blog.csdn.net/jinhuoxingkong/article/details/51191106
TreeSet使用二叉樹原理,對新add()的物件安裝指定的順序排序(升序、降序),每增加一個物件都會進行排序,將物件插入二叉樹指定的位置
Integer和String都是按預設的TreeSet排序,自定義的物件,必須實現Comparable介面,重寫compareTo(),指定比較規則
在重寫compare()方法時,要返回相應的值才能使TreeSet按照一定規則來排序,升序是:比較此物件與指定物件的順序。如果該物件小於、等於或大於指定物件,則分別返回負整數、零或正整數。
如果想把自定義類的物件存入TreeSet進行排序, 那麼必須實現Comparable介面
* 在類上implement Comparable
* 重寫compareTo()方法
* 在方法內定義比較演算法, 根據大小關係, 返回正數負數或零
TreeSet實現排序的2種比較方式:
1.自定義類類 實現 Comparable介面,重寫其 compareTo()方法 ---- 類排序
2.給TreeSet定義一個實現Comparator介面的比較器,重寫其 compare()方法 ---- 比較器排序
* a.自然順序(Comparable)
* TreeSet類的add()方法中會把存入的物件提升為Comparable型別
* 呼叫物件的compareTo()方法和集合中的物件比較
* 根據compareTo()方法返回的結果進行儲存
* b.比較器順序(Comparator)
* 建立TreeSet的時候可以制定 一個Comparator
* 如果傳入了Comparator的子類物件, 那麼TreeSet就會按照比較器中的順序排序
* add()方法內部會自動呼叫Comparator介面中compare()方法排序
* 呼叫的物件是compare方法的第一個引數,集合中的物件是compare方法的第二個引數
* c.兩種方式的區別
* TreeSet建構函式什麼都不傳, 預設按照類中Comparable的順序(沒有就報錯ClassCastException)
* TreeSet如果傳入Comparator, 就優先按照Comparator
15.關於Map --- HashMap、TreeMap
原理:http://blog.csdn.net/chenssy/article/details/26668941
HashMap
按照key的hashCode實現,無序
TreeMap
基於紅黑樹實現,對映根據其key的自然順序進行排序,或根據建立對映時提供的Comparator進行排序,具體取決於使用的構造方法
TreeMap只能依據key來排序,不能根據value排序
如果想對value排序,可以把TreeMap的EntrySet轉換成list,然後使用Collections.sort排序 -- 參考:http://blog.csdn.net/liuxiao723846/article/details/50454622
http://blog.csdn.net/xiaoyu714543065/article/details/38519817
eg:value是String或引用型別的值,按照指定規則對value進行排序
public static Map sortTreeMapByValue(Map map){
List<Map.Entry> list = new ArrayList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry>() {
//升序排
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().toString().compareTo(o2.getValue().toString());
}
});
for (Map.Entry<String, String> e: list) {
System.out.println(e.getKey()+":"+e.getValue());
}
return map;
}
16.關於ThreadLocal
當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本
3個重要方法:
void set(T value)、T get()以及T initialValue()
使用場景:
多執行緒中,每個執行緒需要獨享這個變數,且每個執行緒用的變數最初都是一樣的,可以通過ThreadLocal處理該變數
原理:
ThreadLocal如何為每個執行緒維護變數的副本?
ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中key為執行緒物件,value為執行緒的變數副本
eg:
public class JavaTest {
// 建立一個Integer型的執行緒本地變數, 並通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];// 建立5個執行緒
for (int j = 0; j < 5; j++) {
threads[j] = new Thread(new Runnable() {
@Override
public void run() {
// 獲取當前執行緒的本地變數,然後累加5次
int num = local.get();// 返回當前執行緒的執行緒本地變數值,若對應的thread不存在,則會呼叫initialValue初始化
for (int i = 0; i < 5; i++) {
num++;
}
// 重新設定累加後的本地變數
local.set(num);
System.out.println(Thread.currentThread().getName() + " : "
+ local.get());
}
}, "Thread-" + j);
}
for (Thread thread : threads) {// 啟動執行緒
thread.start();
}
}
}
執行後結果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個執行緒累加後的結果都是5,各個執行緒處理自己的本地變數值,執行緒之間互不影響
17.自定義註解
元註解
Java提供了4種元註解,專門負責註解其他註解使用
@Retention 表示需要在什麼級別儲存該註釋資訊(生命週期)
可選引數:
RetentionPolicy.SOURCE: 停留在java原始檔,編譯器被丟掉
RetentionPolicy.CLASS:停留在class檔案中,但會被VM丟棄(預設)
RetentionPolicy.RUNTIME:記憶體中的位元組碼,VM將在執行時也保留註解,因此可以通過反射機制讀取註解的資訊 --- 最常用
@Target 表示該註解用於什麼地方
可選引數:
ElementType.CONSTRUCTOR: 構造器宣告
ElementType.FIELD: 成員變數、物件、屬性(包括enum例項)
ElementType.LOCAL_VARIABLE: 區域性變數宣告
ElementType.METHOD: 方法宣告
ElementType.PACKAGE: 包宣告
ElementType.PARAMETER: 引數宣告
ElementType.TYPE: 類、介面(包括註解型別)或enum宣告
@Documented 將註解包含在JavaDoc中
@Inheried 執行子型別繼承父類中的註解
自定義註解:
eg:
自定義註解 --- MyAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定義註解
* 作用於方法和類
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
//為註解新增屬性
String color();
String value() default "我是xxx";//為註解屬性提供預設值
int[] array() default {1,2,3};
Gender gender() default Gender.MAN; // 新增一個列舉
// 新增列舉屬性
MetaAnnotation metaAnnotation() default @MetaAnnotation(birthday = "我的出身日期為1988-2-18");
}
定義一個列舉類 --- Gender
public enum Gender{
MAN{
public String getName(){
return "男";
}
},
WOMEN{
public String getName(){
return "女";
}
};
}
定義註解類 --- MetaAnnotation
public @interface MetaAnnotation{
String birthday();
}
解析註解:
/**
* 呼叫註解並賦值
* Created by hetiewei on 2016/10/12.
*/
@MyAnnotation(metaAnnotation = @MetaAnnotation(birthday = "我的出身日期為1991-2-27"),
color = "red", array = {23, 26 })
public class Test {
public static void main(String args[]){
//檢查類Test中是否包含@MyAnnotation註解
if (Test.class.isAnnotationPresent(MyAnnotation.class)){
//若存在則獲取註解,並解析
MyAnnotation annotation = Test.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
//解析註解中的內容
//1.獲取註解屬性
System.out.println(annotation.color());
System.out.println(annotation.value());
//2.獲取屬性陣列
int[] arrs = annotation.array();
System.out.println(arrs.toString());
//3.獲取列舉
Gender gender = annotation.gender();
System.out.println("性別:"+gender);
//4.獲取註解屬性
MetaAnnotation meta = annotation.metaAnnotation();
System.out.println(meta.birthday());
}
}
}
18.關於列舉 列舉類是一種特殊的類,它一樣有自己的Field,方法,可以實現一個或者多個介面,也可以定義自己的構造器
列舉與普通類有如下簡單區別:
(1). 列舉類可以實現一個或者多個介面,使用enum定義的列舉類預設繼承了java.lang.Enum類,而不是繼承Object類。其中java.lang.Enum類實現了java.lang.Serializable和java.lang.Comparable介面。
(2). 使用enum定義,非抽象的列舉類預設會使用final修飾,因此列舉類不能派生子類。
(3). 列舉類的構造器只能使用private訪問控制符,如果省略了構造器的訪問控制符,則預設使用private修飾;如果強制指定訪問控制符,則只能指定private修飾符。
(4). 列舉類的所有例項必須在列舉類的第一行顯示列出,否則這個列舉類永遠不能產生例項。列出這些例項時,系統會自動新增public static final修飾,無須程式設計師顯示新增。
所有的列舉類都提供了一個values方法,該方法可以很方便的遍歷所有的列舉值
(5) 列舉常用方法
name() ,toString() --- 返回此列舉例項名稱,優先使用 toString()
ordinal() --- 返回列舉例項的索引位置,第一個列舉值索引為0
public static T valueOf(Class enumType, String name)
--- 返回指定列舉類中指定名稱的列舉值,名稱必須與在該列舉類中宣告列舉值時所用的識別符號完全匹配,不允許使用額外的空白字元
eg:
public enum SeasonEnum{
//列出4個列舉例項
SPRING,SUMMER,FALL,WINTER;
}
解析:
1.列舉值之間以英文逗號(,)隔開,列舉值列舉結束後以英文分號作為結束
2.列舉值代表了該列舉類的所有可能例項
3.使用列舉值 EnumClass.variable eg: SeasonEnum.SPRING
4.列舉可作為switch條件表示式
5.獲取所有列舉值 EnumClass[] enums = EnumClass.values();
列舉類的屬性Field、方法和構造器
1.列舉類也是一種類,只是它是一種比較特殊的類,因此它一樣可以定義Field,方法
19.幾種集合類解析
1.HashMap 底層 陣列+連結串列,計算出hash值後,得到元素在陣列中存放的位置索引,
若不同元素hash值相同,即:有相同的存放位置,則在相同位置建立連結串列,採用頭插入法依次儲存元素
工作原理:
陣列+連結串列 以 Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模陣列的大小得到陣列的下標
如果多個key佔有同一個下標(碰撞),則使用連結串列將相同的key串起來
通過hash方法,通過put和get儲存和獲取物件,儲存物件時,將K/V傳給put()方法時,它呼叫hashCode()計算hash值得到bucket位置,
進一步儲存,HashMap會根據當前bucket的佔用情況自動調整容量(超過載入因子,容量擴充套件為2倍)。
獲取物件時,通過K,呼叫hashCode()得到bucket位置,並進一步呼叫equals()方法確定鍵值對。
如果發生碰撞時,HashMap通過連結串列將產生碰撞的元素組織起來,在Java8中,如果一個bucket中碰撞的元素超過某個限制(,預設8個),
則使用紅黑樹來替換連結串列,從而提高速度
2.HashSet 底層 是HashMap實現, 優點:利用雜湊表提供查詢效率, 缺點:元素不能重複
由於HashSet不允許元素重複,故需要判斷元素是否相同,
用hash表判斷元素是否相同的方法,即需要hashCode和equals兩個方法,對於hashSet,先通過hashCode判斷元素在雜湊表中的位置是否相同,在通過equals方法判斷元素內容是否相同
雜湊表如何判斷元素是否相同?
1> 雜湊值是否相同 :判斷的其實是物件的hashCode()方法是否相同(看物件是否在雜湊表的同一個位置)
2>內容是否相同:用equals方法判斷物件是否相同。
規則:若hash值不同,不必判斷物件內容,返回false;若hash值相同,有必要判斷物件內容,若在相同,返回true,否則false。
3.TreeSet 使用元素的自然順序,物件集合中元素進行排序,新增的元素需要自己實現Comparable介面,以便預設排序時呼叫其CompareTo()進行比較
2中自定義排序方式
1.元素的類,實現Comparable介面,實現compareTo()
2.給ThreeSet傳遞一個實現Comparator介面的引數物件
20.關於執行緒池
參考:
http://www.codeceo.com/article/java-thread-pool-deep-learn.html
http://www.codeceo.com/article/java-threadpoolexecutor.html
1.核心類:
ThreadPoolExecutor extends AbstractExecutorService implement ExecutorService 提供4個構造器
構造器引數:
corePoolSize: 核心池大小
預設情況,建立執行緒池後,池中執行緒數為0,當有任務來時,建立一個執行緒去執行任務,當執行緒池中執行緒數目達到corePoolSize後,會把任務放入快取佇列、
maxPoolSize: 執行緒池最大執行緒數
keepAliveTime:表示執行緒沒有任務執行時最多保持多久會終止
預設情況下,只有當執行緒池中的執行緒數大於corePoolSize時,keepAliveTime才會起作用,直到執行緒池中的執行緒數不大於corePoolSize,即當執行緒池中的執行緒數大於corePoolSize時,如果一個執行緒空閒的時間達到keepAliveTime,則會終止,直到執行緒池中的執行緒數不超過corePoolSize
unit: 引數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
workQueue: 阻塞佇列,用來儲存等待執行的任務
可選的阻塞佇列:
ArrayBlockingQueue
LinkedBlockingQueue --- 預設,用的最多
SynchronousQueue
PriorityBlockingQueue
threadFactory: 執行緒工廠,主要用來建立執行緒
handler: 表示當拒絕處理任務時的策略,
4種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務
2.執行緒池原理:
1.執行緒池狀態:
ThreadPoolExecutor中定義了一個Volatile變數runState表示當前執行緒池的狀態,使用volatile來保證執行緒之間的可見性
執行緒池的4種狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
解析:
1.建立執行緒池後,初始時,執行緒池處於RUNNING狀態
2.如果呼叫了shutdown()方法,則執行緒池處於SHUTDOWN狀態,此時執行緒池不能接受新任務,但會等待所有任務執行完畢
3.如果呼叫了shutdownNow()方法,執行緒池處於STOP狀態,此時執行緒池不能接受新任務,並且會嘗試終止正在執行的任務
4.當執行緒池處於SHUTDOWN或STOP狀態,並且所有工作執行緒已經銷燬,任務快取佇列已經清空或執行結束後,執行緒池被設定為TERMINATED狀態
2.任務的執行:
執行緒任務儲存在BlockingQueue中,通過execute(Runnable )來呼叫執行,
21.Java的類載入器
類載入器 --- 一個用來載入類檔案的類
Java原始碼通過javac編譯成類檔案,然後JVM來執行類檔案中的位元組碼,類載入器負責載入檔案系統、網路或其他來源的類檔案
JVM中類載入器的樹狀層次結構
Bootstrap ClassLoader 引導類載入器, 載入Java的核心庫(jre/lib/rt.jar),用C++程式碼實現,不繼承子java.lang.ClassLoader
Extension ClassLoader 擴充套件類載入器, 載入Java的擴充套件庫(jre/ext/*.jar), Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類
System ClassLoader 系統類載入器, 根據Java應用的類路徑(classpath)來載入Java類,
Java應用的類都是由它來完成載入的,可通過ClassLoader.getSystemClassLoader()獲取系統類載入器
自定義類載入器 通過繼承java.lang.ClassLoader類,實現自己的類載入器
Java類載入器的3個機制:
委託機制:
將載入器的請求交給父載入器,如果父類載入器找不到或不能載入這個類,則當前類載入器再載入它
可見性機制:
子類的載入器可以看見所有父類載入器載入的類,而父類載入器看不到子類載入器載入的類
單一性機制:
類僅被載入一次, 由 委託機制 確保子類載入器不會再次載入父類載入器載入過的類
類載入過程 3個步驟:
裝載:
連結:(驗證、準備、解析)
初始化:
裝載:
查詢並載入類的二進位制資料
連結:
驗證:確保被載入類資訊符合JVM規範、沒有安全方面問題
準備:為類的靜態變數分配記憶體,並將其初始化為預設值
解析:把虛擬機器常量池中的符號引用轉換為直接引用
初始化:
為類的靜態變數賦予正確的初始值
說明:
1.JVM會為每個載入的類維護一個常量池
2.類的初始化步驟:
1.如果這個類沒被載入和連結,則先進行載入和連結
2.如果這個類存在父類,如果類未初始化,則先初始化其父類
3.如果類中存在static塊,一次執行這些初始化語句
java.lang.ClassLoader類
根據一個指定的類名稱,找到或生成其對於的位元組程式碼,然後從這些位元組碼中定義出一個Java類,即:java.lang.Class類的一個例項
ClassLoader中的方法:
getParent() 返回該類載入器的父類載入器
loadClass(name) 載入名稱為name的類,返回結果是java.lang.Class的例項
findClass(name) 查詢名稱為name的類,返回結果是java.lang.Class的例項
findLoadedClass(name) 查詢名稱為name的已被載入過的類,返回結果是java.lang.Class的例項
resolveClass(Class<?> c) 連結指定的Java類
Java中的類載入過程
載入(可自定義類載入器) 連線 ( 驗證 準備 解析 ) 初始化
載入:
獲取二進位制位元組流 --> 將位元組流靜態儲存結構轉換為方法區的執行時資料結構 --> 在堆中生成Class物件
連線:
驗證:
檔案格式驗證: 1.參照Class檔案格式規範驗證
2.此階段基於位元組流經過此驗證後,位元組流才會進入方法區,後面的驗證都依賴與方法區的驗證
後設資料驗證: Java語法驗證,eg:該類是否繼承了不該繼承的類
位元組碼驗證: 執行時安全性檢查
符號引用驗證: 確保類中引用的類,欄位,方法都是可訪問的
準備:
設定類變數初始值 --- static類變數 初始值 , 注意:final比較特別!!!
1.設定類變數 --- static變數
2.設定變數初始值 (注意:非程式碼中定義的值,8種基本資料型別都有初始值 eg: static int a = 10, 準備階段會把a初始值賦值為0,初始化時,再賦值為10 )
3.對於final的值,設為程式碼中的值(eg:final static int a = 10 , 準備階段直接把 a 賦值為10)
解析:
將符號引用轉換為直接引用
1.符號引用: 用符號來表示所引用的目標
2.直接引用: 一個指向記憶體中目標物件地址的控制程式碼
初始化:
1.根據程式碼實現初始化類變數及其他資源 (準備階段,static類變數還是初始值,這裡賦值為程式碼中指定的值)
2.執行子類初始化方法時,先執行父類的初始化方法(static變數賦值,static程式碼段執行,先父類後子類)
22.Java反射 增加 裝飾模式 的適用性
裝飾模式:在不必改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能,它是通過建立一個包裝物件來包裹真實的物件,比生產子類更加靈活,
使用Java的動態代理實現裝飾模式,會具有更強的靈活性和適用性
裝飾模式有什麼特點呢?
1、裝飾物件和真實物件有相同的介面。這樣呼叫者就能以和真實物件相同的方式和裝飾物件互動。
2、裝飾物件包含一個真實物件的引用(即上面例子中的Ability介面)。
3、裝飾物件接受所有來呼叫者的請求,並把這些請求轉發給真實的物件。
4、裝飾物件可以在呼叫者的方法以前或以後增加一些附加功能。這樣就確保了在執行時,不用修改給定物件的結構就可以在外部增加附加的功能。
什麼樣的地方使用裝飾模式呢?
1、需要動態擴充套件一個類的功能,或給一個類新增附加職責。
2、需要動態的給一個物件新增功能,這些功能可以再動態的撤銷。
3、需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
4、 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類。
23.JVM的執行時棧幀 --- JVM執行程式的過程!!! --- 方法的執行過程 !!!
1.每個方法的執行,在JVM中都是對應的棧幀在JVM棧中的入棧到出棧的過程!!!
2.每個在JVM中執行的程式,都是由許多的幀切換產生的結果
參考:
http://blog.csdn.net/column/details/14217.html
棧幀: --- 執行緒安全!!!每個執行緒的棧幀相互獨立 ---> 區域性變數在多執行緒環境下執行緒安全的原因!!!
存放方法的區域性變數表、運算元棧、動態連結,方法返回值和一些額外的附加資訊
當前棧:
一個方法的呼叫鏈可能很長,當呼叫一個方法時,可能會有很多方法處於執行狀態,但對於執行引擎,置於JVM棧頂的棧幀才是有效的,這個棧幀稱為 當前棧
當前棧所關聯的方法稱為當前方法,執行引擎的所有指令都是針對當前棧幀進行操作的
區域性變數表:
內容: 存放方法的區域性變數
eg:方法引數,方法內定義的區域性變數,物件引用,returnAddress型別
在Java程式被編譯為class檔案時,這個表的容量最大值已經確定
訪問:
虛擬機器利用索引編號的遞增來對區域性變數表中定義的變數進行一次訪問(從0開始),而對於例項方法(非static方法),其區域性變數表的第0個索引是this,
這是可以在例項方法中使用this.name ......的原因
動態連線:
參考:http://blog.csdn.net/eric_sunah/article/details/8014865
方法的呼叫過程:
在虛擬機器執行時,執行時常量池會儲存大量符號引用,這些符號引用可以看做每個方法的間接引用,
如果代表棧幀A的方法要呼叫代表棧幀B的方法,則這個虛擬機器的方法呼叫指令就會以B方法的符號引用作為引數,
但因為符號引用並不是直接指向代表B方法的記憶體位置,所有在呼叫之前還必須要將符號引用轉換為直接引用,然後通過直接引用訪問到真正的方法!
注意:
靜態解析:
如果符號引用在類載入階段或第一次使用時轉化為直接引用,則這種轉換成為靜態解析
動態連線:
如果在執行期間轉換為直接引用,這種轉換稱為動態連線
棧幀A 常量池 棧幀B
區域性變數表 區域性變數表
A方法的符號引用
運算元棧 運算元棧
B方法的符號引用
動態連線 動態連線
字串常量等
返回地址 返回地址
方法返回地址
1.正常退出:根據方法定義決定是否要返回值給上層呼叫者
2.異常退出:不會傳遞返回值給上層呼叫者
注意:
1. 不管那種方式結束,在退出當前方法時,都會跳轉到當前方法被呼叫的位置!!!
如果正常退出,則呼叫者的PC計數器的值可作為返回地址,
如果異常退出,則需要通過異常處理表來確定
2. 方法的一次呼叫對應著棧幀在虛擬機器中的一次入棧出棧操作!!!
方法退出時做的事情:
恢復上層方法的區域性變數表以及運算元棧,如果有返回值,就把返回值壓入到呼叫者棧幀的運算元棧中,
還會把PC計數器的值調整為方法呼叫入口的下一條指令
24.JVM的記憶體溢位分析和引數調優
1.JVM調優 http://blog.csdn.net/eric_sunah/article/details/7862114
1.JVM記憶體引數調優
-Xms 設定初始化堆的記憶體
-Xmx 設定堆最大使用記憶體
-Xss 設定每個執行緒的棧大小
-Xmn 設定年輕代大小
eg:
java -Xmx4096m -Xms4096m -Xmn2g -Xss128k
或
java -Xmx4g -Xms4g -Xmn2g -Xss128k
設定JVM堆最大可以記憶體為4096M,初始記憶體為4096M(-Xms設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配記憶體)
設定JVM年輕代大小為2G JVM堆記憶體 = 年輕代大小 + 年老代大小 + 持久代大小
持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統效能影響較大,推薦配置為整個堆的3/8
設定每個執行緒的棧大小為128k
JDK5+ 執行緒棧預設1M, 相同實體記憶體下,減小該值能生成更多執行緒,但作業系統對一個程式內的執行緒數有限制,最好不超過5000
如果方法遞迴太深,則可能耗盡執行緒棧,報出 StackOverflow !!! 執行緒棧記憶體溢位 <--- 方法呼叫太深
eg:設定堆記憶體中的記憶體分配比例
java -Xmx4g -Xms4g -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=64m -XX:MaxTenuringThreshold=0
-Xmn2g 設定年輕代大小為2G
-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所佔比值為1:4,年輕代佔整個堆的1/5
-XX:SurvivorRatio=4 設定年輕代中from和to區的比例,Eden區(from)與Survivor區(to)的大小比值為4:1:1,即 一個 Survivor 佔 年輕代的1/6
特別注意:
上面的比值 4 <=等價=> 1:4
-XX:MaxPermSize=64m 設定持久代大小為64M
-XX:MaxTenuringThreshold=0:設定垃圾最大年齡
小結:
1,整個堆包括年輕代,老年代和持久代。其中年輕代又包括一個Eden區和兩個Survivor區。
2,年輕代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
2,持久代:
-XX:PermSize
-XX:MaxPermSize
3,年輕代和老年代的比例:
-XX:NewRatio(年輕代和老年代的比值,年輕代多,除去持久代)
當設定了-XX:+UseConcMarkSweepGC後,會使-XX:NewRatio=4失效,此時需要使用-Xmn設定年輕代大小
4,Eden與Survivor的比例
-XX:SurvivorRatio(Eden區與兩個Survivor區的比值,Eden區多)
2.GC引數設定
並行收集器 --- 吞吐量優先,適合後臺處理
eg:
-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
解析:
-XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集
-XX:ParallelGCThreads=20:配置並行收集器的執行緒數,即:同時多少個執行緒一起進行垃圾回收。此值最好配置與處理器數目相等
-XX:+UseParallelOldGC:配置年老代垃圾收集方式為並行收集,JDK6.0支援對年老代並行收集
併發收集器 --- 響應時間優先,保證系統響應時間,減少垃圾收集時的停頓時間,適合應用伺服器和典型領域等
eg:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設定年老代為併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設定。
-XX:+UseParNewGC:設定年輕代為並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值。
3.常見配置彙總
堆設定
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設定年輕代大小
-XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:MaxPermSize=n:設定持久代大小
收集器設定
-XX:+UseSerialGC:設定序列收集器
-XX:+UseParallelGC:設定並行收集器
-XX:+UseParalledlOldGC:設定並行年老代收集器
-XX:+UseConcMarkSweepGC:設定併發收集器
垃圾回收統計資訊
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設定
-XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。
-XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間
-XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)
併發收集器設定
-XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。
4.調優總結
年輕代大小選擇
響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的物件。
吞吐量優先的應用:儘可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設定,一般要考慮併發會話率和會話持續時間等一些引數。
如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;
如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:
併發垃圾收集資訊
持久代併發收集次數
傳統GC資訊
花在年輕代和年老代回收上的時間比例
減少年輕代和年老代花費的時間,一般會提高應用的效率
吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期物件,
減少中期的物件,而年老代盡存放長期存活物件。
較小堆引起的碎片問題
因為年老代的併發收集器使用標記、清除演算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的物件。
但是,當堆空間較小時,執行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,
然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮。
-XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮
2.JVM記憶體溢位分析 --- OutOfMemoryError 不屬於Exception,繼承自Throwable
1.堆記憶體溢位 --- 不斷建立物件,並且不釋放,導致GC無法回收,堆記憶體移除 Java heap space
eg: 製造堆記憶體溢位的程式
1.降低修改虛擬機器堆記憶體大小 -Xms20m -Xmx20m
2.不斷建立強引用物件,方式GC回收
public static void main(String[] args) {
headOutOfMemory();
}
/*
* -verbose:gc -XX:+PrintGCDetails -verbose:gc
* -XX:+HeapDumpOnOutOfMemoryError
*
* -Xms20m -Xms20m
*
*/
static void headOutOfMemory() {
long count = 0;
try {
List<Object> objects = new ArrayList<Object>();
while (true) {
count++;
objects.add(new Object());
}
} catch (Throwable ex) {
System.out.println(count);
ex.printStackTrace();
}
}
}
異常資訊:
java.lang.OutOfMemoryError: Java heap space
2.棧記憶體溢位 --- 棧主要存放棧幀(區域性變數表(基本資料型別,物件引用,returnAddress型別),運算元棧,動態連結,方法出口資訊),
1.StackOverflowError ---- 當執行緒棧的空間大於虛擬機器所允許時,丟擲 StackOverflowError
執行緒棧,因遞迴或方法呼叫太深,導致超過執行緒棧設定時,拋 StackOverflowError
eg:
自定義執行緒棧溢位
1.降低執行緒棧大小 -Xss4k
2.方法迴圈遞迴
public class JVMStackSOF {
/**
* (1) 在hotspot虛擬機器中不區分虛擬機器棧(-Xss)和本地方法棧(-Xoss),且只有對Xss引數的設定,才對棧的分配有影響
*
* (2)
* 由於StackOverflowError->VirtualMachineError->Error
* ->Throwable,所以catch的時候如果用Exception的話將捕獲不到異常 Stack length 會隨著-Xss的減少而相應的變小
*/
private int stackNumber1 = 1;
public void stackLeck1() {
stackNumber1++;
stackLeck1();
}
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeck1();
} catch (Throwable ex) {
System.out.println("Stack length:" + jvmStackSOF.stackNumber1);
ex.printStackTrace();
}
}
}
異常資訊:
java.lang.StackOverflowError
2.OutOfMemoryError ---- 棧空間不足,丟擲OutOfMemoryError
JVM棧,整體記憶體不足時,拋OutOfMemoryError
eg:
自定義JVM棧溢位
1.JVM中除了堆和方法區,剩餘的記憶體基本都由棧佔用
2.每個執行緒都有獨立的棧空間(堆、方法區是執行緒公用)
3.如果-Xss調大每個執行緒的棧空間,可建立的執行緒數量必然減少
public class JVMStackOOM {
/**
* (1)不停的建立執行緒,因為OS提供給每個程式的記憶體是有限的,且虛擬機器棧+本地方法棧=(總記憶體-最大堆容量(X模型)-最大方法區容量(
* MaxPermSize)),於是可以推斷出,當每個執行緒的棧越大時,那麼可以分配的執行緒數量的就越少,當沒有足夠的記憶體來分配執行緒所需要的棧空間時,
* 就會丟擲OutOfMemoryException
* (2)由於在window平臺的虛擬機器中,java的執行緒是隱射到作業系統的核心執行緒上的,所以執行一下程式碼時,會導致作業系統假死(我就嚐到了血的代價)
*/
private static volatile int threadNumber = 0;
public void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
threadNumber++;
while (true) {
System.out.println(Thread.currentThread());
}
}
}.start();
}
}
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
try {
jvmStackOOM.stackLeakByThread();
} catch (Throwable ex) {
System.out.println(JVMStackOOM.threadNumber);
ex.printStackTrace();
}
}
}
異常資訊如下:
java.lang.OutOfMemoryError:unable to create new native thread
3.方法區溢位
方法區:存放JVM載入的類資訊,常量,執行時常量池,靜態變數,編譯期編譯後的程式碼等
異常資訊:
java.lang.OutOfMemoryError: PermGen space
eg:
自定義方法區溢位程式碼
1.通過不斷產生類資訊來佔用方法區記憶體
2.調整 -XX:PermSize=10m --XX:MaxPermSize=10m,來降低方法區記憶體大小
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 利用CGLib技術不斷的生成動態Class,這些Class的資訊會被存放在方法區中,如果方法區不是很大會造成方法去的OOM
*
*
* -XX:PermSize=10m -XX:MaxPermSize=10m
* */
public class MethodAreaOOM {
static class Test {}
public static void main(String[] args) {
try{
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(arg0, arg2);
}
});
System.out.println(enhancer.create());
}
}catch(Throwable th){
th.printStackTrace();
}
}
}
異常資訊:java.lang.OutOfMemoryError: PermGen space
25.JVM記憶體分配策略與回收
1.分配策略
1.物件優先在Eden區上分配
2.大物件直接分配在老年區
-XX:PretenureSizeThreshold 引數設定直接放入老年區的物件大小
3.長期存活的物件直接進入老年區
-XX:MaxTenuringThreshold 引數設定物件年齡,經歷幾次gc可以進入老年區
JVM為每個物件定義了年齡計數器,
如果物件在Eden出生並經過第一次Minor GC後任然存活,並能被Survivor容納,將被移到Survivor空間中,
並將物件年齡設為1,物件在Survivor區每熬過一次Minor GC,年齡+1,
當年齡達到一定程度(預設15,可引數-XX:MaxTenuringThreshold設定)時,進入到老年代中
2.記憶體回收
1.Minor GC 發生在年輕代的GC,當JVM無法為一個新物件分配空間時,觸發Minor GC,清理年輕代記憶體,大多數物件生命週期短,所以Minor GC 非常頻繁,而且速度較快
觸發條件:
Eden區滿時,觸發Minor GC
2.Full GC 發生在年老代的GC
觸發條件:
1.呼叫System.gc(),系統建議執行Full GC,但不一定執行
2.老年代空間不足
3.方法區空間不足
4.通過Minor GC後進入老年代的物件平均大小,大於老年代可用記憶體空間
5.由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小
26.AOP應用和實現
AOP優化改造:http://blog.csdn.net/xvshu/article/details/46288953
1.關於Aspectj
獲取目標方法資訊 --- JoinPoint Spring只支援方法執行的JoinPoint
JoinPoint裡包含了如下幾個常用的方法:
Object[] getArgs:返回目標方法的引數
Signature getSignature:返回目標方法的簽名
Object getTarget:返回被織入增強處理的目標物件
Object getThis:返回AOP框架為目標物件生成的代理物件
注意:當使用@Around處理時,我們需要將第一個引數定義為ProceedingJoinPoint型別,該類是JoinPoint的子類
2.5種增強處理
Before、 在某個連線點JoinPoint之前執行, 不能阻止連線點前的執行
Around、 包圍一個連線點的通知,可以在方法呼叫前後完成自定義的行為
AfterReturning、 在某連線點正常完成後執行的通知,不包括丟擲異常的情況
After、 某連線點退出時執行(不同正常返回還是異常退出,都會執行的通知,類似finally功能,可以用於釋放連線和資源)
AfterThrowing 在方法丟擲異常退出時執行的通知
--- 5種增強處理中,織入增強處理的目標方法、目標方法的引數和被織入增強處理的目標物件等
任何一種織入的增強處理中,都可以獲取目標方法的資訊
切點表示式:
1.execution()表示式
eg:
execution (* com.sample.service.impl..*. *(..))
1.execution() 表示式主體
2.第一個* : 表示返回型別, * 表示所有型別
3.包名: 要攔截的包,後面2個句點表示當前包和當前包的所有子包,即:com.sample.service.impl包、及其子孫包下所有類的方法
4.第二個* : 表示類名, * 表示所有類
5.第三個* : 表示方法名, * 表示所有方法, *(..) 中的2個句點表示方法引數, 2個句點表示任何引數
2.自定義註解
1.定義切點方法
//自定義註解方式
@Pointcut("annotation(com.jay.annotation.MyAnnotation)")
//@Pointcut("execution (* com.gcx.controller..*.*(..))") --- execution()切點表示式方式
public void controllerAspect(){}
2.在增強方法上,使用切點方法
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint){
}
eg:
使用5種增強處理
1.類上新增 @Aspect註解
2.方法上新增 4中增強處理註解
3.特別注意:
@Around中引數是 ProceedJoinPoint, 其他4類增強用 JoinPoint
@Aspect
public class AdviceTest {
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模擬許可權檢查...");
System.out.println("@Before:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:引數為:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被織入的目標物件為:" + point.getTarget());
}
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:執行目標方法之前...");
//訪問目標方法的引數:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改變後的引數1";
}
//用改變後的引數執行目標方法
Object returnValue = point.proceed(args);
System.out.println("@Around:執行目標方法之後...");
System.out.println("@Around:被織入的目標物件為:" + point.getTarget());
return "原返回值:" + returnValue + ",這是返回結果的字尾";
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模擬日誌記錄功能...");
System.out.println("@AfterReturning:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:引數為:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值為:" + returnValue);
System.out.println("@AfterReturning:被織入的目標物件為:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模擬釋放資源...");
System.out.println("@After:目標方法為:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:引數為:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被織入的目標物件為:" + point.getTarget());
}
//標註該方法體為異常通知,當目標方法出現異常時,執行該方法體
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")
public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){
...
}
}
3.動態代理實現
1.靜態代理 --- 代理模式
兩個類實現同一個介面,在代理類的介面方法中,呼叫被代理物件的介面方法,同時在代理類的介面方法中,呼叫被代理類方法前後新增邏輯
2.動態代理 --- JDK動態代理 和 cglib 動態代理(位元組碼增強技術,效率高)
參考:http://blog.csdn.net/zpf336/article/details/52086180
http://blog.csdn.net/wenbo20182/article/details/52021096
2種動態代理區別:
1.JDK動態代理要求被代理類要實現介面,而cglib不需要
2.cglib能根據記憶體中為其建立子類(代理物件)
1.JDK的動態代理 --- 通過建立一個實現InvocationHandler介面的中間物件,實現動態代理
優點:不必要求代理者和被代理者實現相同介面
缺點:僅支援介面代理, JDK動態代理需要實現類通過介面定義業務方法,對於沒有介面的類,無法代理
eg:
自定義的基於JDK的動態代理類
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用JDK動態代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy()
greeting.sayHello("Jack");
2.cglib動態代理 --- 位元組碼增強技術,效率高
原理:
通過位元組碼技術,為一個類建立子類,並在子類中採用方法攔截的技術,攔截所有父類方法的呼叫,順勢織入橫切邏輯
優點:位元組碼增強技術,CGLib建立的動態代理物件效能比JDK建立的動態代理物件的效能高不少,
但是CGLib在建立代理物件時所花費的時間卻比JDK多得多,所以對於單例的物件,因為無需頻繁建立物件,用CGLib合適,
反之,使用JDK方式要更為合適一些
缺點:CGLib由於是採用動態建立子類的方法,對於final方法,無法進行代理
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用cglib動態代理
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
27.關於JVM架構
參考:http://www.ityouknow.com/java/2017/03/01/jvm-overview.html
1.Java記憶體模型
記憶體模型:
描述程式中各個變數(例項域、靜態域、陣列元素)之間的關係,以及在計算機系統中將變數儲存到記憶體,從記憶體取出變數這樣的底層細節
記憶體模型規則:
原子性:
約定了:訪問儲存單元內任何型別欄位的值以及對其進行更新操作時,必須保證其是原子的
即:獲得或初始化某一些值時(),該值的原子性在JVM內部是必須得到保證的
可見性:
一個執行緒修改的狀態,對另一個執行緒是可見的,
即:一個執行緒修改的結果,另一個執行緒馬上就能看到
eg:
volatile修飾的變數具有可見性,不允許執行緒內部快取和重排序,但不保證原子性
可見性規則約束下,定義了一個執行緒在哪種情況下可以訪問或影響另外一個執行緒,以及從另外一個執行緒的可見區域讀取相關資料、將資料寫入到另外一個執行緒內
可排序性:
為了提高效能,編譯器和處理器可能會對指令做重排序
volatile修飾的變數不允許執行緒內部快取和重排序
Java記憶體模型: --- JMM Java Memory Model
JMM: 是控制Java執行緒之間、執行緒和主存直接通向的協議
JMM定義了執行緒和主存之間的抽象關係:
執行緒之間的共享變數儲存在主存(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體儲存了該執行緒以讀/寫共享變數的副本
2.JVM例項內部結構
子系統:
類載入器作為JVM的子系統,針對Class檔案進行檢測來載入對應的類
執行引擎:
負責程式碼的解釋和執行
記憶體區域:
儲存位元組碼、類資訊、物件、引數、變數等
方法區: --- 執行緒共享
儲存類的裝載資訊(類資訊),
靜態變數(類變數),
執行時常量池
堆: --- 執行緒共享
物件、陣列記憶體對分配:
當一個Java程式建立一個物件或者一個陣列時,JVM例項會針對該物件和陣列分配一個新的記憶體堆空間。在JVM例項內部,只存在一個記憶體堆的例項,所有的依賴該JVM的Java程式都共享該例項
程式記憶體堆分配:
多個Java程式啟動時,會得到JVM分配給自己的對空間,多個Java程式的堆空間時相互獨立的
JVM棧: --- 執行緒私有
對於執行緒記憶體棧分配:
當一個新執行緒啟動時,JVM為其建立獨立記憶體棧,
記憶體棧由棧幀構成,棧幀有2中操作:出棧和入棧
當前執行緒方法 --- 正在被執行緒執行的方法, 該方法的棧幀稱為 當前幀
對於方法:
當一個執行緒呼叫某個方法時,JVM建立並將一個新幀壓入到記憶體棧中,這個幀稱為當前棧幀,
當該方法執行時,JVM使用記憶體棧來儲存 引數引用、區域性引用變數、基本型別數值、返回值等相關資料
無論方法正常結束還是異常結束,JVM都彈出或丟棄該棧幀,上一幀方法成為當前幀
本地方法棧:
儲存了本地Java方法呼叫狀態,其狀態包括區域性變數、被呼叫的引數、它的返回值、以及中間計算結果
程式計數器: --- 執行緒私有
每個執行緒都有自己的PC暫存器,通過計數器來指示下一條指令執行
3.JVM記憶體分配策略
靜態儲存:
編譯時,能確定每個資料在執行時需要的儲存空間,因而在編譯時就給它們分配固定的記憶體空間
此分配策略,要求程式碼中不允許有可變資料結構存在,也不允許巢狀或遞迴結構出現(無法計算需要記憶體空間)
eg:
static final 全域性常量
棧式儲存:
動態儲存分配,由一個類似堆疊的執行棧來實現,按先進後出原則分配
程式對資料所需記憶體空間未知,只有到執行時才能知道佔用空間
堆式儲存:
專門負責在編譯時或執行時,無法確定儲存要求的資料結構的記憶體分配
eg:
可變字串和物件例項
4.物件分配規則:
1.物件優先分配在年輕代的Eden區,如果Eden區沒有足夠空間,JVM執行一次 Minor GC
2.大物件直接進入老年代(大物件:需要大量連續記憶體空間的物件, 可引數設定大物件大小),目的:避免在Eden區和2個Survivor區直接進行大量的記憶體拷貝 <--- 新生代採用複製演算法收集記憶體
3.長期存活的物件進入老年代, JVM為每個物件定義了一個年齡計數器,如果物件經過1次Minor GC,物件進入Survivor區,之後沒經過一次Minor GC,物件年齡+1, 直到達到閾值,才進入老年代 (可引數設定年齡計數器大小)
4.動態判斷物件年齡。 如果Survivor區中相同年齡的所有物件大小總和,大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入老年代
5.空間分配擔保。 每次進行Minor GC時, JVM會計算Survivor區移至老年代的物件的平均大小,如果這個值大於老年區剩餘值,則進行一個Full GC
5.GC演算法 垃圾回收
物件存活判斷:
引用計數 --- 每個物件有應用計數屬性,新增一個引用,計數+1, 應用釋放,計數-1,計數為0,可回收 ---- 無法解決物件迴圈引用問題!
可達性演算法(根搜尋演算法) --- 從GC Roots開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到 GC Roots沒有任何引用鏈可達時, 證明此物件不再用,可回收
可作為GC Roots的物件:
虛擬機器棧中引用的物件
方法區中的靜態屬性引用的物件
方法區中常量引用的物件
本地方法棧中JNI引用的物件
特別注意:
GC管理的主要是 Java 堆,一般情況下只針對堆,進行垃圾回收,
方法區、棧、本地方法區不被GC鎖管理,因而選擇這些區域內的物件作為GC Roots,
被GC Roots引用的物件不被GC回收
GC演算法: 3種基礎演算法
參考:http://blog.csdn.net/java2000_wl/article/details/8022293
標記-清除演算法
分"標記" 和 "清除" 兩個階段
首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件
缺點:
標記和清除過程效率低
標記清除後,產生大量不連續的記憶體碎片
複製演算法
將可用記憶體按容量分為相等的2塊,每次只使用其中的一塊,
當這塊記憶體用完了,就將還存活的物件複製到另外一塊上面,然後把已使用過的記憶體空間一次清理掉
缺點:
記憶體縮小為原來的一半
優點:
不會有記憶體碎片,
只需要移動堆的記憶體指標,按順序分配記憶體即可,實現簡單,執行高效
標記-壓縮(整理)演算法
標記與第一種演算法一樣類似,但後續操作不是直接清理物件,而是讓所有存活物件都向一端移動,
並更新引用其物件的指標,然後直接清理掉端邊界意外的記憶體
缺點:
在標記-清除的基礎上,需要進行物件的移動,成本相對較高
優點:
不會產生記憶體碎片
JVM分代收集演算法
JVM把堆記憶體分為新生代和老年代, 根據各個代的特點,採用合適的收集演算法
新生代 --- 複製演算法 !!!
每次垃圾收集都有大批物件死去,只有少量存活,適合複製演算法,只需付出少量存活物件的複製成本就可完成收集
老年代 --- 標記-整理演算法 !!!
老年代中物件存活率高,沒有額外空間對它進行分配擔保,必須使用 "標記-壓縮" 演算法進行回收
垃圾回收器:
Serial收集器 --- 序列收集器,最古老,最穩定,效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收
ParNew收集器 --- Serial收集器的多執行緒版本
Parallel收集器 --- 類似ParNew,更關注系統的吞吐量
Parallel Old收集器 --- 使用多執行緒 和 標記-整理 演算法
CMS收集器 --- 是一種以獲取最短回收停頓時間為目標的收集器
G1收集器 --- 面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵
28.雜湊表 和 HashMap原理
雜湊表和雜湊演算法原理:
http://blog.csdn.net/duan19920101/article/details/51579136
http://blog.csdn.net/tanggao1314/article/details/51457585
一種根據關鍵字直接訪問記憶體儲存位置的資料結構
雜湊函式: 通過雜湊表,資料元素的存放位置和資料元素關鍵字之間建立起某種對應關係, 關鍵字到記憶體的對映
hash表以及HashMap原理:
http://www.code123.cc/258.html
HashMap原理:
HashMap基於雜湊函式,使用put(key,value)儲存物件,使用get(key)獲取物件,
put傳遞鍵值對,先對鍵呼叫hashCode()方法,返回hashCode()用於找到bucket位置來儲存Entry物件,
HashMap是在bucket中儲存鍵物件和值物件,作為 Map.Entry
bucket儲存LinkedList,而LinkedList中儲存的是key-value物件 --> Map.Entry
HashMap在Map.Entry靜態內部類中實現儲存key-value對,
HashMap使用雜湊演算法,在put和get方法中,使用hashCode()和equals()方法,
通過傳遞key-value對,呼叫put方法時,HashMap呼叫鍵物件(key)的hashCode()計算hashCode,然後bucket位置來儲存物件(根據key查詢value存放地址),
如果多個key的hashCode相同,則是發生碰撞,物件將儲存在同一個bucket位置的LinkedList的下一個節點中(鍵值對物件 <==> Map.Entry,Entry儲存在LinkedList中)
兩個物件的hashCode相同會發生什麼?
1.兩個物件的hashCode相同,但它們可能並不相等 hashCode() 和 equals()
2.HashMap的key-value抽象成Map.Entry, Entry儲存在LinkedList中, 每個bucket位置對應一個LinkedList,用於解決雜湊碰撞
3.如果hashCode相同,則它們的bucket位置相同, 儲存時會發生碰撞,
如果傳送碰撞,後一個新增的物件,會存放在bucket位置對應的LinkedList的下個節點中
如果兩個鍵的hashCode相同,如何獲取值物件?
1.get()根據key的hashCode去查詢bucket位置,然後獲取對應的value物件
2.如果兩個key的hashCode相同,則兩個key對應的Entry都儲存在同一個bucket位置的LinkedList中
3.HashMap的LinkedList中儲存的是鍵值對Map.Entry物件
4.根據key的hashCode找到bucket位置後,呼叫key.equals()方法找到LinkedList中正確的節點,最終找到要查的value值物件
5.注意:
hashCode()用於直接定位bucket位置
equals() 用於獲取值物件時使用
如果HashMap大小超過了負載因子定義的容量,怎麼辦?
1.預設負載因子是0.75,即:一個map填滿75%的bucket時,將會建立原來HashMap大小的2倍的bucket陣列,來重新調整map的大小,
並將原來的物件放入新的的bucket陣列中 --- 這個過程稱為 rehashing --- 因為它呼叫hash方法找到新的bucket位置
重新調整HashMap大小時,存在什麼問題?
多執行緒下,可能產生條件競爭
eg:
如果2個執行緒都發現HashMap需要重新調整大小,它們會同時嘗試調整大小。
在調整大小過程中,儲存在LinkedList中的元素的次序將會反過來。
因為移動到新的bucket位置時,HashMap並不會將元素放到LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷,
如果條件競爭發生了,就死迴圈了
多執行緒環境下,使用HashTable或ConcurrentHashMap替代HashMap
注意:
1.String、Integer適合作為HashMap的key
原因:
String是不可變的,final的,已經重寫了equals()和hashCode()方法
如果兩個不相等的物件返回不同的hashCode,碰撞的機率就會小,進而提高HashMap的效能
2.只要遵循equals()和hashCode()的定義規則,並且當物件插入到Map中之後不會再改變了,可以自定義物件作為key,
3.使用ConcurrentHashMap替代HashTable
HashTable是同步的,執行緒安全的,但ConcurrentHashMap同步效能更好,其採用了分段鎖
4.HashTable和ConcurrentHashMap的區別
1.HashTable每次同步執行是,都要鎖住整個結構
2.ConcurrentHashMap將hash表分為16個bucket桶(預設值),採用分段鎖,get、put、remove等操作只鎖當前需要用到的桶
eg:
原先只能一個執行緒進入,現在卻能同時16個寫執行緒進入(寫執行緒才需要鎖定,讀執行緒不受限制),提升了併發性
總結:
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取物件。當我們將鍵值對傳遞給put()方法時,它呼叫鍵物件的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值物件。當獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。HashMap使用連結串列來解決碰撞問題,當發生碰撞了,物件將會儲存在連結串列的下一個節點中。 HashMap在每個連結串列節點中儲存鍵值對物件。
當兩個不同的鍵物件的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的連結串列中。鍵物件的equals()方法用來找到鍵值對
HashTable實現原理:
實現原理與HashMap類似,但為了執行緒安全,HashTable中幾乎所有的public方法都用synchronized做了同步處理,有些方法也是在內部通過 synchronized 程式碼塊來實現
Hashtable 與 HashMap 的簡單比較
HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵對映到相應值的類的抽象父類,而 AbstractMap 是基於 Map 介面的實現,它以最大限度地減少實現此介面所需的工作。
HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時候,呼叫 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap則不是。我們可以看一下原始碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 程式碼塊來實現。
所以有人一般都建議如果是涉及到多執行緒同步時採用 HashTable,沒有涉及就採用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法建立了一個執行緒安全的 Map 物件,並把它作為一個封裝的物件來返回。
不考慮效能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap)來替換HashMap,這兩種方式基本都是對整個 hash 表結構做鎖定操作的
ConcurrentHashMap實現原理: --- 依賴於Java記憶體模型
參考:http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
ConcurrentHashMap結果中包含Segment的陣列,預設併發基本,建立包含16個Segment物件的陣列,
每個Segment又包含若干個雜湊表的桶,每個桶是由HashEntry連線起來的一個連結串列,
如果key能夠均勻雜湊,每個Segment大約守護整個雜湊表桶總數的1/16
併發讀些操作:
執行 put 方法的時候,會需要加鎖來完成,但加鎖操作是針對的hash值對應的Segment,而不是整個ConcurrentHashMap,因為put操作只是在某個Segment中完成,並不需要對整個ConcurrentHashMap加鎖,
此時,其他執行緒可以對另外的Segment進行put操作,雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖
同時,讀執行緒並不會因為本執行緒的加鎖而阻塞
在理想狀態下,ConcurrentHashMap 可以支援 16 個執行緒執行併發寫操作(如果併發級別設定為 16),及任意數量執行緒的讀操作
總結:
雜湊表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 物件的不變性和用 volatile 型變數協調執行緒間的記憶體可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發效能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個併發雜湊對映表的實現,它允許完全併發的讀取,並且支援給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。
在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全域性的鎖來同步不同執行緒間的併發訪問。同一時間點,只能有一個執行緒持有鎖,也就是說在同一時間點,只能有一個執行緒能訪問容器。這雖然保證多執行緒間的安全併發訪問,但同時也導致對容器的訪問變成序列化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
用分離鎖實現多個執行緒間的更深層次的共享訪問。
用 HashEntery 物件的不變性來降低執行讀操作的執行緒在遍歷連結串列期間對加鎖的需求。
通過對同一個 Volatile 變數的寫 / 讀訪問,協調不同執行緒間讀 / 寫操作的記憶體可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 物件的不變性及對同一個 Volatile 變數的讀 / 寫來協調記憶體可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於雜湊對映表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。
通過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高
29.關於Comparable和Comparator介面
1.Comparable和Comparator介面被用來對物件集合或者陣列進行排序
2.內部排序,物件類實現Comparable介面,重寫 CompareTo()方法
2.外部集合排序,呼叫Collections.sort(collection, Comparator<T>)對集合元素排序
eg:
Collections.sort(list,new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
if (user1.getName().equals(user2.getName())) {
return user1.getAge() - user2.getAge();
} else {
return user1.getName().compareTo(user2.getName());
}
}
});
30.OAuth認證流程
參考:http://www.code123.cc/1671.html
31.Java NIO
參考:http://blog.csdn.net/hxpjava1/article/details/56282385
概念:
專門為提高I/O吞吐量而設計,NIO通過Reactor模式的事件驅動機制來達到 Non Blocking
Reactor -- 反應器
將事件註冊到Reactor中,當有相應的事件發生時,Reactor告訴我們哪些事情發生了,我們根據具體的事件去做相應的處理
通道和緩衝區: 標準IO基於位元組流和字元流進行操作,而NIO基於通道Channel和緩衝區Buffer進行操作,資料總是從通道讀取到緩衝區,或從緩衝區寫入到通道中
非同步IO:NIO可以非同步的使用IO,當執行緒從通道讀取資料到緩衝區時,執行緒還可進行其他事情,當資料被寫入到緩衝區時,執行緒可以繼續處理它。從緩衝區寫入通道也類似
Selectors選擇器:選擇器用於監聽多個通道的事件(eg:連線開啟,資料到達等),因此,單執行緒可以監聽多個資料通道
eg:
Selector執行單執行緒處理多個Channel,如果應用開啟了多個連線(通道),但每個連線流量都很低,使用Selector就會很方便, 如:在一個聊天伺服器中
優點:
舊IO對檔案操作只能一個位元組一個位元組或一行一行的讀,對Socket IO會阻塞,可以為每個Socket建立一個Thread,但開銷太大
NIO 對Socket IO可以實現非阻塞,可用單執行緒管理多個通道,
NIO使用緩衝區,File IO和Socket IO都是和Buffer緩衝區互動讀取
NIO將通道資料讀到緩衝區中再進行操作,避免逐位元組或逐行讀取的效能開銷
從Channel讀取到Buffer -->
Channel Buffer
<--從Buffer寫入到Channel
NIO和IO如何影響程式的設計?
對NIO和IO類的API呼叫
資料處理
用來處理資料的執行緒數
使用場景:
1.聊天伺服器 需要管理同時開啟的成千上萬個連線,但這些連線每次只傳送少量的資料
2.P2P網路中, 需要維持許多開啟的連線到其他計算機上,使用一個單獨的執行緒來管理所有出戰連線
3.其他流量小, 連線多的場景
不適合場景:
1.少量連線佔用高頻寬,一次傳送大量資料 --- 檔案伺服器(適合IO實現,一個連線通過一個執行緒處理)
NIO核心模組:
Selector(選擇器):
1.Selector允許單執行緒處理多個Channel pk 舊IO的多執行緒處理, 效能更高
一個單執行緒選擇器可監控多個通道的多個事件(eg:連線開啟,資料到達等事件)
2.使用Selector,需要向它註冊一個Channel,然後輪詢呼叫它的select()方法,該方法將阻塞,
當註冊的某個通道準備好要進行IO操作時,返回已選擇鍵的個數,
此時通過selectedKeys獲得已選擇的鍵,就可進行相關的IO操作,
選擇鍵SelectionKey 是用來連線 Selector 和 Channel
直到這裡註冊中的Channels中有一個準備好的事件,
一旦這個方法返回,這個執行緒將會執行這些事件
事件的例項是進來的連線,接收到的資料等等
3. 要使用Selector,得向Selector註冊Channel,然後呼叫它的select()方法,
該方法會一直阻塞到某個註冊的通道有事件就緒,一旦這個方法返回,執行緒就可以處理這些事件(新連線進來,資料接收等)
---> Channel
Thread ---> Selector ---> Channel
---> Channel
4.NIO的選擇器允許一個單獨的執行緒來監視多個輸入通道,可以註冊多個通道使用一個選擇器,然後使用一個單獨的執行緒來 "選擇" 通道,
這些通道里已經有可以處理的輸入,或選擇已準備寫入的通道, 這種機制,使得一個單獨執行緒很容易來管理多個通道
Channel(通道):
1.4種Channel
FileChannel 檔案IO
ServerSocketChannel TCP IO
SocketChannel TCP IO
DatagramChannel UDP IO
Buffer(緩衝區):
緩衝區 --- 記憶體中預留指定位元組數的記憶體空間,用來對輸入、輸出的資料作臨時儲存, IO操作中資料的中轉站
緩衝區直接為通道Channel服務,寫入資料到通道,或從通道讀取資料
8種緩衝區類:
對應boolean之外的7基本資料型別 + MappedByteBuffer(專門用於記憶體對映的ByteBuffer)
ByteBuffer <--繼承 -- MappedByteBuffer 用於表示記憶體對映檔案
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer讀寫資料的4個步驟:
寫入資料到Buffer
呼叫flip()方法
從Buffer中讀取資料
呼叫clear()或 compact()方法
說明:
1.當向Buffer寫入資料時,buffer會記錄下寫了多少資料,
一旦要讀取資料,呼叫flip()方法,將Buffer從寫模式切換到讀模式,在讀模式下,可以讀取之前寫入到Buffer的所有資料
2.一旦讀完所有資料,就要清空緩衝區,讓它可再次被寫入,2種清空緩衝區方式:
clear() --- 清空整個緩衝區
compact() --- 只清除已讀過的資料,任何未讀的資料都將被移到緩衝區的起始處,新寫入的資料,將放到緩衝區未讀資料的後面
緩衝區的 4 個屬性: capacity>=limit>=position>=mark>=0
capacity
可容納的最大數量,緩衝區建立時被設定,不能改變
limit
上界,緩衝區中當前資料量
讀模式下:
表示最多能讀到多少資料,當切換Buffer到讀模式時,limit被設定為寫模式下的position值,
即:能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)
寫模式下:
表示最多能往Buffer裡寫多少資料,limit等於Buffer的capacity
position
位置,下一個要被讀、寫的元素的索引
寫資料到Buffer時,position表示當前的位置,初始位置為0,當資料寫入到Buffer後,position向前移動到下一個可插入資料的Buffer單元
讀資料時,同某個特定位置讀,當將Buffer從寫模式切換到讀模式,position會被重置為0,當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置
初始:為 0,最大:capacity-1
mark
標記,呼叫mark()來設定mark=position,再呼叫reset()可以讓position回覆到標記的位置,即:position=mark
初始:為 -1
建立緩衝區
緩衝區類都是抽象的,不能new方式例項化,每個緩衝區類都有一個靜態工廠方法,用於建立相應緩衝區物件
格式:
XxBuffer buf = XxBuffer.allocate(capacity)
eg:
//建立一個容量為10的byte緩衝區和char緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(10);
CharBuffer buf2 = CharBuffer.allocate(10);
如果想用一個指定大小的陣列,作為緩衝區的資料儲存器,可用wrap()方法建立緩衝區
eg:
//使用一個指定陣列,作為緩衝區儲存器
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes);
解析:
1.緩衝區資料會存在bytes陣列中,bytes陣列或buf緩衝區任何一方中資料的改動都會影響另一方
2.還可建立指定初始位置(position)和上界(limit)的緩衝區
//使用指定陣列作為緩衝區儲存器,並建立一個position=3,limit=8,capacity=10的緩衝區
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes, 3, 8);
操作緩衝區
1.存取
get() 從緩衝區取資料
put(xx) 向緩衝區存資料
channel.read(buf) 從Channel寫到Buffer
channel.write(buf) 從Buffer讀取資料到Channel
Buffer.rewind() 將position設回0, 可以重讀Buffer中的所有資料,limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
eg:
ByteBuffer buf = ByteBuffer.allocate(10);
//存3個資料
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'D');
//反轉緩衝區,即從寫模式切換到讀模式,從頭開始讀,最多讀取已寫入個數的資料
buf.flip();
//讀取2次資料
System.out.println((char)buf.get());
System.out.println((char)buf.get());
//返回當前位置到上界的資料元素數量
System.out.println(buf.remaining());
//從當前位置到上界是否還有資料元素
System.out.println(buf.hasRemaining());
解析:
1.呼叫put()或get()時,每呼叫一次,position值 +1,指示下次存或取開始的位置
2.buf.flip() 從寫模式切換到讀模式,可以從頭開始讀取最多已存入個數的資料
3.Buffer.remaining():返回從當前位置到上界的資料元素數量;
Buffer.hasRemaining():告訴我們從當前位置到上界是否有資料元素;
2.反轉 flip()
將一個處於存資料狀態的緩衝區變為一個處於準備讀取資料的狀態
反轉緩衝區,
即:將緩衝位元組陣列的指標設定為陣列的開始序列,即:陣列下標0,這樣才能從buffer開頭,對buffer進行遍歷(讀取)
即:呼叫flip()後,讀寫指標知道緩衝區頭部,並且設定了最多隻能讀取已已寫入的資料長度
如果不呼叫flip(),就會從檔案最好開始讀取
特別注意:
buffer.flip() 作用:將Buffer從寫模式切換到讀模式,呼叫flip()方法設定這個position的值為0,以及設定這個limit的值為剛才position的值,
換句話說,position現在標記了讀的位置,limit標記了有多少個Buffer的位元組,字元等等被寫入到buffer。限制有多少個位元組,字元可以去讀取的。
flip原始碼:
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;
}
3.清空資料
1.清空緩衝區內所有資料 --- clear()
2.清空已讀取的資料 --- compact()
在buffer中仍然有未讀取的資料,並且你想稍後讀取,呼叫compact(),
compact()方法拷貝所有未讀取的資料到buffer的開頭,然後設定position值為最後未讀取資料元素的後面,
再寫資料時,從已有資料後面繼續寫
4.標記 --- mark()
記住當前位置,之後可以將位置恢復到標記處(使用reset()方法恢復)
通過呼叫Buffer.mark(),可以標記Buffer中的一個特定的position,
之後可通過呼叫Buffer.reset() 恢復到這個position
eg:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
5.比較2個緩衝區是否相等
6.批量移動緩衝區的資料
public static void batchMove(){
byte[] bytes = "hello nio".getBytes();
/**這裡,可以直接把資料交給陣列來儲存
ByteBuffer buf = ByteBuffer.wrap(bytes);
*/
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
//將byte資料寫入緩衝區 <=等價=> buf.put(bytes);
buf.put(bytes, 0, bytes.length);
//反轉緩衝區,變為讀模式
buf.flip();
//輪詢判斷是否有資料,有則將緩衝區資料批量讀到array中
byte[] array = new byte[bytes.length];
while (buf.hasRemaining()){
buf.get(array, 0, buf.remaining());
}
//輸出緩衝區讀出的資料
System.out.println(new String(array));
}
7.複製緩衝區
通道之間的資料傳輸:
NIO中,如果2個通道中有一個是FileChannel, 則可以直接將資料從一個Channel傳輸到另外一個Channel
transferFrom() --- 對目標Channel呼叫
FileChannel的transferFrom()方法可將資料從源通道傳入到FileChannel中
兩種方式:
toChannel.transferFrom(fromChannel, position, count)
toChannel.transferFrom(position, count, fromChannel)
transferTo() --- 對源Channel呼叫
將資料從FileChannel傳輸到其他Channel中
eg:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Selector選擇器:
NIO能檢測到一到多個通道,並能知道通道的各類事件,一個單獨的執行緒可以管理多個Channel,從而管理多個網路連線
1.Selector建立
Selector selector = Selector.open();
2.向Selector中註冊通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ)
注意:
1.與Selector一起使用時,Channel必須處於非阻塞模式下,所以:不能將FileChannel於Selector一起使用,因為FileChannel不能切換到非阻塞模式
2.Channel通過register()方法註冊到Selector,並標明它感興趣的事件,之後通過Selector的select()判斷是否有感興趣的事件發生,如果有,通過selectedKeys()獲得興趣事件的集合
3.register(selector, Key)中,Key 表示通過Selector監聽Channel時,對什麼事件感興趣
4種不同型別事件:
Connect
Accept
Read
Write
3.SelectionKey
向Selector註冊Channel時,register()方法返回一個SelectionKey物件,這個物件包含感興趣的屬性:
interest集合
ready集合
Channel
Selector
附加的物件(可選)
4.通過Selector選擇通道
select() 方法 返回 "感興趣事件(連線,接受,讀,寫)" 已經就緒的那些通道
NIO聊天室參考:
http://blog.csdn.net/kindz_cn/article/details/51512672
http://blog.csdn.net/abc_key/article/details/29029879
32.Java中執行緒池原理和實現
JDK 執行緒池
Executors -建立-> 4種執行緒池 -實現-> ExecutorService(介面) -繼承-> Executor(介面)
4種執行緒池
newSingleThreadExecutor
建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
--- 單執行緒執行,按提交的執行緒順序依次執行
newFixedThreadPool
建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待
--- 推薦方式,超過執行緒池最大容量,執行緒等待被呼叫,
--- 定長執行緒池的大小最好根據系統資源進行設定。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
建立一個定長執行緒池,支援定時及週期性任務執行,延遲執行
eg:
/**
* 使用執行緒池定時延遲排程
*/
private static void test2(){
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延遲3秒後執行
scheduledExecutorService.schedule(getThread(),3, TimeUnit.SECONDS);
//延遲1秒後,每3秒執行一次
scheduledExecutorService.scheduleAtFixedRate(getThread(),1,3, TimeUnit.SECONDS);
}
newCachedThreadPool
建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒(空閒執行緒預設60s會被回收),若無可回收,則新建執行緒
--- 不推薦,如果一次提交過多執行緒,而且每個執行緒比較耗時耗記憶體,可能瞬間擠爆JVM記憶體!!!
1.建立
ExecutorService executorService = Executors.newXxThreadExecutor()
2.呼叫
1.不要返回值
void executorService(Runnable)
2.需要返回值
Future<T> submit(Callable<T>)
3.如果需要 定時或 延遲 執行執行緒, 使用 ScheduledExecutorService的schedule()和scheduleAtFixedRate() 呼叫執行緒
3.ThreadPoolExecutor是Executors類的底層實現,
ThreadPoolExecutor的構造器:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所儲存的執行緒數,包括空閒執行緒。
maximumPoolSize-池中允許的最大執行緒數。
keepAliveTime - 當執行緒數大於核心時,此為終止前多餘的空閒執行緒等待新任務的最長時間。
unit - keepAliveTime 引數的時間單位。
workQueue - 執行前用於保持任務的佇列。此佇列僅保持由 execute方法提交的 Runnable任務。
threadFactory - 執行程式建立新執行緒時使用的工廠。
handler - 由於超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理程式。
4.關於執行緒等待佇列
1.newFixedThreadPool和newSingleThreadExecutor 執行緒等待佇列是 LinkedBlockingQueue --- 無界阻塞佇列
2.newCachedThreadPool 執行緒等待佇列是 SynchronousQueue --- 同步阻塞佇列 --- 每個插入操作必須等待另一個執行緒的移除操作,同樣任何一個移除操作都等待另一個執行緒的插入操作。因此此佇列內部其 實沒有任何一個元素,或者說容量是0
3.ScheduledThreadPoolExecutor 執行緒等待佇列是 DelayedWorkQueue --- 延遲阻塞佇列
如何選擇執行緒阻塞佇列?
1.直接提交的無界佇列 SynchronousQueue
2.無界佇列 LinkedBlockingQueue
3.有界佇列 ArrayBlockingQueue
關於SynchronousQueue?
1.該Queue,在某次新增元素後,必須等待其他執行緒取走後才能繼續新增 (類似,進棧的元素,必須出棧,才能有新的元素被放入)
2.可避免在處理具有內部依賴性的請求集時出現鎖
eg:
如果你的任務A1,A2有內部關聯,A1需要先執行,那麼先提交A1,再提交A2,當使用SynchronousQueue我們可以保證,
A1必定先被執行,在A1麼有被執行前,A2不可能新增入queue中
5.執行緒池的4個組成:
1.執行緒池管理器(ThreadPool) --- 建立並管理執行緒池,包括:建立執行緒池,銷燬執行緒池,新增新任務
2.工作執行緒(PoolWorker) --- 執行緒池中執行緒,沒有任務時處於等待狀態,可以迴圈執行任務
3.任務介面(Task) --- 每個任務必須實現的介面,以便工作執行緒排程 --- Runnable
4.任務佇列(taskQueue) --- 存放沒有處理的任務,提供一種緩衝機制
6.JDK執行緒池原始碼分析:http://www.cnblogs.com/exe19/p/5359885.html
33.JDK動態代理和cglib位元組碼技術代理的區別?
1.JDK動態代理:
1.靜態代理 --- 代理物件和目標物件實現了相同的介面,目標物件作為代理物件的一個屬性,
具體介面實現中,可以呼叫目標物件相應方法前後加上其他業務處理邏輯
2.JDK動態代理只能針對實現了介面的類生成代理
2.CGLIB代理 --- 通過位元組碼技術,為目標物件生成一個與其功能一樣的子類
1.針對類實現代理
2.主要是對指定的類生產一個子類,覆蓋其中的所有方法
3.被代理類或方法不能宣告為final
3.區別:
1.JDK動態代理只能對實現了介面的類生成代理, 動態代理只能對於介面進行代理
2.cglib針對類實現代理,主要是對指定的類生成一個子類,覆蓋中的方法,因為是繼承,所以該類或方法最好不要宣告成final ,final可以阻止繼承和多型
3.Spring實現中,如果有介面,預設使用JDK動態代理,如果目標物件沒有實現介面,使用cglib代理,
如果目標物件實現了介面,可以強制使用CGLIB實現代理(新增CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
4.動態代理的應用
AOP(Aspect-OrientedProgramming,面向切面程式設計),AOP包括切面(aspect)、通知(advice)、連線點(joinpoint),實現方式就是通過對目標物件的代理在連線點前後加入通知,完成統一的切面操作。
實現AOP的技術,主要分為兩大類:
一是採用動態代理技術,利用擷取訊息的方式,對該訊息進行裝飾,以取代原有物件行為的執行;
二是採用靜態織入的方式,引入特定的語法建立“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的程式碼。
Spring提供了兩種方式來生成代理物件: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport物件的配置來決定。
預設的策略是如果目標類是介面,則使用JDK動態代理技術,如果目標物件沒有實現介面,則預設會採用CGLIB代理。
如果目標物件實現了介面,可以強制使用CGLIB實現代理(新增CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
5.參考:http://www.cnblogs.com/linghu-java/p/5714769.html
34.Spring的事務傳播級別
Spring中定義了7種傳播行為:
參考:https://yq.aliyun.com/articles/71303?spm=5176.8067842.tagmain.14.VE7RJr
35.關於Spring 宣告式事務的原理
參考:http://yemengying.com/2016/11/14/something-about-spring-transaction/
Spring的宣告式事務:
1.JavaConfig方法 --- 在需要管理事務的類或方法上新增 @Transactional註解,然後在配置類上新增 @EnableTransactionManagement註解
2.Xml方式 --- 新增 <tx:annotation-driven />
Spring會利用Aop在相關方法呼叫的前後進行事務管理
問題:
public class JayServiceImpl implements JayService {
public void A(List<Giraffe> giraffes) {
for (Giraffe giraffe : giraffes) {
B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
說明:
Service中A方法呼叫B方法,方法A沒有事務管理,方法B採用宣告式事務,通過在方法上宣告 @Transactional註解來做事務管理
問題:
Junit 測試方法 A 的時候發現方法 B 的事務並沒有開啟, 而直接呼叫方法 B 事務是正常開啟的???
// 沒有開啟事務
@Test
public void testA() {
giraffeService.A();
}
// 正常開啟事務
@Test
public void testB() {
giraffeService.B();
}
}
原理分析:
Spring在載入目標Bean時,會為宣告瞭@Transactional的Bean建立一個代理類,而目標類本身並不能感知到代理類的存在,
呼叫通過Spring上下文注入的Bean的方法,而不是直接呼叫目標類的方法
即:
先呼叫代理類的方法,代理類再呼叫目標類的方法
Calling Code
--call--> Proxy --->foo()
---> Pojo --> pojo.foo()
對於加了@Transactional註解的方法,在呼叫代理類方法時,會先通過攔截器 TransactionInterceptor開啟事務,
然後再呼叫目標類的方法,最後在呼叫結束後, TransactionInterceptor會提交或回滾事務
問題解析:
對於第一段的程式碼,我在方法 A 中呼叫方法 B,實際上是通過“this”的引用,也就是直接呼叫了目標類的方法,而非通過 Spring 上下文獲得的代理類,所以。。。事務是不會開啟滴
解決方法:
通過實現ApplicationContextAware介面獲得 Spring 的上下文,(或自動注入Context物件),然後獲得目標類的代理類,通過代理類的物件,呼叫方法 B,即可
public class GiraffeServiceImpl implements GiraffeService,ApplicationContextAware{
@Setter
private ApplicationContext applicationContext;
public void A(List<Giraffe> giraffes) {
GiraffeService service = applicationContext.getBean(GiraffeService.class);
for (Giraffe giraffe : giraffes) {
service.B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
36.Java類載入機制
裝載 ---> 連結(驗證 --> 準備 --> 解析) ---> 初始化
1.JVM類載入機制:
裝載:
1.找到該型別的class檔案,產生一個該型別的class檔案二進位制資料流(ClassLoader需要實現的loadClassData()方法)
2.解析該二進位制資料流為方法區內的資料結構
3.建立一個該型別的java.lang.Class例項
最終:通過defineClass()建立一個Java型別物件(Class物件)
找到二進位制位元組碼,並載入到JVM中
JVM通過類全限定名(包名.類名) + 類載入器 完成類的載入,生成類對應的Class物件
連結:
驗證:
負責對二進位制位元組碼進行校驗、類資訊是否符合JVM規範,有沒有安全問題、對class檔案長度和型別進行檢查
參考:http://www.importnew.com/17105.html
準備:
初始化類中靜態變數、並將其初始化為預設值 --- 只初始化靜態變數預設值 !!!,給其類變數賦值發生在初始化階段!!!
對於final型別的變數,準備階段直接賦初始值
該記憶體分配發生在方法區
解析:
解析類中呼叫的介面、類、欄位、方法的符號引用,把虛擬機器常量池中的符號引用轉換為直接引用
初始化:
1.對static類變數指定初始值!!!(2種方式:一種是通過類變數的初始化語句,一種是靜態初始化語句)
2.一個類的初始化需要先初始化其父類,並遞迴初始化其祖先類
2.JVM必須在每個類或介面主動使用時進行初始化:
主動使用的情況:
1.建立類的例項(無論是new、還是反射、克隆、序列化建立的)
2.使用某個類的靜態方法
3.訪問某個類或即可的靜態欄位
4.呼叫Java API中的某些反射方法
5.初始化某個類的子類(先初始化其父類)
6.啟動某個標明為啟動類的類(含main()方法)
主動使用會導致類的初始化,其超類均將在該類的初始化之前被初始化,但通過子類訪問父類的靜態欄位或方法時,對於子類(或子介面、介面的實現類)來說,這種訪問就是被動訪問,或者說訪問了該類(介面)中的不在該類(介面)中宣告的靜態成員
3.建立物件時,類中各成員的執行順序:
父靜態塊 <-- 子靜態塊 <-- 父普通程式碼塊 <-- 父構造器 <-- 子普通程式碼塊 <-- 子構造器
1.父類靜態成員和靜態初始化快,按在程式碼中出現的順序依次執行。
2.子類靜態成員和靜態初始化塊,按在程式碼中出現的順序依次執行。
3. 父類的例項成員和例項初始化塊,按在程式碼中出現的順序依次執行。
4.執行父類的構造方法。
5.子類例項成員和例項初始化塊,按在程式碼中出現的順序依次執行。
6.執行子類的構造方法。
eg:
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}
class Parent{
{
System.out.println("parent中的初始化塊");
}
static{
System.out.println("parent中static初始化塊");
}
public Parent(){
System.out.println("parent構造方法");
}
}
class Son extends Parent{
{
System.out.println("son中的初始化塊");
}
static{
System.out.println("son中的static初始化塊");
}
public Son(){
System.out.println("son構造方法");
}
}
結果:
parent中static初始化塊
son中的static初始化塊
parent中的初始化塊
parent構造方法
son中的初始化塊
son構造方法
37.MySQL效能優化
參考:
MySQL效能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.儲存引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支援事務處理,為每個表建立3個檔案,分別儲存不同內容
支援表級鎖,表的寫操作會阻塞其他使用者對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他使用者對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他使用者對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是序列的
eg:
tb_Demo表,那麼就會生成以下三個檔案:
1.tb_demo.frm,儲存表定義;
2.tb_demo.MYD,儲存資料;
3.tb_demo.MYI,儲存索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量資料時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入資料,適合管理:郵件或Web伺服器日誌資料
總結:
1.適合做count的計算 (注意:不含where條件的統計,因為MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 預設引擎
支援事務處理
引入了行級鎖(併發高)和外來鍵約束
不支援全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外來鍵約束 --- MySQL支援外來鍵的儲存引擎只有InnoDB
5.支援自動增加列 Auto_INCREMNET屬性
6.InnoDB是為處理巨大資料量時的最大效能設計
總結:
可靠性要求高,需要事務支援,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多效能開銷,而它的強項在於多執行緒的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定資料引擎的建立
細節和具體實現的差別:
1.InnoDB不支援FULLTEXT型別的索引。
2.InnoDB 中不儲存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT型別的欄位,InnoDB中必須包含只有該欄位的索引,但是在MyISAM表中,可以和其他欄位一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,匯入資料後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外來鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務型別來選擇合適的表型別,才能最大的發揮MySQL的效能優勢。
儲存引擎選擇依據?
是否需要支援事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外來鍵支援;
是否需要全文索引
經常使用什麼樣的查詢模式
資料量大小
eg:
需要事務和外來鍵約束 -- InnoDB
需要全文索引 -- MyISAM
資料量大,傾向於InnoDB,因為它支援事務處理和故障恢復,InnoDB可以利用事務日誌進行資料恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
運算元據表的習慣,也會影響效能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因為InnoDB不儲存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致效能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB效能的方法:
InnoDB支援事務,儲存過程,檢視和行級鎖,在高併發下,表現比MyISAM強很多
影響效能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設定為1的話,那麼每次插入資料的時候都會自動提交,導致效能急劇下降,應該是跟重新整理日誌有關係,設定為0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設定達到好的效能
設定innodb_buffer_pool_size能夠提升InnoDB的效能
設定查詢快取
2.配置檔案my.ini引數優化
1.max_connections --- 最大併發連線數,允許的同時客戶連線數, 預設100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢快取,用於快取select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設定query_cache_size大於0,可以極大改善查詢效率。而如果表資料頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 快取的最大執行緒數
5.sort_buffer --- 每個需要進行排序的執行緒分配該大小的一個緩衝區
6.wait_timeout --- 預設是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連線在8小時內沒有活動,就會自動斷開該連線。 不要設定太長,建議 7200
7.default-storage-engine=INNODB # 建立新表時將使用的預設儲存引擎
配置示例,2G記憶體,針對站多,抗壓型的設定,最佳:
table_cache=1024 實體記憶體越大,設定就越大.預設為2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 預設為2M
innodb_flush_log_at_trx_commit=1
(設定為0就是等到innodb_log_buffer_size列隊滿後再統一儲存,預設為1)
innodb_log_buffer_size=2M 預設為1M
innodb_thread_concurrency=8 你的伺服器CPU有幾個就設定為幾,建議用預設一般為8
key_buffer_size=256M 預設為218 調到128最佳
tmp_table_size=64M 預設為16M 調到64-256最掛
read_buffer_size=4M 預設為64K
read_rnd_buffer_size=16M 預設為256K
sort_buffer_size=32M 預設為256K
max_connections=1024 預設為1210
thread_cache_size=120 預設為60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 檢視執行效率,定位優化物件的效能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連線代替子查詢
7.當只要一行資料時,使用limit 1
8.為搜尋欄位建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啟查詢快取,併為查詢快取優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢快取,
2.像NOW()和RAND()或是其它的諸如此類的SQL函式都不會開啟查詢快取,因為這些函式的返回是會不定的易變的。所以,你所需要的就是用一個變數來代替MySQL的函式,從而開啟快取
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變為一個不衣變的值
38.JVM效能優化
1.
2.Java程式碼效能優化
1.沒必要儘量不要使用靜態變數
2.充分利用單例機制減少對資源的載入,縮短執行的時間,提高系統效率
單例適用場景:
1. 控制資源的使用,通過執行緒同步來控制資源的併發訪問;
2. 控制例項的產生,以達到節約資源的目的
3.減少物件建立,最大限度的重用物件
儘量避免在經常呼叫的方法中迴圈使用new物件 --- 享元模式(可減少物件多次建立)
4.使用final修飾符
5.儘量使用區域性變數
呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在分配給改方法的棧(Stack)中,速度較快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢
6.學會用StringBuilder和StringBuffer,並儘量確定其容量
單執行緒使用StringBuilder,多執行緒情況下使用StringBuffer,這樣效能會有很大提升
7.儘量使用基本資料型別代替物件 eg:字串建立
8.使用HashMa、ArrayList,HashTable、Vector等使用在多執行緒的場合,內部使用了同步機制,這個會降低程式的效能
9.深入理解HashMap原理
當你要建立一個比較大的hashMap時,充分利用另一個建構函式
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次進行了hash重構,擴容是一件很耗費效能的事,在預設initialCapacity只有16,
而 loadFactor是 0.75,需要多大的容量,你最好能準確的估計你所需要的最佳大小,同樣的Hashtable,Vectors也是一樣的道理
10.儘量在finally塊中釋放資源
11.儘早釋放無用物件的引用
12.儘量避免使用split,split由於支援正規表示式,所以效率比較低,考慮使用apache的 StringUtils.split(string,char),頻繁split的可以快取結果
13.儘量使用System.arraycopy ()代替通過來迴圈複製陣列
System.arraycopy()要比通過迴圈來複制陣列快的多
14..儘量快取經常使用的物件 推薦:redis快取
15.儘量避免非常大的記憶體分配
參考:http://developer.51cto.com/art/201511/496263.htm
61.MySQL效能優化
參考:
MySQL效能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.儲存引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支援事務處理,為每個表建立3個檔案,分別儲存不同內容
支援表級鎖,表的寫操作會阻塞其他使用者對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他使用者對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他使用者對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是序列的
eg:
tb_Demo表,那麼就會生成以下三個檔案:
1.tb_demo.frm,儲存表定義;
2.tb_demo.MYD,儲存資料;
3.tb_demo.MYI,儲存索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量資料時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入資料,適合管理:郵件或Web伺服器日誌資料
總結:
1.適合做count的計算 (注意:不含where條件的統計,因為MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 預設引擎
支援事務處理
引入了行級鎖(併發高)和外來鍵約束
不支援全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外來鍵約束 --- MySQL支援外來鍵的儲存引擎只有InnoDB
5.支援自動增加列 Auto_INCREMNET屬性
6.InnoDB是為處理巨大資料量時的最大效能設計
總結:
可靠性要求高,需要事務支援,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多效能開銷,而它的強項在於多執行緒的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定資料引擎的建立
細節和具體實現的差別:
1.InnoDB不支援FULLTEXT型別的索引。
2.InnoDB 中不儲存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT型別的欄位,InnoDB中必須包含只有該欄位的索引,但是在MyISAM表中,可以和其他欄位一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,匯入資料後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外來鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務型別來選擇合適的表型別,才能最大的發揮MySQL的效能優勢。
儲存引擎選擇依據?
是否需要支援事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外來鍵支援;
是否需要全文索引
經常使用什麼樣的查詢模式
資料量大小
eg:
需要事務和外來鍵約束 -- InnoDB
需要全文索引 -- MyISAM
資料量大,傾向於InnoDB,因為它支援事務處理和故障恢復,InnoDB可以利用事務日誌進行資料恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
運算元據表的習慣,也會影響效能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因為InnoDB不儲存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出儲存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致效能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB效能的方法:
InnoDB支援事務,儲存過程,檢視和行級鎖,在高併發下,表現比MyISAM強很多
影響效能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設定為1的話,那麼每次插入資料的時候都會自動提交,導致效能急劇下降,應該是跟重新整理日誌有關係,設定為0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設定達到好的效能
設定innodb_buffer_pool_size能夠提升InnoDB的效能
設定查詢快取
2.配置檔案my.ini引數優化
1.max_connections --- 最大併發連線數,允許的同時客戶連線數, 預設100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢快取,用於快取select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設定query_cache_size大於0,可以極大改善查詢效率。而如果表資料頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 快取的最大執行緒數
5.sort_buffer --- 每個需要進行排序的執行緒分配該大小的一個緩衝區
6.wait_timeout --- 預設是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連線在8小時內沒有活動,就會自動斷開該連線。 不要設定太長,建議 7200
7.default-storage-engine=INNODB # 建立新表時將使用的預設儲存引擎
配置示例,2G記憶體,針對站多,抗壓型的設定,最佳:
table_cache=1024 實體記憶體越大,設定就越大.預設為2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 預設為2M
innodb_flush_log_at_trx_commit=1
(設定為0就是等到innodb_log_buffer_size列隊滿後再統一儲存,預設為1)
innodb_log_buffer_size=2M 預設為1M
innodb_thread_concurrency=8 你的伺服器CPU有幾個就設定為幾,建議用預設一般為8
key_buffer_size=256M 預設為218 調到128最佳
tmp_table_size=64M 預設為16M 調到64-256最掛
read_buffer_size=4M 預設為64K
read_rnd_buffer_size=16M 預設為256K
sort_buffer_size=32M 預設為256K
max_connections=1024 預設為1210
thread_cache_size=120 預設為60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 檢視執行效率,定位優化物件的效能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連線代替子查詢
7.當只要一行資料時,使用limit 1
8.為搜尋欄位建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啟查詢快取,併為查詢快取優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢快取,
2.像NOW()和RAND()或是其它的諸如此類的SQL函式都不會開啟查詢快取,因為這些函式的返回是會不定的易變的。所以,你所需要的就是用一個變數來代替MySQL的函式,從而開啟快取
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變為一個不衣變的值
62.Java常見的鎖型別有哪些?請簡述其特點。
1、synchronized物件同步鎖:synchronized是對物件加鎖,可作用於物件、方法(相當於對this物件加鎖)、靜態方法(相當於對Class例項物件加鎖,鎖住的該類的所有物件)以保證併發環境的執行緒安全。同一時刻只有一個執行緒可以獲得鎖。
其底層實現是通過使用物件監視器Monitor,每個物件都有一個監視器,當執行緒試圖獲取Synchronized鎖定的物件時,就會去請求物件監視器(Monitor.Enter()方法),如果監視器空閒,則請求成功,會獲取執行鎖定程式碼的權利;如果監視器已被其他執行緒持有,執行緒進入同步佇列等待。
2、Lock同步鎖:與synchronized功能類似,可從Lock與synchronized區別進行分析:
1、Lock可以通過tryLock()方法非阻塞地獲取鎖而。如果獲取了鎖即立刻返回true,否則立刻返回false。這個方法還有加上定時等待的過載方法tryLock(long time, TimeUnit unit)方法,在定時期間內,如果獲取了鎖立刻返回true,否則在定時結束後返回false。在定時等待期間可以被中斷,丟擲InterruptException異常。而Synchronized在獲得鎖的過程中是不可被中斷的。
2、Lock可以通過lockInterrupt()方法可中斷的獲取鎖,與lock()方法不同的是等待時可以響應中斷,丟擲InterruptException異常。
3、Synchronized是隱式的加鎖解鎖,而Lock必須顯示的加鎖解鎖,而且解鎖應放到finnally中,保證一定會被解鎖,而Synchronized在出現異常時也會自動解鎖。但也因為這樣,Lock更加靈活。
4、Synchronized是JVM層面上的設計,對物件加鎖,基於物件監視器。Lock是程式碼實現的。
3、可重入鎖:ReentrantLock與Synchronized都是可重入鎖。可重入意味著,獲得鎖的執行緒可遞迴的再次獲取鎖。當所有鎖釋放後,其他執行緒才可以獲取鎖。
4、公平鎖與非公平鎖:“公平性”是指是否等待最久的執行緒就會獲得資源。如果獲得鎖的順序是順序的,那麼就是公平的。不公平鎖一般效率高於公平鎖。ReentrantLock可以通過建構函式引數控制鎖是否公平。
5、ReentrantReadWriteLock讀寫鎖:是一種非排它鎖, 一般的鎖都是排他鎖,就是同一時刻只有一個執行緒可以訪問,比如Synchronized和Lock。讀寫鎖就多個執行緒可以同時獲取讀鎖讀資源,當有寫操作的時候,獲取寫鎖,寫操作之後的讀寫操作都將被阻塞,直到寫鎖釋放。讀寫鎖適合寫操作較多的場景,效率較高。
6、樂觀鎖與悲觀鎖:在Java中的實際應用類並不多,大多用在資料庫鎖上,可參看:http://blog.csdn.net/sdyy321/article/details/6183412
7、死鎖:是當兩個執行緒互相等待獲取對方的物件監視器時就會發生死鎖。一旦出現死鎖,整個程式既不會出現異常也不會有提示,但所有執行緒都處於阻塞狀態。死鎖一般出現於多個同步監視器的情況。
63.volatile與automicInteger是什麼?如何使用?
在併發環境中有三個因素需要慎重考量,原子性、可見性、有序性。
voatile 保證了有序性(防止指令衝排序)和變數的記憶體可見性(每次都強制取主存資料),每次取到volatile變數一定是最新的
volatile主要用於解決可見性,它修飾變數,相當於對當前語句前後加上了“記憶體柵欄”。使當前程式碼之前的程式碼不會被重排到當前程式碼之後,當前程式碼之後的指令不會被重排到當前程式碼之前,一定程度保證了有序性。而volatile最主要的作用是使修改volatile修飾的變數值時會使所有執行緒中的快取失效,並強制寫入公共主存,保證了各個執行緒的一致。可以看做是輕量級的Synchronized。詳情可參看:http://www.cnblogs.com/dolphin0520/p/3920373.html。
automicXXX主要用於解決原子性,有一個很經典的問題:i++是原子性的操作碼?答案是不是,它其實是兩步操作,一步是取i的值,一步是++。在取值之後如果有另外的執行緒去修改這個值,那麼當前執行緒的i值就是舊資料,會影響最後的運算結果。使用automicXXX就可以非阻塞、保證原子性的對資料進行增減操作。詳情可參看:http://ifeve.com/java-atomic/
注:在此列舉的只是Java多執行緒最基礎的知識,也是面試官最常問到的,先打牢基礎,再去探討底層原理或者高階用法,除了這十個問題,在此再推薦一些其他的資料:
JVM底層又是如何實現synchronized的:http://www.open-open.com/lib/view/open1352431526366.html
Java執行緒池詳解:http://blog.csdn.net/zhangliangzi/article/details/52389766
Java執行緒池深度解析:http://www.cnblogs.com/dolphin0520/p/3932921.html
ConcurrentHashMap原理分析:http://www.cnblogs.com/ITtangtang/p/3948786.html
Java阻塞佇列詳解:http://ifeve.com/java-blocking-queue/
64.MyBatis中timestamp時間型別,在Spring MVC出參時無法轉為正確的時間型別?
在xml中配置: javaType為 java.sql.Timestamp
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" javaType="java.sql.Timestamp"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" javaType="java.sql.Timestamp"/>
65.Datatable自定義搜尋
//自定義搜尋,每次只能根據一個維度進行搜尋(按渠道或產品型別)
$("#channel_select,#brand_select").change(function(){
var tsval = $(this).val()
table.search(tsval, false, false).draw();
});
66.synchronized和Lock的底層實現原理?
參考:http://www.open-open.com/lib/view/open1352431526366.html
鎖原理:http://blog.csdn.net/Luxia_24/article/details/52403033
synchronized 在軟體層面依賴JVM
Lock 在硬體層面依賴特殊的CPU指令 --- CAS + JNI呼叫CPU指令來實現
synchronized 可以吧任何一個非 null 物件作為 "鎖",
作用於方法上時,鎖住的是物件例項this,
作用於靜態方法,鎖住的是物件對應的Class例項,因為Class資料儲存在永久帶,因此靜態方法鎖相當於該類的全域性鎖,
作用於某個物件例項,鎖住的是對應的程式碼塊
HotSpot JVM中,鎖 --- 物件監視器(物件來監視執行緒的互斥) --- synchronized的實現原理
物件監視器,設定幾種狀態來區分請求的執行緒:
Contention Set: 所有請求鎖的執行緒,被首先放置到該競爭佇列 --- 先進後出的虛擬佇列,會被執行緒併發訪問
Entry Set: 等待獲取鎖的執行緒(來自Contention Set)排隊佇列 --- 等待獲取物件鎖執行
Wait Set: 獲取鎖後,呼叫wait()方法,被阻塞的執行緒佇列 --- 等待再次獲取物件鎖
OnDeck: 任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck
Owner: 獲得鎖的執行緒稱為Owner
!Owner: 釋放鎖的執行緒
說明:
1.Entry Set 和Contention Set 同屬等待佇列,
2.Contention Set會被執行緒併發訪問,為了降低對Contention Set隊尾的爭用(為了減少加入與取出兩個執行緒對於contentionList的競爭),而建立Entry Set,如果Entry Set為空,則從Contention Set隊尾取出節點
3.Owner執行緒在unlock時,會從Contention Set中遷移執行緒到Entry Set,並會指定Entry Set中的某個執行緒(一般為Head)為Read(OnDeck)執行緒
4.Owner執行緒並不是把鎖傳遞給OnDeck執行緒,只是把競爭鎖的權利交給OnDeck,OnDeck執行緒需要重新競爭鎖
5.OnDeck執行緒獲得鎖喉變為Owner執行緒,無法獲得鎖的執行緒依然留在Entry Set中
6.如果Owner執行緒被wait()方法阻塞,則轉移後WaitSet中,如果某個時刻被notify/notifyAll喚醒,則再次轉移到EntrySet
執行緒的互斥,其實是執行緒對同一個物件的監視器monitor的操作:
每個物件都有一個監視器(monitor)!,當monitor被佔就會處於鎖定狀態,執行緒執行monitorentry指令時嘗試獲取monitor的所有權
1.如果monitor的進入數為0,則執行緒進入monitor,然後將進入數設定為1,執行緒即為monitor的所有者
2.如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數 +1 --- 鎖可重入 --- ReentrantLock 和synchronized 都是 可重入鎖
3.其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,知道monitor的進入數為0,再嘗試獲取monitor的所有權
4.執行緒呼叫一次unlock()釋放鎖,monitor的進入數就 -1 (只有monitor進入數為0,才能被其他執行緒搶佔)
5.一個執行緒獲取多少次鎖,就必須釋放多少次鎖,對於synchronized內建鎖 ,每一次進入和離開synchronized方法(程式碼塊),就是一個完整的鎖獲取和釋放
sleep()不會釋放鎖,等待指定時間後繼續執行
wait()會釋放鎖,進入物件監視器的 Wait Set佇列,等待被喚醒,被喚醒後,需要重新獲取鎖
wait()和notify/notifyAll必須成對出現,而且必須放在synchronized中,
yield() 不會釋放鎖, 只是讓當前執行緒讓出CPU佔用權
Synchronized底層優化 --- 偏向鎖、輕量級鎖
參考:http://www.cnblogs.com/paddix/p/5405678.html
http://www.jianshu.com/p/5dbb07c8d5d5
Synchronized效率低的原因?
Synchronized是通過物件內部的物件監視器鎖(monitor)來實現的,monitor本質是依賴於底層的作業系統的Mutex Lock(互斥鎖)來實現,
作業系統實現執行緒間切換需要從使用者態轉到核心態(JVM轉到作業系統核心),這個成本非常高,狀態轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因
Synchronized底層優化:
1.鎖的4種狀態:
無鎖狀態、偏向鎖、輕量級鎖、重量級鎖
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖(鎖升級只能從低到高升級,不會出現鎖的降級)
JDK1.6預設開啟偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖
鎖的狀態儲存在物件的標頭檔案中
重量級鎖 --- 依賴於作業系統Mutex Lock所實現的鎖, 需要從JVM轉到作業系統核心,進行互斥操作
輕量級鎖 --- 並不是用來代替重量級鎖,本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗
輕量級鎖目的:
為了線上程交替執行同步塊時提高效能 !!!
輕量級鎖適用場景:
執行緒交替執行同步塊的情況 ---- 鎖競爭不激烈的情況!
如果存在同一時間訪問同一個鎖,就會導致輕量級鎖升級為重量級鎖
偏向鎖 --- 為了在無多執行緒競爭的情況下,儘量減少不必要的輕量級鎖執行路徑
一旦執行緒第一次獲得了監視物件,之後讓監視物件 "偏向"這個執行緒,在該執行緒重複獲取鎖時,避免CAS操作
即:
設定一個變數,如果發現是true,無需再走加鎖、解鎖的流程!
偏向鎖目的:
解決無競爭下的鎖效能問題,在只有一個執行緒執行同步塊時,進一步提高效能
總結:
ynchronized的底層實現主要依靠Lock-Free的佇列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量 !!!
67.Spring如何解決Bean的迴圈依賴? --- 只支援Singleton作用域的, setter方式的迴圈依賴!!!
參考:https://my.oschina.net/yibuliushen/blog/737640
http://blog.csdn.net/caomiao2006/article/details/46511123
Spring容器迴圈依賴包括構造器迴圈依賴和setter迴圈依賴
如果是構造器迴圈依賴,Spring容器將無法啟動,報迴圈依賴異常BeanCurrentlInCreationException
解決方式:
將構造器注入方式改為屬性注入方式 --- setter
Spring 支援setter方法注入屬性方式的迴圈依賴
Spring中將迴圈依賴的處理分3中情況:
1.構造器迴圈依賴 --- 原理, Spring 不支援構造器方式的迴圈依賴
通過構造器注入構成的迴圈依賴是無法解決的,只能在容器啟動時丟擲BeanCurrentlInCreationException異常 --- 表示迴圈依賴
Spring容器將每一個正在建立的Bean的識別符號(id)放到 "當前建立bean池" 中,bean識別符號在建立過程中將一直保持在這個池中,
如果在建立bean過程中,發現自己已經在 "當前建立bean池"裡時,將丟擲 BeanCurrentlInCreationException異常,表示迴圈依賴,
而對於建立完畢的bean將從 "當前建立bean池"中清除掉
eg:
如在建立TestA類時,構造器需要TestB類,那將去建立TestB,在建立TestB類時又發現需要TestC類,則又去建立TestC,
最終在建立TestC時發現又需要TestA,從而形成一個環,沒辦法建立 --- 迴圈依賴,丟擲 BeanCurrentlInCreationException異常
配置檔案;
<bean id="testA" class="com.bean.TestA">
<constructor-arg index="0" ref="testB"/>
</bean>
<bean id="testB" class="com.bean.TestB">
<constructor-arg index="0" ref="testC"/>
</bean>
<bean id="testC" class="com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>
測試用例:
@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test.xml");
} catch (Exception e) {
//因為要在建立testC時丟擲;
Throwable ee1 = e.getCause().getCause().getCause();
throw e1;
}
}
分析:
Spring容器建立"testA"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testB",並將"testA"識別符號放到"當前建立bean池"。
Spring容器建立"testB"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testC",並將"testB"識別符號放到"當前建立bean池"。
Spring容器建立"testC"bean,首先去"當前建立bean池"查詢是否當前bean正在建立,如果沒發現,則繼續準備其需要的構造器引數"testA",並將"testC"識別符號放到"當前建立Bean池"。
到此為止Spring容器要去建立"testA"bean,發現該bean識別符號在"當前建立bean池"中,因為表示迴圈依賴,丟擲BeanCurrentlyInCreationException。
說明:
Spring中bean預設是單例的,對於singleton作用於的Bean,可通過setAllowCircularReferences(false)來禁用迴圈引用
2.Setter迴圈依賴 --- 原理, Spring支援setter方式注入屬性的迴圈依賴!
setter注入方式構成的迴圈依賴,通過Spring容器提前暴露剛完成構造器注入但未完成其他步驟(eg:setter注入)的bean來完成的,
而且只能解決Singleton單例作用域的bean迴圈依賴,通過提前暴露一個單例工廠方法,從而使其他bean能引用到該bean(注意:此時僅僅只是生了一個bean,該bean還未呼叫其他方法,如setter注入)
對單例Bean迴圈依賴的處理:通過遞迴方法,找出當前Bean的所有依賴Bean,然後提前快取起來
原理:
建立Bean A時,先通過無參構造器建立一個A例項,此時屬性都是空的,但物件引用已經建立建立出來,然後把Bean A的引用提前暴露出來,
然後setter B屬性時,建立B物件,此時同樣通過無參構造器,構造一個B物件的引用,並將B物件引用暴露出來。
接著B執行setter方法,去池中找到A(因為此時,A已經暴露出來,有指向該物件的引用了),這樣依賴B就構造完成,也初始化完成,然後A接著初始化完成,
迴圈依賴就這麼解決了!!!
總結:
先建立物件引用,再通過setter()方式,給屬性賦值,層層建立物件 !!!
Bean A初始化時,先對其依賴B進行初始化,同時,通過預設無參構造器,生成自己的引用,而不呼叫其setter()方法,
當B物件建立時,如果還依賴C,則也通過無參構造器,生成B的引用,
C物件建立時,如果引用了A,則去物件池中查到A的引用,然後呼叫setter()方式,注入A,完成C物件的建立
C建立完成後,B使用setter()方式,注入C,完成B物件建立,
B物件場景完成後,A使用setter()方式,注入B,完成A物件建立,
最終,完成setter()方式的迴圈依賴!
如果迴圈依賴的都是單例物件(都是通過setter方式注入屬性的),那麼這個肯定沒問題,放心使用即可!!!
如果一個是單例,一個是原型,那麼一定要保證單例物件能提前暴露出來,才可以正常注入屬性!!!
3.prototype範圍的依賴處理
對於"prototype"作用域bean,Spring容器無法完成依賴注入,因為Spring容器不進行快取"prototype"作用域的bean,因此無法提前暴露一個建立中的bean
這個spring也無能為力,因為是原型物件,A建立的時候不會提前暴露出來,所以,每次都是要建立,建立的時候,發現有相同的物件正在建立,同樣報錯,迴圈依賴錯誤
4.Spring建立Bean的原始碼解釋:
1.建立Bean的入口
AbstractBeanFactory-->doGetBean()
Object sharedInstance = getSingleton(beanName); //從快取中查詢,或者如果當前建立池中有並且已經暴露出來了,就返回這個物件
2.建立單例Bean方法
DefaultSingletonBeanRegistry-->getSingleton(String beanName, ObjectFactory<?> singletonFactory)
3.建立真正物件
AbstractAutowireCapableBeanFactory-->doCreateBean
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
} 注意這一步很關鍵,是呼叫構造方法建立一個例項物件,如果這個構造方法有引數,而且就是迴圈依賴的引數,那麼這個物件就無法建立了,
因為到這裡物件沒有建立,也沒有暴露當前物件,如果是無參的構造方法,那麼就可以,先建立一個物件,儘管所有的屬性都為空
68.Spring事務管理的原理?
參考:http://www.codeceo.com/article/spring-transactions.html
宣告式事務管理,在Service之上或Service的方法之上,新增 @Transactional註解
@Transactional如何工作?
Spring在啟動時,會去解析生成相關的Bean,這是會檢視擁有相關注解的類和方法,
並且為這些類和方法生成代理,並根據 @Transactional的相關引數進行相關配置注入,
這樣就在代理中把相關的事務處理掉了(開啟正常提交事務,異常回滾事務)
真正的資料庫層,事務提交和回滾是通過binlog和redo log實現的
Spring事務管理機制實現原理:
參考:
http://www.jianshu.com/p/4312162b1458
http://www.cnblogs.com/duanxz/p/3750845.html
http://www.92to.com/bangong/2016/11-05/12533010.html
在呼叫一個需要事務的元件時,管理器首先判斷當前呼叫(即:當前執行緒)有沒有事務,如果沒有事務則啟動一個事務,並把事務與當前執行緒繫結,
Spring使用TransactionSynchronizationManager的bindResource方法將當前執行緒與一個事務繫結,採用的方式就是ThreadLocal,
參考:DataSourceTransactionManager的啟動事務用的程式碼 doBegin()
通過動態代理或AOP方式,對所有需要事務管理的Bean進行載入,生成代理物件,並根據配置在invoke()方法中對當前呼叫的方法名進行判定,
並在method.invoke()方法前後為其加上合適的事務管理程式碼,根據method.invoke()執行結果,正常提交事務,異常回滾事務
實現了EntityManager介面的持久化上下文代理,包含3個組成部分:
1.EntityManager Proxy本身
2.事務的切面
3.事務管理器
遇到過的問題:
參考:59,為何宣告式事務沒有生效?
69.Spring如何處理高併發?高併發下,如何保證效能?
1.單例模式 + ThreadLocal
單例模式大大節省了物件的建立和銷燬,有利於效能提高,ThreadLocal用來保證執行緒安全性
Spring單例模式下,用ThreadLocal來切換不同執行緒直接的引數,用ThreadLocal是為了保證執行緒安全,實際上,ThreadLocal的key就是當前執行緒的Thread例項
單例模式下,Spring把每個執行緒可能存線上程安全問題的引數值放進了ThreadLocal,雖然是一個例項,但在不同執行緒下的資料是相互隔離的,
因為執行時建立和銷燬的bean大大減少了,所以大多數場景下,這種方式對記憶體資源的消耗較少,並且併發越高,優勢越明顯
2.ThreadLocal
相比同步機制,ThreadLocal則從另一個角度來解決多執行緒的併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突。
因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,
在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal !!!
3.Spring MVC在併發訪問時,是否會存線上程安全問題?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
http://blog.csdn.net/a236209186/article/details/61460211
Struts2是基於類的攔截(每次處理請求,都會例項化一個物件Action,不會有執行緒安全問題)
Spring MVC 是基於方法的攔截,粒度更細,而Spring的Controller預設是Singleton的,即:每個request請求,系統都會用同一個Controller去處理,
Spring MVC和Servlet都是方法級別的執行緒安全,如果單例的Controller或Servlet中存在例項變數,都是執行緒不安全的,而Struts2確實是執行緒安全的
優點:
不用每次建立Controller,減少了物件建立和銷燬
缺點:
Controller是單例的,Controller裡面的變數執行緒不安全
解決方案:
1.在Controller中使用ThreadLocal變數,把不安全的變數封裝進ThreadLocal,使用ThreadLocal來儲存類變數,將類變數儲存線上程的變數域中,讓不同的請求隔離開來
2.宣告Controller為原型 scope="prototype",每個請求都建立新的Controller
3.Controller中不使用例項變數
Spring MVC 如何保證request物件執行緒安全?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
InvocationHandler介面:這是springmvc保證request物件執行緒安全的核心。
通過實現該介面,開發者能夠在Java物件方法執行時進行干預,搭配Threadlocal就能夠實現執行緒安全
問題:判斷一下程式是否執行緒安全?
@Controller
public class UserController{
@Autowired
private HttpSession session
@RequestMapping(xxxxxxx)
public void getUser{
session.get ...
session.set...
....
}
}
結論:
該程式是執行緒安全的
解析:
專案啟動和執行時,Controller物件中的HttpSession並不是HttpSession例項,而是一個代理,
是org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler代理了HttpSession ,可通過這個程式碼求證:System.out.println(Proxy.getInvocationHandler(session));
只要當你真正呼叫HttpSession中的非java.lang.Object方法時才會真真去呼叫被代理的HttpSession裡面的方法
說一下session.get ...過程:首先從物件工廠從Threadlocal中取得HttpSession例項,然後通過反射呼叫該例項的set方法
特別注意:
1.Spring 應該是在請求進來的時候ThreadLocal.set(Session),然後在請求的生命週期中都是一個Thread ,執行完後ThreadLocal.remove(Session)
2.一個請求使用一個ThreadLocal,繫結對應的HttpSession,所以是執行緒安全的
3.對於 "注入" 到Controller中的單例物件, 都是由Spring統一管理的,Spring對注入Controller的物件使用了ThreadLocal + 代理機制,保證了執行緒安全
4.但是,對於在Controller中直接定義的例項變數,是執行緒不安全的!!!
eg:
@RestController
@RequestMapping("/test1")
public class ControllerTest1 {
private int i = 0;
@GetMapping("/count")
public void test1(){
System.out.println(i++);
}
//驗證Controller中的例項變數,執行緒不安全
@GetMapping("/t1")
public void test3(){
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i=0;i<1000;i++) {
service.execute(new Runnable() {
@Override
public void run() {
HttpUtils.sendGet("http://localhost:8080/test1/count", null);
}
});
}
}
}
呼叫:http://localhost:8080/test1/t1 方法,使用多執行緒對i進行操作,發現i的結果不是999,證明Controller中的例項變數是執行緒不安全的!
結論:
1.對於單例的Controller,Service中定義的例項變數,都不是執行緒安全的!!!
2.儘量避免在Controller和Service中定義多執行緒共享的例項變數
3.Spring使用ThreadLocal + InvocationHandler(動態代理)提供了高併發訪問的效能
4.對於Controller和Service中的例項變數,多執行緒訪問時,需要加鎖處理 或 設定 scope = "prototype"為每個請求創一個物件
5.對於 @Autowire注入的HttpServletRequest和HttpSession,Spring進行了特殊處理,不會有執行緒安全問題
70.ConcurrentLinkedQueue與BlockingQueue
2類執行緒安全的佇列:
1.阻塞佇列 --- 阻塞演算法 --- 佇列使用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式實現
2.同步佇列 --- 非阻塞演算法 --- 使用迴圈CAS方式實現
LinkedBlockingQueue 執行緒安全的阻塞佇列,實現了BlockingQueue介面,BlockingQueue繼承自java.util.Queue介面,
並在介面基礎上增加了take()和put()方法, 這2個方法正式佇列操作的阻塞版本
先進先出,可以指定容量,預設最大是Integer.MAX_VALUE;其中主要用到put和take方法,put方法在佇列滿的時候會阻塞直到有佇列成員被消費,take方法在佇列空的時候會阻塞,直到有佇列成員被放進來!!!
put() 向佇列中放資料 take() 從佇列中取資料
ConcurrentLinkedQueue 是Queue的一個安全實現.Queue中元素按FIFO原則進行排序.採用CAS操作,來保證元素的一致性。
非阻塞方式實現的無界執行緒安全佇列 !!!
offer()新增元素, poll()獲取元素 isEmpty()判斷佇列是否為空 (特別注意,不用size(),效率低,會遍歷佇列,儘量要避免用size而改用isEmpty())
採用CAS操作,允許多個執行緒併發執行,並不會因為你加鎖而阻塞執行緒,使得併發效能更好!!!
使用:http://www.cnblogs.com/dmir/p/4907515.html
http://blog.csdn.net/sunxianghuang/article/details/52046150
ConcurrentLinkedQueue原始碼解析:http://ifeve.com/concurrentlinkedqueue/
總結:
多數生產消費模型的首選資料結構就是佇列(先進先出)。Java提供的執行緒安全的Queue可以分為阻塞佇列和非阻塞佇列,
其中阻塞佇列的典型例子是BlockingQueue,非阻塞佇列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞佇列或者非阻塞佇列
Java中的7種阻塞佇列
阻塞佇列: --- 阻塞的是執行緒操作(拿和取元素)
常用於生產者和消費者場景,是生產者用來存放元素、消費者用來獲取元素的容器
put()阻塞:佇列滿時,阻塞插入元素的執行緒,直到佇列不滿
take()阻塞:佇列空時,阻塞獲取元素的執行緒,直到佇列不空
ArrayBlockingQueue:一個由陣列結構組成的有界阻塞佇列。
LinkedBlockingQueue:一個由連結串列結構組成的有界阻塞佇列。
PriorityBlockingQueue:一個支援優先順序排序的無界阻塞佇列。
DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。
SynchronousQueue:一個不儲存元素的阻塞佇列。 生產者和消費者直接傳遞資料,不對資料作快取,生產者和消費者通過在佇列裡排隊的方式來阻塞和喚醒 --- 速度快
執行緒數少時,使用SynchronousQueue 速度更快!!!
LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的區別和使用場景?
區別:
1.三者都是執行緒安全的
2.2個BlockingQueue是阻塞的,ConcurrentLinkedQueue是併發的
3.2個BlockingQueue使用鎖機制實現阻塞和執行緒安全(通過ReentrantLock + Condition阻塞容量為空時的取操作和容量滿時的寫操作),
ConcurrentLinkedQueue使用cas演算法保證執行緒安全
4.ArrayBlockingQueue使用一個鎖(lock + 2個Condition),而LinkedBlockingQueue使用2個鎖(鎖分離,取用takeLock + Condition,寫用putLock+Condition),所以LinkedBlockingQueue的吞吐量大,併發效能比Array高
LinkedBlockingQueue,對頭和尾採用不同的鎖,提高了吞吐量,適合 "消費者生產者" 模式
ArrayBlockingQueue, 陣列實現,使用一把全域性鎖並行對queue的讀寫操作,同時使用2個Condition阻塞容量為空時的讀操作和容量滿時的寫操作
5.正因為LinkedBlockingQueue使用兩個獨立的鎖控制資料同步,所以可以使存取兩種操作並行執行,從而提高併發效率。
而ArrayBlockingQueue使用一把鎖,造成在存取兩種操作爭搶一把鎖,而使得效能相對低下。LinkedBlockingQueue可以不設定佇列容量,預設為Integer.MAX_VALUE.其容易造成記憶體溢位,一般要設定其值
使用場景:
阻塞佇列優點:
多執行緒操作不需要同步,
佇列會自動平衡負載,即:生產和消費兩邊,處理快了會被阻塞,減少兩邊的處理速度差距,
自動平衡負載特性,造成它能被用於多生產者佇列,佇列滿了就要阻塞等著,直到消費者使佇列不滿才能繼續生產
ConcurrentLinkedQueue:
允許多執行緒共享訪問一個集合,多用於訊息佇列!!!
多消費者消費同一個 用 ConcurrentLinkedQueue:
BlockingQueueue:
多執行緒共享時阻塞,多用於任務佇列!!!
單消費者用 BlockingQueueue:
總結: 單個消費者用LinkedBlockignQueue, 多消費者用ConcurrentLinkedQueue !!!
單生產者,單消費者 用 LinkedBlockingqueue
多生產者,單消費者 用 LinkedBlockingqueue
單生產者 ,多消費者 用 ConcurrentLinkedQueue
多生產者 ,多消費者 用 ConcurrentLinkedQueue
71.Java多執行緒同步機制:3種型別
volatile 變數:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。僅提供記憶體可見性保證,不提供原子性。 --- 只保證可見性,不保證原子性,不絕對執行緒安全!!!
CAS 原子指令:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。它同時提供記憶體可見性和原子化更新保證。
內部鎖(synchronized)和顯式鎖(各種Lock):重量級多執行緒同步機制,可能會引起上下文切換和執行緒排程,它同時提供記憶體可見性和原子性。
參考:
非阻塞演算法在併發容器中的實現:ConcurrentLinkedQueue https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html
72.CAS在JDK中的實現
參考:http://blog.csdn.net/canot/article/details/50759424
1.Synchronized鎖機制存在的問題:
(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。
(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。
(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險
優化:
偏向鎖、輕量級鎖、減小鎖粒度
2.鎖分類
悲觀鎖 --- 獨佔鎖 --- synchronized是獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖
樂觀鎖 --- 每次不加鎖,而是假設沒有衝突,而去完成某項操作,如果因為衝突失敗就重試,直到成功為止
3.CAS原理 --- 實現樂觀鎖
CAS操作:
CAS有3個運算元:
V 記憶體值
A 舊的預期值
B 要修改的新值
當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做!!!
非阻塞演算法: 一個執行緒的失敗或掛起,不應該影響其他執行緒的失敗或掛起的演算法
CAS的硬體基礎和實現原理:
現代的CPU提供了特殊指令,可以自動更新共享資料,而且能夠檢測到其他執行緒的干擾,
而compareAndSet()就用這些代替了鎖定, compareAndSet利用JNI,藉助呼叫的C語言來完成CPU指令的操作
eg:
AtomicInteger 如何實現無鎖下的執行緒安全?
//在沒有鎖的機制下可能需要藉助volatile原語,保證執行緒間的資料是可見的(共享的)。這樣才獲取變數的值的時候才能直接讀取。
private volatile int value;
public final int get(){
return value;
}
//i++操作, 每次從記憶體中讀取資料,然後將此資料和 +1 後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止
public final int incrementAndGet(){
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
39.Redis效能優化
40.MongoDB效能優化
41.MQ效能優化和對比
42.一次搞定Java多執行緒併發程式設計
參考:http://blog.csdn.net/liuguangqiang/article/details/52137188
Java併發程式設計:CountDownLatch、CyclicBarrier和Semaphore
http://blog.csdn.net/zheng0518/article/details/42297259
http://www.cnblogs.com/dongguacai/p/6023028.html
執行緒安全:
1.Synchronized 執行緒同步
2.Lock + ReentrantLock 執行緒安全
執行緒通訊與協作:
0.wait()、notify()、notifyAll() 每個物件都有的3個方法,通道Monitor、waitSet、enterSet用來監聽鎖,存放執行緒佇列
1.ReentrantLock + Condition 併發控制多路複用
每個ReentrantLock可以建立多個Condition,每個Condition都可以通過控制一個物件鎖,來實現多個執行緒通訊
Condition方法:
執行緒1中呼叫await()後,執行緒1將釋放鎖,等待被喚醒
執行緒2獲取到鎖後,執行完畢,呼叫signal()方法,喚醒執行緒1
eg:
列印1到9這9個數字,A執行緒列印1,2,3,然後B執行緒列印4,5,6,,然後再A執行緒列印 7,8,9
2.ReadWriteLock 讀寫鎖
3.CountDownLatch
4.CyclicBarrier
5.Semaphore
效率提升:
6.執行緒池 + 阻塞佇列
相關文章
- 讀書筆記之《深入理解Java虛擬機器:JVM高階特性與最佳實踐》筆記Java虛擬機JVM
- 讀書筆記之《深入理解Java虛擬機器:JVM高階特性與最佳實踐》(下)筆記Java虛擬機JVM
- 大型Java進階專題(十一) 深入理解JVM (下)JavaJVM
- 探索高階 JVM 選項JVM
- Java高階篇——深入淺出Java類載入機制Java
- 高階Java工程師必備 ----- 深入分析 Java IO (三)Java工程師
- 高階Java工程師必備 ----- 深入分析 Java IO (二)NIOJava工程師
- [譯] 深入 React 高階元件React元件
- 深入React高階元件(HOC)React元件
- 深入理解JVM(③)執行緒與Java的執行緒JVM執行緒Java
- JVM知識點總覽:高階Java工程師面試必備JVMJava工程師面試
- 深入理解 React 高階元件React元件
- React 深入系列6:高階元件React元件
- 深入探索LangChain的高階功能LangChain
- Android自我進階——JAVA之JVMAndroidJavaJVM
- 阿里P8大佬帶你深入解析JVM與java阿里JVMJava
- Java面試題中高階進階(JVM篇Java垃圾回收)Java面試題JVM
- Python 高階程式設計:深入探索高階程式碼實踐Python程式設計
- 深入理解javascript系列(十六):深入高階函式JavaScript函式
- Java面試題中高階進階(JVM篇01)Java面試題JVM
- Java面試題中高階進階(JVM篇Java記憶體)Java面試題JVM記憶體
- [譯] 深入理解 React 高階元件React元件
- 深入淺出理解 React高階元件React元件
- 深入 Python 資料分析:高階技術與實戰應用Python
- 深入理解JVM(③)Java的鎖優化JVMJava優化
- 深入理解JVM(③)Java的模組化JVMJava
- Java面試題中高階進階(JVM調優篇)Java面試題JVM
- JVM(四):深入分析Java位元組碼-下JVMJava
- JVM(三):深入分析Java位元組碼-上JVMJava
- 深入解析Vue中watch的高階用法Vue
- javascript高階基礎的深入總結JavaScript
- Java 8 Strem高階操作JavaREM
- java知識點-高階Java
- Java高階特性—泛型Java泛型
- Java高階特性之集合Java
- JAVA進階之IO模型深入解析Java模型
- 深入理解JVM:效能分析與監控工具JVM
- JVM深入淺出 -- Java記憶體分配機制JVMJava記憶體
- 深入理解javascript系列(十五):高階函式JavaScript函式