1.堆溢出
java堆用于存放程序運(yùn)行期間所產(chǎn)生的對象實(shí)例人断,因此當(dāng)對象足夠多的時(shí)候,就會(huì)產(chǎn)生堆內(nèi)存溢出朝蜘,異常堆棧信息為”java.lang.OurOfMemoryError : java heap space“恶迈。這類異常程序代碼非常簡單,這里不多贅述谱醇。
2.棧溢出
java虛擬機(jī)中存在虛擬機(jī)棧和本地方法棧暇仲,在這兩種棧中可能出現(xiàn)兩種異常:
1.當(dāng)線程請求的棧深度大于虛擬機(jī)所允許的最大深度,會(huì)拋出“java.lang.StackOverflowError”
一般出現(xiàn)該異常是由于在遞歸調(diào)用中方法調(diào)用層數(shù)大多枣抱,致使棧深度超過1000~2000
通過使用-Xss參數(shù)減少棧內(nèi)存容量或定義大量本地變量增大棧幀中本地變量表長度都會(huì)使拋出異常時(shí)的棧深度降低
2.當(dāng)虛擬機(jī)擴(kuò)展棧時(shí)無法申請到足夠的內(nèi)存空間熔吗,會(huì)拋出“java.lang.OurOfMemoryError”
新建大量線程可能會(huì)導(dǎo)致虛擬機(jī)沒有足夠空間分配給新建線程辆床,這時(shí)會(huì)拋出“java.lang.OurOfMemoryError:unable to create new native thread”佳晶。
進(jìn)程內(nèi)存 = 最大堆容量(Xmx設(shè)定) + 最大方法區(qū)容量(永久代,MaxPermSize) + 程序計(jì)數(shù)器(很小可忽略) + 棧容量
可見讼载,若每個(gè)線程的棧容量越大轿秧,那么可建立的線程就越少,建立線程時(shí)就越容易出現(xiàn)內(nèi)存不足的情況咨堤。因此解決這類問題可以考慮減少最大堆或減少棧容量來換取更多的線程菇篡,這點(diǎn)與我們的常識(shí)相反。
3.方法區(qū)和運(yùn)行時(shí)常量池溢出
3.1.方法區(qū)
方法區(qū)主要用于存放Class的基本信息一喘,如類名驱还、訪問修飾符嗜暴、常量池、字段描述议蟆、方法描述等信息闷沥,此外運(yùn)行時(shí)常量池也是方法區(qū)的一部分。
可以發(fā)現(xiàn)咐容,當(dāng)需要加載大量的類或生成大量常量時(shí)舆逃,就會(huì)出現(xiàn)方法區(qū)溢出,但是根據(jù)java版本的不同以及虛擬機(jī)的選擇戳粒,異常信息也會(huì)有所不同路狮,其主要差別來源于方法區(qū)的實(shí)現(xiàn),下面我們介紹永久代與元空間蔚约。
3.2 永久代與元空間
永久帶
方法區(qū)是java虛擬機(jī)規(guī)范所定義的一個(gè)內(nèi)存區(qū)域奄妨,而永久代是在hotspot虛擬機(jī)中方法區(qū)的一種實(shí)現(xiàn)方式,在不同的虛擬機(jī)中有不同的實(shí)現(xiàn)炊琉,例如JRockit虛擬機(jī)就沒有永久代的概念展蒂。
當(dāng)產(chǎn)生大量類的時(shí)候,永久帶溢出則會(huì)拋出"java.lang.OutOfMemoryError: PermGen space"苔咪。
元空間
從JDK1.7開始锰悼,hotSpot就開始逐步轉(zhuǎn)移永久帶到別的內(nèi)存空間
JDK 1.7 和 1.8 將字符串常量、類的靜態(tài)變量由永久代轉(zhuǎn)移到堆中团赏,而Class的基本信息則轉(zhuǎn)移到元空間當(dāng)中箕般,當(dāng)產(chǎn)生大量類的時(shí)候會(huì)拋出"java.lang.OutOfMemoryError: Metaspace"。
元空間本質(zhì)上也是一種java虛擬機(jī)對方法區(qū)的實(shí)現(xiàn)舔清,但是元空間不在虛擬機(jī)中丝里,而是使用本地內(nèi)存,所以其最大可利用空間是整個(gè)系統(tǒng)內(nèi)存的可用空間体谒。但類的元數(shù)據(jù)也可以分配在本地內(nèi)存以外的空間杯聚,當(dāng)本地內(nèi)存溢出時(shí),溢出的內(nèi)存會(huì)交換到硬盤空間(Linux系統(tǒng)中稱為交換區(qū))當(dāng)中抒痒,這樣就有效的避免了OOM問題幌绍。默認(rèn)情況下,類的元數(shù)據(jù)僅受到本地內(nèi)存的限制故响,也可以通過在命令行設(shè)定-XX:MaxMetaspaceSize的數(shù)值對其進(jìn)行限制傀广。
1、-XX:MetaspaceSize彩届,class metadata的初始空間配額伪冰,以bytes為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載樟蠕,同時(shí)GC會(huì)對該值進(jìn)行調(diào)整:如果釋放了大量的空間贮聂,就適當(dāng)?shù)慕档驮撝悼扛蹋蝗绻尫帕撕苌俚目臻g,那么在不超過MaxMetaspaceSize(如果設(shè)置了的話)吓懈,適當(dāng)?shù)奶岣咴撝怠?br>
2病往、 -XX:MaxMetaspaceSize,可以為class metadata分配的最大空間骄瓣。默認(rèn)是沒有限制的停巷。
3、-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio
此外榕栏,元空間在達(dá)到-XX:MetaspaceSize設(shè)定的數(shù)據(jù)以后畔勤,會(huì)進(jìn)行一次FullGC,卸載那些類加載器已死的類扒磁,然后根據(jù)釋放的空間調(diào)整MetaspaceSize庆揪。如果僅釋放了少量空間,那么MetaspaceSize會(huì)適當(dāng)增大妨托,如果釋放了大量空間缸榛,則MetaspaceSize會(huì)適當(dāng)減小,我們可以通過設(shè)置-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio來調(diào)控這一過程的觸發(fā)兰伤。
元空間可以分割為多塊元空間内颗,一個(gè)類加載器都對應(yīng)了一塊元空間,類的元數(shù)據(jù)與其對應(yīng)類加載器的生命周期是一致的敦腔,若該類加載器是存活的均澳,那么其加載類的元數(shù)據(jù)則也不可回收,若類加載器死亡符衔,那么該類加載器對應(yīng)的元空間即可被回收找前。
元空間的意義:
1、字符串存在永久代中判族,容易出現(xiàn)性能問題和內(nèi)存溢出躺盛。JDK1.7以后將字符串存在java堆當(dāng)中。
2形帮、類及方法的信息等比較難確定其大小槽惫,因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出沃缘,太大則容易導(dǎo)致老年代溢出躯枢。
3则吟、永久代會(huì)為 GC 帶來不必要的復(fù)雜度槐臀,并且回收效率偏低。
4氓仲、Oracle 可能會(huì)將HotSpot 與 JRockit 合二為一水慨。
4.本機(jī)直接內(nèi)存溢出(這塊不太懂得糜,暫放)
DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定晰洒,則默認(rèn)與Java堆的最大值相同朝抖。
直接內(nèi)存的大小與操作系統(tǒng)相關(guān),如32位Windows平臺(tái)內(nèi)存總大小為2GB谍珊,其中劃給java堆1.6GB治宣,那么直接內(nèi)存最多也只能從剩余的0.4GB中劃分。
直接內(nèi)存不能自己主動(dòng)通知虛擬機(jī)進(jìn)行GC砌滞,因此它僅能等待老年代滿了以后觸發(fā)的FullGC對其進(jìn)行清理侮邀。如果直接內(nèi)存溢出時(shí)老年代還沒有觸發(fā)FullGC,那么它只能在拋出內(nèi)存異常時(shí)先catch住并在catch中執(zhí)行System.gc()贝润,若此時(shí)虛擬機(jī)不響應(yīng)(如打開了-XX:+DisableExplicitGC)绊茧,那么還是會(huì)拋出內(nèi)存溢出異常。
若在內(nèi)存溢出時(shí)在Heap Dump中僅顯示"java.lang.OutOfMemoryError"沒有特征的異常打掘,且OOM后Dump文件很谢贰(因?yàn)橥且驗(yàn)槔夏甏€沒有達(dá)到觸發(fā)FullGC的條件),程序中又直接或間接使用了NIO尊蚁,那么可能是這個(gè)問題亡笑。
5.Android中常見的內(nèi)存泄露情景
5.1.單例造成的內(nèi)存泄露
單例的生命周期與應(yīng)用一樣長,因此當(dāng)創(chuàng)建出來后就會(huì)一直存在横朋,如果在創(chuàng)建的時(shí)候持有了某個(gè)對象的引用况芒,就會(huì)一直持有它導(dǎo)致內(nèi)存泄露。如下面的例子叶撒,在創(chuàng)建ActivityManager單例的時(shí)候我們傳入了一個(gè)上下文Context參數(shù)绝骚,包含了一個(gè)對Activity的引用,那么在這種情況下就會(huì)造成該Activity無法回收祠够,發(fā)生內(nèi)存泄露
public class ActivityManager
{
private Context mContext;
private static ActivityManager manager;
private ActivityManager(Context mContext)
{
this.mContext = mContext; //持有了Activity的Context
}
public static ActivityManager getInstance(Context mContext)
{
if (manager!=null)
{
manager = new ActivityManager(mContext);
}
return manager;
}
}
修正方案為压汪,通過傳入的Activity的Context參數(shù)獲取到ApplicationContext,這樣這個(gè)單例持有的就是應(yīng)用本身的引用古瓤,本身單例就與應(yīng)用生命周期相同止剖,因此就不會(huì)有內(nèi)存泄露發(fā)生。
5.2.Handler造成的內(nèi)存泄漏
public class DemoActivity extends AppCompatActivity
{
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...更新UI操作
}
};
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
initDatas();
}
private void initDatas()
{
//...子線程獲取數(shù)據(jù)落君,在主線程中更新UI
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
Handler是Activity中的非靜態(tài)匿名內(nèi)部類穿香,因此創(chuàng)建的mHandler會(huì)持有外部對象DemoActivity的引用,looper會(huì)在Activity同一線程不斷循環(huán)查詢消息并進(jìn)行處理绎速,當(dāng)Activity退出時(shí)如果還有未處理完成的消息皮获,消息隊(duì)列會(huì)一直持有handler的引用,而handler又一直存在并持有Activity的引用纹冤,導(dǎo)致Activity無法被回收洒宝,導(dǎo)致內(nèi)存泄露购公。
public class DemoActivity extends AppCompatActivity
{
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null)
{
//...更新UI操作
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
initDatas();
}
private void initDatas()
{
//...子線程獲取數(shù)據(jù),在主線程中更新UI
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
//移除消息隊(duì)列中所有消息和所有的Runnable
mHandler.removeCallbacksAndMessages(null);
}
}
5.3.匿名內(nèi)部類造成的內(nèi)存泄漏(實(shí)際上是第二點(diǎn)的泛化情況)
匿名內(nèi)部類會(huì)持有外部對象(如Activity)的引用雁歌,當(dāng)這個(gè)匿名內(nèi)部類對象生命周期與Activity一致時(shí)不會(huì)出現(xiàn)問題宏浩,但是當(dāng)匿名內(nèi)部類生命周期超出外部對象(如啟動(dòng)了一個(gè)新的線程),則會(huì)出現(xiàn)外部對象無法被回收靠瞎,而導(dǎo)致內(nèi)存泄露比庄。
例如AscynTask:
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
}
解決方案就是將匿名內(nèi)部類改為一個(gè)靜態(tài)內(nèi)部類。
private static class NimbleTask extends AsyncTask<Void, Void, Void> {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}
void startAsyncTask() {
new NimbleTask().execute();
}
如果一定要持有外部對象乏盐,請將其設(shè)置為弱引用印蔗。
另外一種解決方案是在將其生命周期與外部對象同步,如匿名內(nèi)部類啟動(dòng)了一個(gè)新的線程丑勤,那么我們在外部對象被銷毀時(shí)終止該線程
private Thread thread;
@Override
public void onDestroy() {
super.onDestroy();
if (thread != null) {
thread.interrupt();
}
}
void spawnThread() {
thread = new Thread() {
@Override public void run() {
while (!isInterrupted()) {
}
}
}
thread.start();
}
5.4.靜態(tài)變量導(dǎo)致的內(nèi)存泄露
public class DemoActivity extends AppCompatActivity
{
private static Context mContext;
@Override
private void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.mContext = this; //靜態(tài)變量與類的生命周期相同华嘹,但持有該Activity實(shí)例的引用,造成泄漏
}
}
public class DemoActivity extends AppCompatActivity
{
private static View sView;
@Override
private void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sView = new View(this); //靜態(tài)變量與類的生命周期相同法竞,但持有該Activity實(shí)例的引用耙厚,造成泄漏
}
}
該問題主要為類聲明的靜態(tài)變量持有了某個(gè)實(shí)例的引用,若類不被卸載岔霸,則該靜態(tài)變量不會(huì)被回收薛躬,同樣的,其持有的實(shí)例引用也不會(huì)被回收呆细,造成內(nèi)存泄露型宝。
5.5.雜七雜八的東西
在Activity生命周期技術(shù)的時(shí)候完成結(jié)束動(dòng)畫、置空bitmap絮爷、關(guān)閉流Stream趴酣、關(guān)閉游標(biāo)Cursor、注銷BroadcastReceiver等操作坑夯。
5.6.小結(jié)
內(nèi)存泄露的核心實(shí)際上都是由于某長生命周期對象持有了較短生命周期對象的引用岖寞,所以需要著重注意單例、靜態(tài)變量柜蜈、會(huì)啟動(dòng)長周期任務(wù)的匿名內(nèi)部類等長周期對象仗谆,注意不要讓其持有Activity實(shí)例的引用。