【java虚拟机】内存溢出异常

1.堆溢出

java堆用于存放程序运行期间所产生的对象实例,因此当对象足够多的时候,就会产生堆内存溢出,异常堆栈信息为”java.lang.OurOfMemoryError : java heap space“。这类异常程序代码非常简单,这里不多赘述。

2.栈溢出

java虚拟机中存在虚拟机栈和本地方法栈,在这两种栈中可能出现两种异常:

1.当线程请求的栈深度大于虚拟机所允许的最大深度,会抛出“java.lang.StackOverflowError”

一般出现该异常是由于在递归调用中方法调用层数大多,致使栈深度超过1000~2000
通过使用-Xss参数减少栈内存容量定义大量本地变量增大栈帧中本地变量表长度都会使抛出异常时的栈深度降低

2.当虚拟机扩展栈时无法申请到足够的内存空间,会抛出“java.lang.OurOfMemoryError”

新建大量线程可能会导致虚拟机没有足够空间分配给新建线程,这时会抛出“java.lang.OurOfMemoryError:unable to create new native thread”。

进程内存 = 最大堆容量(Xmx设定) + 最大方法区容量(永久代,MaxPermSize) + 程序计数器(很小可忽略) + 栈容量

可见,若每个线程的栈容量越大,那么可建立的线程就越少,建立线程时就越容易出现内存不足的情况。因此解决这类问题可以考虑减少最大堆或减少栈容量来换取更多的线程,这点与我们的常识相反。

3.方法区和运行时常量池溢出

3.1.方法区

方法区主要用于存放Class的基本信息,如类名、访问修饰符、常量池、字段描述、方法描述等信息,此外运行时常量池也是方法区的一部分。
可以发现,当需要加载大量的类或生成大量常量时,就会出现方法区溢出,但是根据java版本的不同以及虚拟机的选择,异常信息也会有所不同,其主要差别来源于方法区的实现,下面我们介绍永久代与元空间。

3.2 永久代与元空间

永久带

方法区是java虚拟机规范所定义的一个内存区域,而永久代是在hotspot虚拟机中方法区的一种实现方式,在不同的虚拟机中有不同的实现,例如JRockit虚拟机就没有永久代的概念。
当产生大量类的时候,永久带溢出则会抛出"java.lang.OutOfMemoryError: PermGen space"。

元空间

从JDK1.7开始,hotSpot就开始逐步转移永久带到别的内存空间
JDK 1.7 和 1.8 将字符串常量、类的静态变量由永久代转移到堆中,而Class的基本信息则转移到元空间当中,当产生大量类的时候会抛出"java.lang.OutOfMemoryError: Metaspace"。
元空间本质上也是一种java虚拟机对方法区的实现,但是元空间不在虚拟机中,而是使用本地内存,所以其最大可利用空间是整个系统内存的可用空间。但类的元数据也可以分配在本地内存以外的空间,当本地内存溢出时,溢出的内存会交换到硬盘空间(Linux系统中称为交换区)当中,这样就有效的避免了OOM问题。默认情况下,类的元数据仅受到本地内存的限制,也可以通过在命令行设定-XX:MaxMetaspaceSize的数值对其进行限制。

1、-XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
2、 -XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
3、-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio

此外,元空间在达到-XX:MetaspaceSize设定的数据以后,会进行一次FullGC,卸载那些类加载器已死的类,然后根据释放的空间调整MetaspaceSize。如果仅释放了少量空间,那么MetaspaceSize会适当增大,如果释放了大量空间,则MetaspaceSize会适当减小,我们可以通过设置-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio来调控这一过程的触发。

元空间可以分割为多块元空间,一个类加载器都对应了一块元空间,类的元数据与其对应类加载器的生命周期是一致的,若该类加载器是存活的,那么其加载类的元数据则也不可回收,若类加载器死亡,那么该类加载器对应的元空间即可被回收。

元空间的意义:
1、字符串存在永久代中,容易出现性能问题和内存溢出。JDK1.7以后将字符串存在java堆当中。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

