java核心技術-(總結自楊曉峰-java核心技術36講)
阿新 • • 發佈:2018-12-14
1. 談談你對java平臺的理解
- 首先是java最顯著的兩個特性,一次寫入處處執行;還有垃圾收集器gc,gc能夠對java記憶體進行管理回收,程式設計師不需要關心記憶體的分配和回收問題
- 然後談談jre和jdk的區別,jre包含了jvm和java類庫;jdk除了jvm和java類庫,還包含了一些java工具集
- 常見的垃圾收集器有:
- Serial GC:序列收集,垃圾回收時會阻塞工作執行緒
- Parallel GC:並行收集,多執行緒收集,停頓時間短,吞吐量高
- CMS:使用標記清除演算法,多執行緒進行垃圾收集
- G1:吸收了CMS的優點,將堆劃分為多個連續的區域,進行多執行緒收集。區域間採用複製演算法,整體採用標記整理演算法,避免記憶體碎片
- 垃圾收集器特點
- Serial收集器:序列執行;作用於新生代;複製演算法;響應速度優先;適用於單CPU環境下的client模式。
- ParNew收集器:並行執行;作用於新生代;複製演算法;響應速度優先;多CPU環境Server模式下與CMS配合使用。
- Parallel Scavenge收集器:並行執行;作用於新生代;複製演算法;吞吐量優先;適用於後臺運算而不需要太多互動的場景。
- Serial Old收集器:序列執行;作用於老年代;標記-整理演算法;響應速度優先;單CPU環境下的Client模式。
- Parallel Old收集器:並行執行;作用於老年代;標記-整理演算法;吞吐量優先;適用於後臺運算而不需要太多互動的場景。
- CMS收集器:併發執行;作用於老年代;標記-清除演算法;響應速度優先;適用於網際網路或B/S業務。
- G1收集器:併發執行;可作用於新生代或老年代;標記-整理演算法+複製演算法;響應速度優先;面向服務端應用
- .class檔案在JVM中,進行的是解析或編譯執行,JVM會對.class檔案進行解析執行,同時JVM中存在JIT編譯器,會對位元組碼檔案進行編譯預熱,熱點程式碼會編譯優化成機器碼執行
2. Exception和Error
- Exception是異常,Error是錯誤,異常可以捕獲處理,錯誤不需要處理
- try-catch儘量包裹需要包裹的程式碼塊,而不是全部
- Exception捕獲儘量細化,不要直接捕獲Exception
3. 強引用、軟引用、弱引用、幻象引用有什麼區別
- 強引用:最常見的引用,我們平時Object obj = new Object(),產生的引用都是強引用,只有在沒有引用關係或obj = null的時候會被垃圾收集器收集
- 軟引用:當記憶體不夠的時候,會優先回收軟引用的物件,平時的用法和強引用一樣
- 弱引用:生命週期比軟引用短,當垃圾收集器掃描到弱引用物件時,就會被回收
- 虛引用(幻象引用):無法通過引用獲取物件屬性,通常用來監視物件是否被回收
4. 談談Java反射機制,動態代理是基於什麼原理?
- 動態代理可以用反射實現,比如jdk自身提供的動態代理
- 也可以利用位元組碼操作機制,cglib(基於asm)
5. Java提供了哪些IO方式? NIO如何實現多路複用?
- 由於nio實際上是同步非阻塞io,是一個執行緒在同步的進行事件處理,當一組channel處理完畢以後,去檢查有沒有又可以處理的channel。這也就是同步+非阻塞。同步,指每個準備好的channel處理是依次進行的,非阻塞,是指執行緒不會傻傻的等待讀。只有當channel準備好後,才會進行。
- 當每個channel所進行的都是耗時操作時,由於是同步操作,就會積壓很多channel任務,從而完成影響。那麼就需要對nio進行類似負載均衡的操作,如用執行緒池去進行管理讀寫,將channel分給其他的執行緒去執行,這樣既充分利用了每一個執行緒,
- nio不適合資料量太大互動的場景
6. IO和NIO拷貝的效率問題
- 你需要理解使用者態空間(User Space)和核心態空間(Kernel Space),這是作業系統層面的基本概念,作業系統核心、硬體驅動等執行在核心態空間,具有相對高的特權;而使用者態空間,則是給普通應用和服務使用。
當我們使用輸入輸出流進行讀寫時,實際上是進行了多次上下文切換,比如應用讀取資料時,先在核心態將資料從磁碟讀取到核心快取,再切換到使用者態將資料從核心快取讀取到使用者快取。所以,這種方式會帶來一定的額外開銷,可能會降低IO效率。
- 而基於NIO transferTo的實現方式,在Linux和Unix上,則會使用到零拷貝技術,資料傳輸並不需要使用者態參與,省去了上下文切換的開銷和不必要的記憶體拷貝,進而可能提高應用拷貝效能。注意,transferTo不僅僅是可以用在檔案拷貝中,與其類似的,例如讀取磁碟檔案,然後進行Socket傳送,同樣可以享受這種機制帶來的效能和擴充套件性提高。
零拷貝可以理解為核心態空間與磁碟之間的資料傳輸,不需要再經過使用者態空間
7. 談談你知道的設計模式?請手動實現單例模式,Spring等框架中使用了哪些模式?
- 設計模式可分為三種類型,建立型、結構型和行為型
- 建立型例如:單例,工廠,建造者
- 結構型例如:介面卡,裝飾器,代理模式等
- 行為型例如:觀察者,模板模式,命令模式
- Spring中比較明顯的有BeanFactory工廠模式,AOP代理模式,jdbcTemplate模板模式,各種監聽器Listener,觀察者模式
8. Java併發包提供了哪些併發工具類?
- 提供了比synchronized更加高階的各種同步結構,包括CountDownLatch、CyclicBarrier、Semaphore等,可以實現更加豐富的多執行緒操作,比如利用Semaphore作為資源控制器,限制同時進行工作的執行緒數量。
- 各種執行緒安全的容器,比如最常見的ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通過類似快照機制,實現執行緒安全的動態陣列CopyOnWriteArrayList等。
- 各種併發佇列實現,如各種BlockedQueue實現,比較典型的ArrayBlockingQueue、 SynchorousQueue或針對特定場景的PriorityBlockingQueue等。
- 強大的Executor框架,可以建立各種不同型別的執行緒池,排程任務執行等
9. 併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?
- Concurrent型別基於lock-free,在常見的多執行緒訪問場景,一般可以提供較高吞吐量
- 而LinkedBlockingQueue內部則是基於鎖,並提供了BlockingQueue的等待性方法。
- Concurrent型別沒有類似CopyOnWrite之類容器相對較重的修改開銷。
- 但是,凡事都是有代價的,Concurrent往往提供了較低的遍歷一致性。你可以這樣理解所謂的弱一致性,例如,當利用迭代器遍歷時,如果容器發生修改,迭代器仍然可以繼續進行遍歷
- 與弱一致性對應的,就是同步容器常見的行為“fast-fail”,也就是檢測到容器在遍歷過程中發生了修改,則丟擲ConcurrentModifcationException,不再繼續遍歷。
- 弱一致性的另外一個體現是,size等操作準確性是有限的,未必是100%準確
- 與此同時,讀取的效能具有一定的不確定性
10. Java併發類庫提供的執行緒池有哪幾種? 分別有什麼特點?
- Executors目前提供了5種不同的執行緒池建立配置
- newCachedThreadPool(),它是一種用來處理大量短時間工作任務的執行緒池,具有幾個鮮明特點:它會試圖快取執行緒並重用,當無快取執行緒可用時,就會建立新的工作執行緒;如果執行緒閒置的時間超過60秒,則被終止並移出快取;長時間閒置時,這種執行緒池,不會消耗什麼資源。其內部使用SynchronousQueue作為工作佇列
- newFixedThreadPool(int nThreads),重用指定數目(nThreads)的執行緒,其背後使用的是無界的工作佇列,任何時候最多有nThreads個工作執行緒是活動的。這意味著,如果任務數量超過了活動佇列數目,將在工作佇列中等待空閒執行緒出現;如果有工作執行緒退出,將會有新的工作執行緒被建立,以補足指定的數目nThreads。
- newSingleThreadExecutor(),它的特點在於工作執行緒數目被限制為1,操作一個無界的工作佇列,所以它保證了所有任務的都是被順序執行,最多會有一個任務處於活動狀態,並且不允許使用者改動執行緒池例項,因此可以避免其改變執行緒數目
- newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),建立的是個ScheduledExecutorService,可以進行定時或週期性的工作排程,區別在於單一工作執行緒還是多個工作執行緒。
- newWorkStealingPool(int parallelism),這是一個經常被人忽略的執行緒池,Java 8才加入這個建立方法,其內部會構建ForkJoinPool,利用Work-Stealing演算法,並行地處理任務,不保證處理順序。
- 執行緒數大致計算 執行緒數 = CPU核數 × (1 + 平均等待時間/平均工作時間)
11. 談談JVM記憶體區域的劃分,哪些區域可能發生OutOfMemoryError?
- 程式計數器(PC,Program Counter Register)。在JVM規範中,每個執行緒都有它自己的程式計數器,並且任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法。程式計數器會儲存當前執行緒正在執行的Java方法的JVM指令地址;或者,如果是在執行本地方法,則是未指定值(undefned)。
- Java虛擬機器棧(Java Virtual Machine Stack),早期也叫Java棧。每個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),對應著一次次的Java方法呼叫。
前面談程式計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,通常叫作當前幀,方法所在的類叫作當前類。如果在該方法中呼叫了其他方法,對應的新的棧幀會被創建出來,成為新的當前幀,一直到它返回結果或者執行結束。JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧。
棧幀中儲存著區域性變量表、運算元(operand)棧、動態連結、方法正常退出或者異常退出的定義等。
- 堆(Heap),它是Java記憶體管理的核心區域,用來放置Java物件例項,幾乎所有建立的Java物件例項都是被直接分配在堆上。堆被所有的執行緒共享,在虛擬機器啟動時,我們指定的“Xmx”之類引數就是用來指定最大堆空間等指標。
理所當然,堆也是垃圾收集器重點照顧的區域,所以堆內空間還會被不同的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。 - 方法區(Method Area)。這也是所有執行緒共享的一塊記憶體區域,用於儲存所謂的元(Meta)資料,例如類結構資訊,以及對應的執行時常量池、欄位、方法程式碼等。
由於早期的Hotspot JVM實現,很多人習慣於將方法區稱為永久代(Permanent Generation)。Oracle JDK 8中將永久代移除,同時增加了元資料區(Metaspace) - 本地方法棧(Native Method Stack)。它和Java虛擬機器棧是非常相似的,支援對本地方法的呼叫,也是每個執行緒都會建立一個。在Oracle Hotspot JVM中,本地方法棧和Java虛擬機器棧是在同一塊兒區域,這完全取決於技術實現的決定,並未在規範中強制
- 兩點區別
- 直接記憶體(Direct Memory)區域,它就是Direct Bufer所直接分配的記憶體,也是個容易出現問題的地方。儘管,在JVM工程師的眼中,並不認為它是JVM內部記憶體的一部分,也並未體現JVM記憶體模型中。
- JVM本身是個本地程式,還需要其他的記憶體去完成各種基本任務,比如,JIT Compiler在執行時對熱點方法進行編譯,就會將編譯後的方法儲存在Code Cache裡面;GC等功能需要執行在本地執行緒之中,類似部分都需要佔用記憶體空間。這些是實現JVM JIT等功能的需要,但規範中並不涉及
- 除了程式計數器,其他區域都有可能會因為可能的空間不足發生OutOfMemoryError,簡單總結如下:
- 堆記憶體不足是最常見的OOM原因之一,丟擲的錯誤資訊是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在記憶體洩漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯式指定JVM堆大小或者指定數值偏小;或者出現JVM處理引用不及時,導致堆積起來,記憶體無法釋放等。
- 而對於Java虛擬機器棧和本地方法棧,這裡要稍微複雜一點。如果我們寫一段程式不斷的進行遞迴呼叫,而且沒有退出條件,就會導致不斷地進行壓棧。類似這種情況,JVM實際會丟擲StackOverFlowError;當然,如果JVM試圖去擴充套件棧空間的的時候失敗,則會丟擲OutOfMemoryError。
- 對於老版本的Oracle JDK,因為永久代的大小是有限的,並且JVM對永久代垃圾回收(如,常量池回收、解除安裝不再需要的型別)非常不積極,所以當我們不斷新增新型別的時
候,永久代出現OutOfMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似Intern字串快取佔用太多空間,也會導致OOM問題。對應的異常資訊,會標記出來和永久代相關:“java.lang.OutOfMemoryError: PermGen space”。 - 隨著元資料區的引入,方法區記憶體已經不再那麼窘迫,所以相應的OOM有所改觀,出現OOM,異常資訊則變成了:“java.lang.OutOfMemoryError: Metaspace”。直接記憶體不足,也會導致OOM
12. 如何監控和診斷JVM堆內和堆外記憶體使用?
- 可以使用綜合性的圖形化工具,如JConsole、VisualVM(注意,從Oracle JDK 9開始,VisualVM已經不再包含在JDK安裝包中)等。這些工具具體使用起來相對比較直觀,直接連線到Java程序,然後就可以在圖形化介面裡掌握記憶體使用情況
- 也可以使用命令列工具進行執行時查詢,如jstat和jmap等工具都提供了一些選項,可以檢視堆、方法區等使用資料。
- 或者,也可以使用jmap等提供的命令,生成堆轉儲(Heap Dump)檔案,然後利用jhat或Eclipse MAT等堆轉儲分析工具進行詳細分析。
- 如果你使用的是Tomcat、Weblogic等Java EE伺服器,這些伺服器同樣提供了記憶體管理相關的功能。
- 另外,從某種程度上來說,GC日誌等輸出,同樣包含著豐富的資訊。
jdk自帶實用工具 https://www.jianshu.com/p/36ac6403df44
- 為什麼CMS兩次標記時要 stop the world?
- 兩次標記為了安全回收物件,虛擬機器在特定的指令位置設定了“安全點”,當執行到該位置時,程式就會停頓,暫停當前執行的所有使用者執行緒,進而進行標記清除
- 特定指令的位置:
- 迴圈末尾
- 方法返回前/呼叫方法call指令之後
- 可能拋異常的地方
13. 談談你的GC調優思路?
- 從效能的角度看,通常關注三個方面,記憶體佔用(footprint)、延時
(latency)和吞吐量(throughput) - 基本的調優思路可以總結為:
- 確定調優目標,比如服務停頓嚴重,希望GC暫停儘量控制在200ms以內,並且保證一定標準的吞吐量
- 通過jstat等工具檢視GC等相關狀態,可以開啟GC日誌,或者是利用作業系統提供的診斷工具等
- 選擇的GC型別是否符合我們的應用特徵,如CMS和G1都是更側重於低延遲的GC選項。
- 根據實際情況調整新生代老年代比例和大小等
- 思路歸納為:
- 選擇合適的垃圾收集器;
- 使用jdk工具分析GC狀況;
- 調整gc引數
14. Java記憶體模型中的happen-before是什麼?
- Happen-before關係,是Java記憶體模型中保證多執行緒操作可見性的機制
- 它的具體表現形式,包括但遠不止是我們直覺中的synchronized、volatile、lock操作順序等方面,例如:
- 執行緒內執行的每個操作,都保證happen-before後面的操作,這就保證了基本的程式順序規則,這是開發者在書寫程式時的基本約定。
- 對於volatile變數,對它的寫操作,保證happen-before在隨後對該變數的讀取操作。
- 對於一個鎖的解鎖操作,保證happen-before加鎖操作。
- 物件構建完成,保證happen-before於fnalizer的開始動作。
- 甚至是類似執行緒內部操作的完成,保證happen-before其他Thread.join()的執行緒等。
15. Java程式執行在Docker等容器環境有哪些新問題?
- Docker其記憶體、CPU等資源限制是通過CGroup(Control Group)實現的,早期的JDK版本(8u131之前)並不能識別這些限制,進而會導致一些基礎問題:
- 如果未配置合適的JVM堆和元資料區、直接記憶體等引數,Java就有可能試圖使用超過容器限制的記憶體,最終被容器OOM kill,或者自身發生OOM。
- 錯誤判斷了可獲取的CPU資源,例如,Docker限制了CPU的核數,JVM就可能設定不合適的GC並行執行緒數等
- 從應用打包、釋出等角度出發,JDK自身就比較大,生成的映象就更為臃腫,當我們的映象非常多的時候,映象的儲存等開銷就比較明顯了
- 雖然看起來Docker之類容器和虛擬機器非常相似,例如,它也有自己的shell,能獨立安裝軟體包,執行時與其他容器互不干擾。但是,如果深入分析你會發現,Docker並不是一種完全的虛擬化技術,而更是一種輕量級的隔離技術。
- 容器雖然省略了虛擬作業系統的開銷,實現了輕量級的目標,但也帶來了額外複雜性,它限制對於應用不是透明的,需要使用者理解Docker的新行為。
針對這種情況,JDK 9中引入了一些實驗性的引數,以方便Docker和Java“溝通”,例如針對記憶體限制,可以使用下面的引數設定
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap如果你可以切換到JDK 10或者更新的版本,問題就更加簡單了。Java對容器(Docker)的支援已經比較完善,預設就會自適應各種資源限制和實現差異。前面提到的實驗性參
數“UseCGroupMemoryLimitForHeap”已經被標記為廢棄。與此同時,新增了引數用以明確指定CPU核心的數目。-XX:ActiveProcessorCount=N
- 如果實踐中發現有問題,也可以使用“-XX:-UseContainerSupport”,關閉Java的容器支援特性
- 如果只能使用老版本
- 明確設定堆、元資料區等記憶體區域大小,保證Java程序的總大小可控
- 能在環境中,這樣限制容器記憶體:
- $ docker run -it --rm --name yourcontainer -p 8080:8080 -m 800M repo/your-java-container:openjdk
- 額外配置下面的環境變數,直接指定JVM堆大小。
- -e JAVA_OPTIONS='-Xmx300m'
- 明確配置GC和JIT並行執行緒數目,以避免二者佔用過多計算資源。
- -XX:ParallelGCThreads -XX:CICompilerCount
- 建議配置下面引數,明確告知JVM系統記憶體限額。
- -XX:MaxRAM=
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
- -XX:MaxRAM=
- 也可以指定Docker執行引數,例如:
- --memory-swappiness=0
16. 你瞭解Java應用開發中的注入攻擊嗎?
- 注入式(Inject)攻擊是一類非常常見的攻擊方式,其基本特徵是程式允許攻擊者將不可信的動態內容注入到程式中,並將其執行,這就可能完全改變最初預計的執行過程,產生惡意效果
- 最常見的SQL注入攻擊。一個典型的場景就是Web系統的使用者登入功能,根據使用者輸入的使用者名稱和密碼,我們需要去後端資料庫核實資訊。
- 假設應用邏輯是,後端程式利用介面輸入動態生成類似下面的SQL,然後讓JDBC執行。
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 “” = “”
- 第二,作業系統命令注入。
- 第三,XML注入攻擊。
- 解決方法,例如針對SQL注入:
- 在資料輸入階段,填補期望輸入和可能輸入之間的鴻溝。可以進行輸入校驗,限定什麼型別的輸入是合法的,例如,不允許輸入標點符號等特殊字元,或者特定結構的輸入。
- 在Java應用進行資料庫訪問時,如果不用完全動態的SQL,而是利用PreparedStatement,可以有效防範SQL注入。不管是SQL注入,還是OS命令注入,程式利用字串拼接
生成執行邏輯都是個可能的風險點! - 在資料庫層面,如果對查詢、修改等許可權進行了合理限制,就可以在一定程度上避免被注入刪除等高破壞性的程式碼。
17. 後臺服務出現明顯“變慢”,談談你的診斷思路?
- 理清問題的症狀,這更便於定位具體的原因,有以下一些思路:
- 問題可能來自於Java服務自身,也可能僅僅是受系統裡其他服務的影響。初始判斷可以先確認是否出現了意外的程式錯誤,例如檢查應用本身的錯誤日誌。
- 監控Java服務自身,例如GC日誌裡面是否觀察到Full GC等惡劣情況出現,或者是否Minor GC在變長等;利用jstat等工具,獲取記憶體使用的統計資訊也是個常用手段;利用jstack等工具檢查是否出現死鎖等
- 如果還不能確定具體問題,對應用進行Profling也是個辦法,但因為它會對系統產生侵入性,如果不是非常必要,大多數情況下並不建議在生產系統進行。
- 大致可以分幾個方面檢查
- 系統異常報錯
- JVM配置不合理或記憶體不夠,FULL gc頻繁
- 檢查變慢介面具體程式碼,可能由於資料量增多,sql沒有優化查詢變慢
- 呼叫第三方介面變慢