【JVM进阶之路】四:直面内存溢出和内存泄漏

简介: 【JVM进阶之路】四:直面内存溢出和内存泄漏

     

在Java中,和内存相关的问题主要有两种,内存溢出内存泄漏

  • 内存溢出(Out Of Memory :就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。
  • 内存泄露 (Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。

1、内存溢出

在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能。

image.png

1.1、Java堆溢出

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

我们来看一个代码的例子:

/**
 * VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

接下来,我们来设置一下程序启动时的JVM参数。限制内存大小为20M,不允许扩展,并通过参数-XX:+HeapDumpOnOutOf-MemoryError 让虚拟机Dump出内存堆转储快照。

在Idea中设置JVM启动参数如下图:

image.png

运行一下:

image.png

Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java  heap space”。 Java堆文件快照文件dump到了java_pid18728.hprof文件。

要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如JProfiler、Eclipse Memory Analyzer等)对Dump出来的堆转储快照进行分析。

看到内存占用信息如下:

image.png

然后可以查看代码问题如下:

image.png

常见堆JVM相关参数:

-XX:PrintFlagsInitial: 查看所有参数的默认初始值-XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)

-Xms: 初始堆空间内存(默认为物理内存的1/64)

-Xmx: 最大堆空间内存(默认为物理内存的1/4)

-Xmn: 设置新生代大小(初始值及最大值)

-XX:NewRatio: 配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例

-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)

-XX:+PrintGCDetails:输出详细的GC处理日志

打印GC简要信息:① -XX:+PrintGC-verbose:gc

-XX:HandlePromotionFailure:是否设置空间分配担保

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

HotSpot虚拟机中将虚拟机栈和本地方法栈合二为一,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,有两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。

1.2.1、StackOverflowError

HotSpot虚拟机不支持栈的动态扩展,在HotSpot虚拟机中,以下两种情况都会导致StackOverflowError。

  • 栈容量过小
    如下,使用Xss参数减少栈内存容量
/**
 * vm参数:-Xss128k
 */
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;
        }
    }
}

运行结果:

image.png

  • 栈帧太大
    如下,通过一长串变量,来占用局部变量表空间。
    image.png

运行结果:

image.png

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常。

1.2.2、OutOfMemoryError

虽然不支持动态扩展栈,但是通过不断建立线程的方式,也可以在HotSpot上产生内存溢出异常。

需要注意,这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。因为操作系统给每个进程的内存时有限的,线程数一多,自然会超过进程的容量。

创建线程导致内存溢出异常 :

/**
 * vm参数:-Xss2M
 */
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
        }
    }
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

以上是一段比较有风险的代码,可能会导致系统假死,运行结果如下:

image.png

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

这里再提一下方法区和运行时常量池的变迁,JDK1.7以后字符串常量池移动到了堆中,JDK1.8在直接内存中划出一块区域元空间来实现方区域。

String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的  字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK  6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,永久代本身内存不限制可能会出现错误:

java.lang.OutOfMemoryError: PermGen space

1.4、本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。

直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:

/**
 * vm参数:-Xmx20M -XX:MaxDirectMemorySize=10M
 */
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);
        }
    }
}

运行结果:

image.png

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况。

2、内存泄漏

内存回收,简单说就是应该被垃圾回收的对象没有被垃圾回收。

image.png

在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y生命周期结束的时候,垃圾回收器不会回收对象Y。

我们来看几个内存泄漏的例子:

  • 静态集合类引起内存泄漏
    静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
public class OOM {
 static List list = new ArrayList();
 public void oomTests(){
   Object obj = new Object();
   list.add(obj);
  }
}
  • 单例模式
    和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
  • 数据连接、IO、Socket等连接
    创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
        try {
            Connection conn = null;
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url", "", "");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("....");
          } catch (Exception e) { 
          }finally {
            //不关闭连接
          }
        }
  • 变量不合理的作用域
    一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
public class Simple {
    Object object;
    public void method1(){
        object = new Object();
        //...其他代码
        //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
        object = null;
    }
}
  • 引用了外部类的非静态内部类
    非静态内部类(或匿名类)的初始化总是需要依赖外部类的实例。默认情况下,每个非静态内部类都包含对其包含类的隐式引用,若在程序中使用这个内部类对象,那么即使在包含类对象超出范围之后,也不会被回收(内部类对象隐式地持有外部类对象的引用,使其成不能被回收)。
  • Hash 值发生改变
    对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hah值和存储进容器时的Hash值不同,会导致无法从容器中单独删除当前对象,造成内存泄露。
  • ThreadLocal 造成的内存泄漏
    ThreadLocal 可以实现变量的线程隔离,但若使用不当,就可能会引入内存泄漏问题。
目录
相关文章
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
291 1
|
2天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
25天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
23天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
1月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
22 3
|
1月前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
1月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
50 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
22 1

热门文章

最新文章