Java 內(nèi)存安全問題的注意事項(xiàng)
前言
Java在內(nèi)存管理方面是要比C/C++更方便的,不需要為每一個(gè)對(duì)象編寫釋放內(nèi)存的代碼,JVM虛擬機(jī)將為我們選擇合適的時(shí)間釋放內(nèi)存空間,使得程序不容易出現(xiàn)內(nèi)存泄漏和溢出的問題
不過,也正是因?yàn)镴ava把內(nèi)存控制的權(quán)利交給了Java虛擬機(jī),一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問題,如果不了解虛擬機(jī)是怎么使用內(nèi)存的,那排查錯(cuò)誤將會(huì)成為一項(xiàng)異常艱難的工作
下面先看看JVM如何管理內(nèi)存的
內(nèi)存管理
根據(jù)Java虛擬機(jī)規(guī)范(第3版) 的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行內(nèi)存數(shù)據(jù)區(qū)域:
- 線程隔離數(shù)據(jù)區(qū):
- 程序計(jì)數(shù)器: 當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器
- 虛擬機(jī)棧: 里面的元素叫棧幀,存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等,方法被調(diào)用到執(zhí)行完成的過程對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。
- 本地方法棧: 和虛擬機(jī)棧的區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法,本地方法棧為虛擬機(jī)使用到的本地Native方法服務(wù)。
- 線程共享數(shù)據(jù)區(qū):
- 方法區(qū): 可以描述為堆的一個(gè)邏輯部分,或者說使用永久代來實(shí)現(xiàn)方法區(qū)。存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 堆: 唯一目的就是存放對(duì)象的實(shí)例,是垃圾回收管理器的主要區(qū)域,分為Eden、From/To Survivor空間。
Java各版本內(nèi)存管理改進(jìn)
下圖中永久代理解為堆的邏輯區(qū)域,移除永久代的工作從JDK7就已經(jīng)開始了,部分永久代中的數(shù)據(jù)(常量池)在JDK7中就已經(jīng)轉(zhuǎn)移到了堆中,JDK8中直接去除了永久代,方法區(qū)中的數(shù)據(jù)大部分被移到堆里面,還剩下一些元數(shù)據(jù)被保存在元空間里
內(nèi)存溢出
- 內(nèi)存泄露Memory Leak: 申請(qǐng)的內(nèi)存空間沒有及時(shí)釋放,導(dǎo)致后續(xù)程序里這塊內(nèi)容永遠(yuǎn)被占用。
- 內(nèi)存溢出Out Of Memory: 要求的內(nèi)存超過了系統(tǒng)所能提供的
運(yùn)行時(shí)數(shù)據(jù)區(qū)域的常見異常
在JVM中,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域都有發(fā)生OOM異常的可能。
堆內(nèi)存溢出
不斷的創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象。
public class HeapOOM { static class ObjectInHeap{ } public static void main(String[] args) { List<ObjectInHeap> list = new ArrayList(); while (true) { list.add(new ObjectInHeap()); } } }
棧溢出
單個(gè)線程下不斷擴(kuò)大棧的深度引起棧溢出。
public class StackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { StackSOF sof = new StackSOF(); try { sof.stackLeak(); } catch (Throwable e) { System.out.println("Stack Length: " + sof.stackLength); throw e; } } }
循環(huán)的創(chuàng)建線程,達(dá)到最大棧容量。
public class StackOOM { private void dontStop() { while (true) { } } public void stackLeadByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { StackOOM stackOOM = new StackOOM(); stackOOM.stackLeadByThread(); } }
運(yùn)行時(shí)常量池溢出
不斷的在常量池中新建String,并且保持引用不釋放。
public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持著常量池的引用,避免Full GC回收常量池 List<String> list = new ArrayList<String>(); int i = 0; while (true) { // intern()方法使String放入常量池 list.add(String.valueOf(i++).intern()); } } }
方法區(qū)溢出
借助CGLib直接操作字節(jié)碼運(yùn)行時(shí)產(chǎn)生大量的動(dòng)態(tài)類,最終撐爆內(nèi)存導(dǎo)致方法區(qū)溢出。
public class MethodAreaOOM { static class ObjectInMethod { } public static void main(final String[] args) { // 借助CGLib實(shí)現(xiàn) while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(ObjectInMethod.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, objects); } }); enhancer.create(); } } }
元空間溢出
助CG Lib運(yùn)行時(shí)產(chǎn)生大量動(dòng)態(tài)類,唯一的區(qū)別在于運(yùn)行環(huán)境修改為Java 1.8,設(shè)置-XX:MaxMetaspaceSize參數(shù),便可以收獲java.lang.OutOfMemoryError: Metaspace這一報(bào)錯(cuò)
本機(jī)直接內(nèi)存溢出
直接申請(qǐng)分配內(nèi)存(實(shí)際上并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配,于是拋出異常)
public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
常見案例
在工作中一般會(huì)遇到有以下幾種情況導(dǎo)致內(nèi)存問題
- 傳輸數(shù)據(jù)量過大
因?yàn)閭鬏敂?shù)量過大、或一些極端情況導(dǎo)致代碼中間結(jié)果對(duì)象數(shù)據(jù)量過大,過大的數(shù)據(jù)量撐爆內(nèi)存
- 查詢出大量對(duì)象
這個(gè)多為SQL語句設(shè)置問題,SQL未設(shè)置分頁,用戶一次查詢數(shù)據(jù)量過大、頻繁查詢SQL導(dǎo)致內(nèi)存堆積、或是未作判空處理導(dǎo)致WHERE條件為空查詢出超大數(shù)據(jù)量等
- 接口性能問題導(dǎo)致
這類為外部接口性能較慢,占用內(nèi)存較大,并且短時(shí)間內(nèi)高QPS導(dǎo)致的,導(dǎo)致服務(wù)內(nèi)存不足,線程堆積或掛起進(jìn)而出現(xiàn)FullGC
- 元空間問題
使用了大量的反射代碼,Java字節(jié)碼存取器生成的類不斷生成
問題排查
使用jmap分析內(nèi)存泄漏
1.生成dump文件
jmap -dump:format=b,file=/xx/xx/xx.hprof pid
2.dump文件下載到本地
3.dump文件分析
可以使用MAT,MAT可作為Eclipse插件或一個(gè)獨(dú)立軟件使用,MAT是一個(gè)高性能、具備豐富功能的Java堆內(nèi)存分析工具,主要用來排查內(nèi)存泄漏和內(nèi)存浪費(fèi)的問題。
使用MAT打開上一部后綴名.hprof的dump文件
- Histogram:直方圖,各個(gè)類的實(shí)例,包括個(gè)數(shù)和大小,可以查看類引用和被引用的路徑。
- Dominator Tree:支配圖,列出所有線程和線程下面的那些對(duì)象占用的空間。
- Top Consumers:通過圖形列出消耗內(nèi)存多的實(shí)例。
- Leak Suspects:MAT自動(dòng)分析的內(nèi)存泄漏報(bào)表
可以用這個(gè)工具分析出什么對(duì)象什么線程占用內(nèi)存空間較大,對(duì)象是被什么引用的,線程內(nèi)有哪些資源占用很高
以運(yùn)行時(shí)常量池溢出為例
打開Histogram類實(shí)例表
Objects是類的對(duì)象的數(shù)量;Shallow是對(duì)象本身占用內(nèi)存大小、不包含其他引用;
Retained是對(duì)象自己的Shallow加上直接或間接訪問到對(duì)象的Shallow之和,也可以說是GC之后可以回收的內(nèi)存總和
從圖中可以看出運(yùn)行時(shí)常量池溢出的情況,產(chǎn)生了大量的String和char[]實(shí)例
在char[]上右鍵可以得到上圖所有char[]對(duì)象的被引用路徑,可以看出這些char數(shù)組都是以String的形式存在ArrayList中,并且是由main這個(gè)線程運(yùn)行的
可以看出是main線程中新建了一個(gè)數(shù)組,其中存了32w+個(gè)長度為6的char數(shù)組組成的String造成的內(nèi)存溢出
關(guān)于MAT的詳細(xì)使用可以從MAT官方教程學(xué)習(xí)更多
以上就是Java 內(nèi)存安全問題的注意事項(xiàng)的詳細(xì)內(nèi)容,更多關(guān)于Java 內(nèi)存安全問題的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Java DWR內(nèi)存泄漏問題解決方案
- macOS上使用gperftools定位Java內(nèi)存泄漏問題及解決方案
- Java內(nèi)存模型可見性問題相關(guān)解析
- Java內(nèi)存泄漏問題處理方法經(jīng)驗(yàn)總結(jié)
- 解決Java導(dǎo)入excel大量數(shù)據(jù)出現(xiàn)內(nèi)存溢出的問題
- 完美解決java讀取大文件內(nèi)存溢出的問題
- 詳解Java中synchronized關(guān)鍵字的死鎖和內(nèi)存占用問題
- 解析Java的JNI編程中的對(duì)象引用與內(nèi)存泄漏問題
- JAVA程序內(nèi)存溢出問題原因分析
- Java中典型的內(nèi)存泄露問題和解決方法
- 基于Java 數(shù)組內(nèi)存分配的相關(guān)問題
相關(guān)文章
JDBC中使用Java8的日期LocalDate和LocalDateTime操作mysql、postgresql
這篇文章主要給大家介紹了關(guān)于JDBC中如何使用Java8的日期LocalDate和LocalDateTime的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09SpringBoot配置Spring?Security的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot配置Spring?Security的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10詳解SpringBoot 發(fā)布ApplicationEventPublisher和監(jiān)聽ApplicationEvent事
這篇文章主要介紹了詳解SpringBoot 發(fā)布ApplicationEventPublisher和監(jiān)聽ApplicationEvent事件,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-06-06SpringBoot?Loki安裝簡介及實(shí)戰(zhàn)思路
這篇文章主要為大家介紹了SpringBoot?Loki安裝簡介及實(shí)戰(zhàn)思路詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪的相關(guān)資料2022-11-11MyBatis-Plus實(shí)現(xiàn)連表查詢的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于MyBatis-Plus實(shí)現(xiàn)連表查詢的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-01-01基于java實(shí)現(xiàn)websocket代碼示例
這篇文章主要介紹了基于java實(shí)現(xiàn)websocket代碼示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12Java Validation方法入?yún)⑿r?yàn)實(shí)現(xiàn)過程解析
這篇文章主要介紹了Java Validation方法入?yún)⑿r?yàn)實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11