一、Java多線程二
1.Java內存模型
? ? ? ?首先,程序計數器 (PC,Program CounterRegister)。在JVM規(guī)范中,每個線程都有它自己的程序計數器,并且任何時間一個線程都只有一個方法在執(zhí)行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執(zhí)行的Java方法的JVM指令地址;或者,如果是在執(zhí)行本地方法,則是未指定值(undefined)。
? ? ? ?第二, Java虛擬機棧(Java Virtual MachineStack) 。早期也叫Java棧。每個線程在創(chuàng)建時都會創(chuàng)建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應著一次次的Java方法調用。前面談程序計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,通常叫作當前幀,方法所在的類叫作當前類。如果在該方法中調用了其他方法,對應的新的棧幀會被創(chuàng)建出來,成為新的當前幀,一直到它返回結果或者執(zhí)行結束。JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧。棧幀中存儲著局部變量表、操作數(operand)棧、動態(tài)鏈接、方法正常退出或者異常退出的定義等。
? ? ? ?第三,堆(Heap)。它是Java內存管理的核心區(qū)域,用來放置Java對象實例,幾乎所有創(chuàng)建的Java對象實例都是被直接分配在堆上。堆被所有的線程共享,在虛擬機啟動時,我們指定的“Xmx”之類參數就是用來指定最大堆空間等指標。理所當然,堆也是垃圾收集器重點照顧的區(qū)域,所以堆內空間還會被不同的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。
? ? ? ?第四,方法區(qū) (Method Area)。這也是所有線程共享的一塊內存區(qū)域,用于存儲所謂的元(Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。由于早期的Hotspot JVM實現,很多人習慣于將方法區(qū)稱為永久代(Permanent Generation)。Oracle JDK8中將永久代移除,同時增加了元數據區(qū)(Metaspace)。
? ? ? ?第五,運行時常量池(Run-Time Constant Pool)。這是方法區(qū)的一部分。如果仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各種信息,還有一項信息就是常量池。Java的常量池可以存放各種常量信息,不管是編譯期生成的各種字面量,還是需要在運行時決定的符號引用,所以它比一般語言的符號表存儲的信息更加寬泛。
? ? ? ?第六, 本地方法棧(Native Method Stack)。它和Java虛擬機棧是非常相似的,支持對本地方法的調用,也是每個線程都會創(chuàng)建一個。在Oracle Hotspot JVM中,本地方法棧和Java虛擬機棧是在同一塊兒區(qū)域,這完全取決于技術實現的決定,并未在規(guī)范中強制。
? ? ? ?JMM總結,1 從JVM運行時視角來看,JVM內存可分為JVM棧、本地方法棧、PC計數器、方法區(qū)、堆;其中前三區(qū)是線程所私有的,后兩者則是所有線程共有的;2 從JVM內存功能視角來看,JVM可分為堆內存、非堆內存與其他。其中堆內存對應于上述的堆區(qū);非堆內存對應于上述的JVM棧、本地方法棧、PC計數器、方法區(qū);其他則對應于直接內存;3 從線程運行視角來看,JVM可分為主內存與線程工作內存。Java內存模型規(guī)定了所有的變量都存儲在主內存中;每個線程的工作內存保存了被該線程使用到的變量,這些變量是主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量;4 從垃圾回收視角來看,JVM中的堆區(qū)=新生代+老年代。新生代主要用于存放新創(chuàng)建的對象與存活時長小的對象,新生代=E+S1+S2;老年代則用于存放存活時間長的對象。
2.如何監(jiān)控和診斷JVM內存
- 可以使用綜合性的圖形化工具,如JConsole、VisualVM(注意,從Oracle JDK 9開始,VisualVM已經不再包含在JDK安裝包中)等。這些工具具體使用起來相對比較直觀,直接連接到Java進程,然后就可以在圖形化界面里掌握內存使用情況。以JConsole為例,其內存頁面可以顯示常見的 堆內存 和 各種堆外部分 使用狀態(tài)。
- 也可以使用命令行工具進行運行時查詢,如jstat和jmap等工具都提供了一些選項,可以查看堆、方法區(qū)等使用數據。
- 或者,也可以使用jmap等提供的命令,生成堆轉儲(Heap Dump)文件,然后利用jhat或Eclipse MAT等堆轉儲分析工具進行詳細分析。
- 如果你使用的是Tomcat、Weblogic等Java EE服務器,這些服務器同樣提供了內存管理相關的功能。
- 另外,從某種程度上來說,GC日志等輸出,同樣包含著豐富的信息。
這里有一個相對特殊的部分,就是是堆外內存中的直接內存,前面的工具基本不適用,可以使用JDK自帶的Native Memory Tracking(NMT)特性,它會從JVM本地內存分配的角度進行解讀。
3.Java常見的垃圾收集器
- Serial GC,它是最古老的垃圾收集器,“Serial”體現在其收集工作是單線程的,并且在進行垃圾收集過程中,會進入臭名昭著的“Stop-The-World”狀態(tài)。當然,其單線程設計也意味著精簡的GC實現,無需維護復雜的數據結構,初始化也簡單,所以一直是Client模式下JVM的默認選項。 從年代的角度,通常將其老年代實現單獨稱作Serial Old,它采用了標記-整理(Mark-Compact)算法,區(qū)別于新生代的復制算法。
Serial GC的對應JVM參數是:
-XX:+UseSerialGC
- ParNew GC,很明顯是個新生代GC實現,它實際是Serial GC的多線程版本,最常見的應用場景是配合老年代的CMS GC工作,下面是對應參數
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
- CMS(Concurrent Mark Sweep) GC,基于標記-清除(Mark-Sweep)算法,設計目標是盡量減少停頓時間,這一點對于Web等反應時間敏感的應用非常重要,一直到今天,仍然有很多系統(tǒng)使用CMS GC。但是,CMS采用的標記-清除算法,存在著內存碎片化問題,所以難以避免在長時間運行等情況下發(fā)生full GC,導致惡劣的停頓。另外,既然強調了并發(fā)(Concurrent),CMS會占用更多CPU資源,并和用戶線程爭搶。
- Parrallel GC,在早期JDK 8等版本中,它是server模式JVM的默認GC選擇,也被稱作是吞吐量優(yōu)先的GC。它的算法和Serial GC比較相似,盡管實現要復雜的多,其特點是新生代和老年代GC都是并行進行的,在常見的服務器環(huán)境中更加高效。
開啟選項是:
-XX:+UseParallelGC
另外,Parallel GC引入了開發(fā)者友好的配置項,我們可以直接設置暫停時間或吞吐量等目標,JVM會自動進行適應性調整,例如下面參數:
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC時間和用戶時間比例 = 1 / (N+1)
- G1 GC這是一種兼顧吞吐量和停頓時間的GC實現,是Oracle JDK 9以后的默認GC選項。G1可以直觀的設定停頓時間的目標,相比于CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。 G1 GC仍然存在著年代的概念,但是其內存結構并不是簡單的條帶式劃分,而是類似棋盤的一個個region。Region之間是復制算法,但整體上實際可看作是標記-整理(Mark-Compact)算法,可以有效地避免內存碎片,尤其是當Java堆非常大的時候,G1的優(yōu)勢更加明顯。 G1吞吐量和停頓表現都非常不錯,并且仍然在不斷地完善,與此同時CMS已經在JDK 9中被標記為廢棄(deprecated),所以G1 GC值得你深入掌握。各種垃圾收集器特點總結如下:
? ? ? ?Serial收集器:串行運行;作用于新生代;復制算法;響應速度優(yōu)先;適用于單CPU環(huán)境下的client模式。
? ? ? ?ParNew收集器:并行運行;作用于新生代;復制算法;響應速度優(yōu)先;多CPU環(huán)境Server模式下與CMS配合使用。
? ? ? ?Parallel Scavenge收集器:并行運行;作用于新生代;復制算法;吞吐量優(yōu)先;適用于后臺運算而不需要太多交互的場景。
? ? ? ?Serial Old收集器:串行運行;作用于老年代;標記-整理算法;響應速度優(yōu)先;單CPU環(huán)境下的Client模式。
? ? ? ?Parallel Old收集器:并行運行;作用于老年代;標記-整理算法;吞吐量優(yōu)先;適用于后臺運算而不需要太多交互的場景。
? ? ? ?CMS收集器:并發(fā)運行;作用于老年代;標記-清除算法;響應速度優(yōu)先;適用于互聯(lián)網或B/S業(yè)務。
? ? ? ?G1收集器:并發(fā)運行;可作用于新生代或老年代;標記-整理算法+復制算法;響應速度優(yōu)先;面向服務端應用。
4.GC調優(yōu)的思路
? ? ? ?對于GC調優(yōu)來說,首先就需要清楚調優(yōu)的目標是什么?從性能的角度看,通常關注三個方面,內存占用(footprint)、延時(latency)和吞吐量(throughput),大多數情況下調優(yōu)會側重于其中一個或者兩個方面的目標,很少有情況可以兼顧三個不同的角度。當然,除了上面通常的三個方面,也可能需要考慮其他GC相關的場景,例如,OOM也可能與不合理的GC相關參數有關;或者,應用啟動速度方面的需求,GC也會是個考慮的方面?;镜恼{優(yōu)思路可以總結為:
- 理解應用需求和問題,確定調優(yōu)目標。假設,我們開發(fā)了一個應用服務,但發(fā)現偶爾會出現性能抖動,出現較長的服務停頓。評估用戶可接受的響應時間和業(yè)務量,將目標簡化為,希望GC暫停盡量控制在200ms以內,并且保證一定標準的吞吐量。
- 掌握JVM和GC的狀態(tài),定位具體的問題,確定真的有GC調優(yōu)的必要。具體有很多方法,比如,通過jstat等工具查看GC等相關狀態(tài),可以開啟GC日志,或者是利用操作系統(tǒng)提供的診斷工具等。例如,通過追蹤GC日志,就可以查找是不是GC在特定時間發(fā)生了長時間的暫停,進而導致了應用響應不及時。
- 這里需要思考,選擇的GC類型是否符合我們的應用特征,如果是,具體問題表現在哪里,是Minor GC過長,還是Mixed GC等出現異常停頓情況;如果不是,考慮切換到什么類型,如CMS和G1都是更側重于低延遲的GC選項。
- 通過分析確定具體調整的參數或者軟硬件配置。
- 驗證是否達到調優(yōu)目標,如果達到目標,即可以考慮結束調優(yōu);否則,重復完成分析、調整、驗證這個過程。
5.happens-before原則
? ? ? ?程序順序原則——一個線程內保證語義的串行性;
? ? ? ?volatile規(guī)則——volatile變量的寫先發(fā)生于讀,這保證了volatile變量的可見性;
? ? ? ?鎖規(guī)則——解鎖必然發(fā)生在隨后的加鎖前;
? ? ? ?傳遞性——a先于b,b先于c,那么a必然先于c;
? ? ? ?線程的start()方法先于它的每一個動作;
? ? ? ?線程的所有操作先于線程的終結;
? ? ? ?線程的中斷先于被中斷線程的代碼;
? ? ? ?對象的構造函數執(zhí)行、結束先于finalize()方法。
6.Java程序在Docker上運行有哪些問題?
- 如果未配置合適的JVM堆和元數據區(qū)、直接內存等參數,Java就有可能試圖使用超過容器限制的內存,最終被容器OOM kill,或者自身發(fā)生OOM。
- 錯誤判斷了可獲取的CPU資源,例如,Docker限制了CPU的核數,JVM就可能設置不合適的GC并行線程數等。
- 從應用打包、發(fā)布等角度出發(fā),JDK自身就比較大,生成的鏡像就更為臃腫,當我們的鏡像非常多的時候,鏡像的存儲等開銷就比較明顯了。如果考慮到微服務、Serverless等新的架構和場景,Java自身的大小、內存占用、啟動速度,都存在一定局限性,因為Java早期的優(yōu)化大多是針對長時間運行的大型服務器端應用。
7.Java注入攻擊
? ? ? ?注入式(Inject)攻擊是一類非常常見的攻擊方式,其基本特征是程序允許攻擊者將不可信的動態(tài)內容注入到程序中,并將其執(zhí)行,這就可能完全改變最初預計的執(zhí)行過程,產生惡意效果。下面是幾種主要的注入式攻擊途徑,原則上提供動態(tài)執(zhí)行能力的語言特性,都需要提防發(fā)生注入攻擊的可能。
? ? ? ?首先,就是最常見的SQL注入攻擊。一個典型的場景就是Web系統(tǒng)的用戶登錄功能,根據用戶輸入的用戶名和密碼,我們需要去后端數據庫核實信息。假設應用邏輯是,后端程序利用界面輸入動態(tài)生成類似下面的SQL,然后讓JDBC執(zhí)行。
Select * from use_info where username = “input_usr_name” and password = “input_pwd”
? ? ? ?但是,如果我輸入的input_pwd是類似下面的文本,
“ or “”=”
? ? ? ?那么,拼接出的SQL字符串就變成了下面的條件,OR的存在導致輸入什么名字都是復合條件的。
Select * from use_info where username = “input_usr_name” and password = “” or “” = “”
? ? ? ?這里只是舉個簡單的例子,它是利用了期望輸入和可能輸入之間的偏差。上面例子中,期望用戶輸入一個數值,但實際輸入的則是SQL語句片段。類似場景可以利用注入的不同SQL語句,進行各種不同目的的攻擊,甚至還可以加上“;delete
xxx”之類語句,如果數據庫權限控制不合理,攻擊效果就可能是災難性的。
? ? ? ?第二,操作系統(tǒng)命令注入。Java語言提供了類似Runtime.exec(…)的API,可以用來執(zhí)行特定命令,假設我們構建了一個應用,以輸入文本作為參數,執(zhí)行下面的命令:
ls –la input_file_name
? ? ? ?但是如果用戶輸入是 “input_file_name;rm –rf/”,這就有可能出現問題了。當然,這只是個舉例,Java標準類庫本身進行了非常多的改進,所以類似這種編程錯誤,未必可以真的完成攻擊,但其反映的一類場景是真實存在的。
? ? ? ?第三,XML注入攻擊。Java核心類庫提供了全面的XML處理、轉換等各種API,而XML自身是可以包含動態(tài)內容的,例如XPATH,如果使用不當,可能導致訪問惡意內容。還有類似LDAP等允許動態(tài)內容的協(xié)議,都是可能利用特定命令,構造注入式攻擊的,包括XSS(Cross-site Scripting)攻擊,雖然并不和Java直接相關,但也可能在JSP等動態(tài)頁面中發(fā)生。