JVM 内存溢出与泄漏

尽管 JVM 提供了自动内存管理的机制,试图降低程序员的开发门槛,确实也实现了这一目标,在日常开发中,我们一般都不需要关心对象的内存释放。JVM 大部分都是使用 trace 算法来判断一个对象是否该被回收,那么 JVM 只能回收那些从 GC roots 不可达的对象。如果我们在使用某些大的对象、集合对象或者一些三方包里的资源,忘记及时释放资源的话,还是会造成 JVM 的内存泄漏或内存浪费的问题。

  • 内存溢出:就是你要求分配的 Java 虚拟机内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
  • 内存泄漏:是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问,该块已分配出来的内存也无法再使用,随着服务器内存的不断消耗,而无法使用的内存越来越多,系统也不能再次将它分配给需要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。

Java 内存泄露

是指你向系统申请分配内存进行使用 (new),可是使用完了以后却不归还 (delete),结果你申请到的那块内存你自己也不能再访问, 该块已分配出来的内存也无法再使用,随着服务器内存的不断消耗,而无法使用的内存越来越多,系统也不能再次将它分配给需要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。

案例

设置软引用

下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。

设计思路是:用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题。在 Android 开发中对于大量图片下载会经常用到。

.....
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
public void addBitmapToCache(String path) {
        // 强引用的 Bitmap 对象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的 Bitmap 对象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加该对象到 Map 中使其缓存
        imageCache.put(path, softBitmap);
    }
 public Bitmap getBitmapByPath(String path) {
        // 从缓存中取软引用的 Bitmap 对象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap对象,如果由于内存不足 Bitmap 被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }

内存溢出

内存溢出是指应用新建一个对象实例时,所需的内存空间大于堆的可用空间。内存溢出的种类较多,一般会在报错日志里看到 OutOfMemoryError 关键字。

系统内存分配

首先看下操作系统如何划分内存给应用系统,其实在 Win 32、Linux 32 的系统中,地址总线为 32 位的理论上应该可以支持 4G 内存空间,但是当你在 Win 32 上设置初始化内存如果达到 2G,就会报错,说这个块空间没法做。

首先默认的 Win32 系统,会按照 50% 比例给予给 Kernel 使用,而另一部分给应用内存,也就是说操作系统内核部分不论是否使用,这一半是不会给你的,而还有 2G 呢,它在系统扩展的部分,也就是并非 Kernel 的部分,有很多静态区域 和字典表的内容,所以要划分一个连续的 2G 内存给 JVM 在 Win 32 上是不可能的,Win 32 提出了一种 Win 32 3G 模式,貌似可以划分 3G 空间,其实它只是将内核部分缩小也就是管理部分缩小,也就是将一部分划分到外部来使用,而且 Win 32 习惯在内存 2G 的位置做一些手脚,让你分配连续 2G 没有可能性,一般来说在 Win 32 平台上,在物理内存足够的情况下给 JVM 划分的空间一般是 1.4 ~ 1.5G 左右,具体数据没有测试过;而 Linux 32 类似于 Win 32 3G 模式,但是它还是一般情况下分布不凌乱的情况下,一般可以给 JVM 划分到 2G 的大小。Linux 32 Hugemem 是一个扩展版本,可以划分更大的空间,但是需要付出一些其他的代价,理论上可以支持到 4G 给应用,也就是 Kenel 是独立 的;Solaris x86-32 和 AIX 32 等系统,也类似于 Linux 32 平台一样。

当你申请一个线程的时候,它的除了线程内部对象的开销外,线程本身的开销,是需要 OS 来调度完成,一般来说,会在 OS 的线程与虚拟机内部有都有一个一一对 应的,但是会根据操作系统不同有所变化,有些可能只有一个,总之 heapSize 外的那部分空间是跑不掉的,它放在哪里呢?就是放在 Stack 中的,所以 上文中的-Xss 就是设置这个的,在 JDK 1.5 以后,每个线程的大小被默认设置为 1M 的 stack 开销,我们习惯将这个开销降低。

常见内存溢出

常见内存溢出种类及分析思路如下:

  • java.lang.OutOfMemoryError: Java heap space。原因:堆中(新生代和老年代)无法继续分配对象了、某些对象的引用长期被持有没有被释放,垃圾回收器无法回收、使用了大量的 Finalizer 对象,这些对象并不在 GC 的回收周期内等。一般堆溢出都是由于内存泄漏引起的,如果确认没有内存泄漏,可以适当通过增大堆内存。
  • java.lang.OutOfMemoryError: GC overhead limit exceeded。原因:垃圾回收器超过 98%的时间用来垃圾回收,但回收不到 2%的堆内存,一般是因为存在内存泄漏或堆空间过小。
  • java.lang.OutOfMemoryError: Metaspace 或 java.lang.OutOfMemoryError: PermGen space。排查思路:检查是否有动态的类加载但没有及时卸载,是否有大量的字符串常量池化,永久代/元空间是否设置过小等。
  • java.lang.OutOfMemoryError: unable to create new native Thread。原因:虚拟机在拓展栈空间时,无法申请到足够的内存空间。可适当降低每个线程栈的大小以及应用整体的线程个数。此外,系统里总体的进程/线程创建总数也受到系统空闲内存和操作系统的限制,请仔细检查。注:这种栈溢出,和 StackOverflowError 不同,后者是由于方法调用层次太深,分配的栈内存不够新建栈帧导致。

此外,还有 Swap 分区溢出、本地方法栈溢出、数组分配溢出等 OutOfMemoryError 类型。

内存溢出工具

使用非常多的工具区检测 Java 的内存如:jstat(只能看 HeapSize 和 PermSize)、jmap(很细的东西)、jps(java 的 ps -ef 呵呵)、jdb(这个不是监控工具哈,这个是 debug 工具)、jprofile(图形支持,但是可以远程连接)等等;jconsole(可以看到 heapsize、permsize+native mem size(这这里叫做:non-heapsize)等等的使用的趋势图)、visualvm(极为推荐的东西,图形化查看,你可以查看到内存单元分配、交 换、回收、移动等等整个过程,非常清晰展现 jvm 的全局资源)、另外 pmap 可以展现非常清晰的资料,可以精确到某一个 Java 进程内部的每一个细节,而 且可以看到 heapsize 只是其中很小一部分(在 solaris 操作系统上看得最齐全,Linux 下有些进程可能看不太懂);也可以在/proc/进程 号/maps 中查看(这里可以看到内存地址单元的起始地址,包含了 reserved 的地址范围和 commited 的地址范围),全局资源使用操作系统 top 命令和 free 命令看;IBM 有一个 GCMV 免费下载工具也很好;Win32 有一个 WMMap 工具都是很好的工具