Java 虚拟机的内存溢出

在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。

在Eclipse中进行JVM参数设置

可以直接通过上方菜单栏的 Run 下的 Run Configurations... 或者 Debug Configurations... 选项进行设置。

双击 Java Application 会生成一个新的配置卡。

在新的配置卡中可配置名称Name,Main里面的Project配置项目名,Main class配置main方法所在的类。

(如果是从main方法里面右键打开的Run As-->Run Configurations...或者Debug As-->Debug Configurations...,这几项会自己生成)

Arguments下面的VM arguments 里面配置虚拟机参数。

 参数解释: 

1,堆是存储对象实例的,创建的对象都是在堆中进行内存分配的;设置堆的大小:-Xmx20M(最大值) ,-Xms20M(最小值),其中-Xmn设置年轻代大小。

2,栈是存储局部变量,操作栈,动态链接,方法出口(都在栈桢中) 的地方,调用方法时,会创建栈桢;设置栈的大小:-Xss128K。

3,方法区是存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。此外运行时常量池是属于方法区的,即存放常量,静态常量等;设置方法区大小,-XX:PermSize=10M和-XX:MaxPermSize=10M。

4,本地直接内存;设置本地直接内存大小:-XX:MaxDirectMemorySize(默认与-Xmx的值一样)。

堆内存溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

参数设置:-verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError

限制Java堆的大小为20M,不可扩展(将堆的最小值 -Xms 参数和最大值 -Xmx 参数设置一样即可避免堆自动扩展),通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。

代码:

复制代码
package jvm;

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();

        while (true) {
            list.add(new OOMObject());
        }

    }

}
复制代码

运行结果:

栈内存溢出

在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,所以 -Xoss 参数无效,栈容量只由 -Xss 参数控制。

参数设置:-Xss128k

代码:

复制代码
package jvm;

public class JavaVMStackSOF {

    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    
    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
        
    }

}
复制代码

运行结果:

方法区内存溢出

参数设置:-XX:PermSize=10M -XX:MaxPermSize=10M

注意:JDK8中用 MetaspaceSize 代替 PermSize,因此将 -XX:PermSize=10M -XX:MaxPermSize=10M 修改为 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

String.intern() 方法是一个Native方法,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

代码一:

复制代码
package jvm;

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持常量池引用,避免 Full GC 回收常量池行为
        List<String> list = new ArrayList<>();
        // 10MB的 PermSize在Integer范围内足够产生OOM了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }

    }

}
复制代码

以上代码在JDK1.6及之前的版本下,由于常量池分配在永久带内,运行时常量池溢出,使用JDK1.7以及更高版本运行这段程序不会得到相同结果,while循环将一直进行下去。因为1.7及更高版本的常量池值只保存了引用。

代码二:

复制代码
package jvm;

public class RuntimeConstantPoolOOM {

    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);

    }

}
复制代码

运行结果:

结果分析:

JDK1.6会得到两个false。

在JDK1.6中,intern() 方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久带中这个字符串实例的引用。而 StringBuilder 创建的字符串实例在堆上,所以不是同一个引用,返回 false。

JDK1.7中会得到一个true和一个false。

在JDK1.7中,intern() 方法不会再复制实例,只在常量池中记录首次出现的实例引用,该引用指向的是堆中的实例,这个实例就是 StringBuilder 创建的那个字符串实例,所以是同一个引用,返回true。

返回false是因为 “java” 这个字符串在中StringBuilder之前已经出现过,字符串常量池中已经有它的引用了,而 StringBuilder 又重新创建了一个“java”字符串实例,所以str2中保存的是新创建的实例的引用,而str2.intern() 返回的是旧的实例的引用,所以不是同一个引用,返回false。

代码三:

复制代码
package jvm;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class JavaMethodAreaOOM {

    public static void main(String[] args) {

        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOmObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });

            enhancer.create();

        }

    }

    static class OOmObject {
    }

}
复制代码

以上代码使用使用CGLib使方法区出现内存溢出异常。使用到下面的两个jar包。

JDK8运行结果:

本机直接内存溢出

参数设置:-Xmx20M  -XX:MaxDirectMemorySize=10M

直接内存容量可通过 MaxDirectMemorySize 指定,如果不指定,则默认和Java堆最大值一样(-Xmx)。

代码:

复制代码
package jvm;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        //通过反射获取实例
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            //申请分配内存
            unsafe.allocateMemory(_1MB);
        }
    }

}
复制代码

运行结果:

posted @   一线大码  Views(456)  Comments(0Edit  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示