jvm内存溢出实践

参考https://blog.csdn.net/weixin_42709563/article/details/106234230
《深入理解java虚拟机:jvm高级特性和最佳实践》

堆内存溢出

public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args) {
        ArrayList<OOMObject> objects = new ArrayList<>();
        while (true){
            objects.add(new OOMObject());
        }
    }
}

错误信息提示“java.lang.OutOfMemoryError: Java heap space”
可以通过MAT进行分析定位

虚拟机栈和本地方法栈溢出

由于HotSpot不区分虚拟机栈和本地方法栈,所以栈容量只能由-Xss参数设置。虚拟机栈和本地方法栈在《规范》中有两种异常:(1)栈深度超限,抛出StackOverflowError;(2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。HotSpot虚拟机不支持扩展栈内存,所以除非 在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
每个线程的栈空间是线程独有的。对于HotSpot,-Xss参数表示单个线程的栈空间上限,如果-Xss较小,会导致没有空间创建新的栈帧,抛出StackOverflowError,如果在方法里定义大量变量,增加每个栈帧的大小,在相同的-Xss参数下,会导致能创建的栈帧数量变少,方法调用深度变少,最终也会抛出StackOverflowError。
对于多线程而言,每个线程都拥有-Xss参数大小的栈空间,如果反复循环创建线程。如果是32位的windows或者linux环境,每个进程可用的内存上限为若干GB,进程内存上限-堆内存-方法区内存-JVM自身内存-直接内存~=栈空间可用内存,当多个线程的栈空间和大于栈空间可用内存时,会抛出OutOfMemoryError异常。如果-Xss参数设置越大,会越快的出现这个异常。

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

不过对于64位的系统,进程内存上限达上百TB,不会出现上述的OutOfMemoryError异常,但是可能会不断侵占本地内存。在64位linux服务器上尝试了下,cpu基本占满,虚拟内存占用较多(54.3G),实际内存缓慢增长,开始内存增长快,后面速度降低,可能因为线程较多,主线程创建新线程的速度下降,看上去内存增长似乎很难达到服务器上限,主要的问题应该还是集中在cpu占用上。

  • 一个程序最多可以使用多大的内存
    在虚拟地址模式下,一个程序可以使用的内存容量跟计算机的物理内存(也就是你的内存条)没有关系,它由虚拟地址的取值范围决定。
    在32位操作系统中,程序能使用的最大内存是 4GB,也就是2的32次方。即使你的电脑安装的是16G的内存条也没用,剩下的12G只能空闲着。
    在64位操作系统中,理论上能够访问的虚拟地址的范围是 2^64,这是一个很大的值,几乎是无限的,就目前的技术来讲,不但物理内存不可能做到这么大,CPU的寻址能力也没有这么大,实现64位长的虚拟地址只会增加系统的复杂度,带不来任何好处。Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低48位(6个字节),总的虚拟地址大小为 2^48 = 256TB。
    此外,操作系统也需要占用内存,32位的Windows默认占用4GB中的2GB,程序只能使用剩下的2GB。32位的Linux默认占用4GB中的1GB,程序只能使用剩下的3GB。64位的Windows默认占用256TB中的248TB,程序只能使用剩下的8TB。64位的Linux默认占用256TB中的128TB,程序只能使用剩下的128TB。但是,操作系统占用的内存是可以通过设置来更改的。

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

字符串常量池

运行时常量池是方法区的一部分,但是jdk7以上将字符串常量池移到了java堆中。以下代码,如果在jdk6上运行,并且设置-XX: PermSize=6M -XX: MaxPermSize=6M时,会报OutOfMemoryError: PermGen space,即永久代(方法区)内存溢出;但是如果在jdk7及以上,字符串常量池移到了java堆中,由于堆内存十分大,下面代码的循环几乎一直运行,但是如果设置-Xmx为6MB,也会报OutOfMemoryError: Java heap space。

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        short i = 0;
        while (true){
            System.out.println(i);
            set.add(String.valueOf(i).intern());
        }
    }
}
  • 关于字符串常量池的特别注意
    下面程序在jdk6中运行会有两个false,在jdk7及以上会得到一个true和一个false。jdk6中,字符串常量池在方法区中,StringBuilder创建的字符串对象实例在java堆上,而str1.intern返回的引用是方法区里的常量,所以不是同一个引用,结果均为false。jdk7中,因为字符串常量池已经移到了java堆中,所以只需要在常量池里记录一下堆上首次出现的实例引用即可,对于“计算机软件”,intern()返回的引用和StringBuilder创建的那个字符串实例就是同一个,但是对于“java”,由于这个常量在加载sum.misc.Version类的时候已经加载,所以intern()返回的是常量池中已有的常量,即第一次加载的“java”,而StringBuilder又重新在堆上创建了一个新的“java”,故这两个引用不一样。
public class ConstantPool {
    public static void main(String[] args){
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);
        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

方法区其他内容溢出

jdk7,方法区还由永久代实现,可以比较容易的出发方法区溢出,jdk8之后方法区由元空间实现,理论上仅受限于系统内存,同时可以实现垃圾回收,很难出现方法区溢出。但是HotSpot提供了若干防御性参数避免元空间的任意使用。例如:
-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。

本机直接内存溢出

直接内存(DirectMemory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。由直接内存导致的内存溢出,一个明显的特征是在HeapDump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

原文地址:https://www.cnblogs.com/lllliuxiaoxia/p/15789715.html