4.本机直接内存溢出(这块不太懂,暂放)

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值相同。

直接内存的大小与操作系统相关,如32位Windows平台内存总大小为2GB,其中划给java堆1.6GB,那么直接内存最多也只能从剩余的0.4GB中划分。

直接内存不能自己主动通知虚拟机进行GC,因此它仅能等待老年代满了以后触发的FullGC对其进行清理。如果直接内存溢出时老年代还没有触发FullGC,那么它只能在抛出内存异常时先catch住并在catch中执行System.gc(),若此时虚拟机不响应(如打开了-XX:+DisableExplicitGC),那么还是会抛出内存溢出异常。

若在内存溢出时在Heap Dump中仅显示"java.lang.OutOfMemoryError"没有特征的异常,且OOM后Dump文件很小(因为往往是因为老年代还没有达到触发FullGC的条件),程序中又直接或间接使用了NIO,那么可能是这个问题。

5.Android中常见的内存泄露情景

5.1.单例造成的内存泄露
单例的生命周期与应用一样长,因此当创建出来后就会一直存在,如果在创建的时候持有了某个对象的引用,就会一直持有它导致内存泄露。如下面的例子,在创建ActivityManager单例的时候我们传入了一个上下文Context参数,包含了一个对Activity的引用,那么在这种情况下就会造成该Activity无法回收,发生内存泄露

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参数获取到ApplicationContext,这样这个单例持有的就是应用本身的引用,本身单例就与应用生命周期相同,因此就不会有内存泄露发生。
5.2.Handler造成的内存泄漏

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()
    {
        //...子线程获取数据,在主线程中更新UI
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

Handler是Activity中的非静态匿名内部类,因此创建的mHandler会持有外部对象DemoActivity的引用,looper会在Activity同一线程不断循环查询消息并进行处理,当Activity退出时如果还有未处理完成的消息,消息队列会一直持有handler的引用,而handler又一直存在并持有Activity的引用,导致Activity无法被回收,导致内存泄露。

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()
    {
        //...子线程获取数据,在主线程中更新UI
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //移除消息队列中所有消息和所有的Runnable
        mHandler.removeCallbacksAndMessages(null);
    }
}

5.3.匿名内部类造成的内存泄漏(实际上是第二点的泛化情况)
匿名内部类会持有外部对象(如Activity)的引用,当这个匿名内部类对象生命周期与Activity一致时不会出现问题,但是当匿名内部类生命周期超出外部对象(如启动了一个新的线程),则会出现外部对象无法被回收,而导致内存泄露。
例如AscynTask:

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

解决方案就是将匿名内部类改为一个静态内部类。

private static class NimbleTask extends AsyncTask<Void, Void, Void> {
    @Override protected Void doInBackground(Void... params) {
        while(true);
    }
}

void startAsyncTask() {
    new NimbleTask().execute();
}

如果一定要持有外部对象,请将其设置为弱引用。
另外一种解决方案是在将其生命周期与外部对象同步,如匿名内部类启动了一个新的线程,那么我们在外部对象被销毁时终止该线程

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.静态变量导致的内存泄露

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;         //静态变量与类的生命周期相同,但持有该Activity实例的引用,造成泄漏
    }
}
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);         //静态变量与类的生命周期相同,但持有该Activity实例的引用,造成泄漏
    }
}

该问题主要为类声明的静态变量持有了某个实例的引用,若类不被卸载,则该静态变量不会被回收,同样的,其持有的实例引用也不会被回收,造成内存泄露。
5.5.杂七杂八的东西
在Activity生命周期技术的时候完成结束动画、置空bitmap、关闭流Stream、关闭游标Cursor、注销BroadcastReceiver等操作。
5.6.小结
内存泄露的核心实际上都是由于某长生命周期对象持有了较短生命周期对象的引用,所以需要着重注意单例、静态变量、会启动长周期任务的匿名内部类等长周期对象,注意不要让其持有Activity实例的引用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容