Java 内存安全问题的注意事项

前言

Java在内存管理方面是要比C/C++更方便的,不需要为每一个对象编写释放内存的代码,JVM虚拟机将为我们选择合适的时间释放内存空间,使得程序不容易出现内存泄漏和溢出的问题

不过,也正是因为Java把内存控制的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎么使用内存的,那排查错误将会成为一项异常艰难的工作

下面先看看JVM如何管理内存的

内存管理

根据Java虚拟机规范(第3版) 的规定,Java虚拟机所管理的内存将会包括以下几个运行内存数据区域:

  • 线程隔离数据区:

    • 程序计数器: 当前线程所执行字节码的行号指示器
    • 虚拟机栈: 里面的元素叫栈帧,存储局部变量表、操作栈、动态链接、方法出口等,方法被调用到执行完成的过程对应一个栈帧在虚拟机栈中入栈到出栈的过程。
    • 本地方法栈: 和虚拟机栈的区别在于虚拟机栈为虚拟机执行Java方法,本地方法栈为虚拟机使用到的本地Native方法服务。
  • 线程共享数据区:
    • 方法区: 可以描述为堆的一个逻辑部分,或者说使用永久代来实现方法区。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 堆: 唯一目的就是存放对象的实例,是垃圾回收管理器的主要区域,分为Eden、From/To Survivor空间。

Java各版本内存管理改进

下图中永久代理解为堆的逻辑区域,移除永久代的工作从JDK7就已经开始了,部分永久代中的数据(常量池)在JDK7中就已经转移到了堆中,JDK8中直接去除了永久代,方法区中的数据大部分被移到堆里面,还剩下一些元数据被保存在元空间里

内存溢出

  • 内存泄露Memory Leak: 申请的内存空间没有及时释放,导致后续程序里这块内容永远被占用。
  • 内存溢出Out Of Memory: 要求的内存超过了系统所能提供的

运行时数据区域的常见异常

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

堆内存溢出

不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。

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

栈溢出

单个线程下不断扩大栈的深度引起栈溢出。

public class StackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        StackSOF sof = new StackSOF();
        try {
            sof.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length: " + sof.stackLength);
            throw e;
        }
    }
}

循环的创建线程,达到最大栈容量。

public class StackOOM {
    private void dontStop() {
        while (true) {
        }
    }
    public void stackLeadByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeadByThread();
    }
}

运行时常量池溢出

不断的在常量池中新建String,并且保持引用不释放。

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用List保持着常量池的引用,避免Full GC回收常量池
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            // intern()方法使String放入常量池
            list.add(String.valueOf(i++).intern());
        }
    }
}

方法区溢出

借助CGLib直接操作字节码运行时产生大量的动态类,最终撑爆内存导致方法区溢出。

public class MethodAreaOOM {
    static class ObjectInMethod {
    }
    public static void main(final String[] args) {
        // 借助CGLib实现
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(ObjectInMethod.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();
        }
    }
}

元空间溢出

助CG Lib运行时产生大量动态类,唯一的区别在于运行环境修改为Java 1.8,设置-XX:MaxMetaspaceSize参数,便可以收获java.lang.OutOfMemoryError: Metaspace这一报错

本机直接内存溢出

直接申请分配内存(实际上并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是抛出异常)

public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

常见案例

在工作中一般会遇到有以下几种情况导致内存问题

  • 传输数据量过大

因为传输数量过大、或一些极端情况导致代码中间结果对象数据量过大,过大的数据量撑爆内存

  • 查询出大量对象

这个多为SQL语句设置问题,SQL未设置分页,用户一次查询数据量过大、频繁查询SQL导致内存堆积、或是未作判空处理导致WHERE条件为空查询出超大数据量等

  • 接口性能问题导致

这类为外部接口性能较慢,占用内存较大,并且短时间内高QPS导致的,导致服务内存不足,线程堆积或挂起进而出现FullGC

  • 元空间问题

使用了大量的反射代码,Java字节码存取器生成的类不断生成

问题排查

使用jmap分析内存泄漏

1.生成dump文件

jmap -dump:format=b,file=/xx/xx/xx.hprof pid

2.dump文件下载到本地

3.dump文件分析

可以使用MAT,MAT可作为Eclipse插件或一个独立软件使用,MAT是一个高性能、具备丰富功能的Java堆内存分析工具,主要用来排查内存泄漏和内存浪费的问题。

使用MAT打开上一部后缀名.hprof的dump文件

  • Histogram:直方图,各个类的实例,包括个数和大小,可以查看类引用和被引用的路径。
  • Dominator Tree:支配图,列出所有线程和线程下面的那些对象占用的空间。
  • Top Consumers:通过图形列出消耗内存多的实例。
  • Leak Suspects:MAT自动分析的内存泄漏报表

可以用这个工具分析出什么对象什么线程占用内存空间较大,对象是被什么引用的,线程内有哪些资源占用很高

以运行时常量池溢出为例

打开Histogram类实例表

Objects是类的对象的数量;Shallow是对象本身占用内存大小、不包含其他引用;

Retained是对象自己的Shallow加上直接或间接访问到对象的Shallow之和,也可以说是GC之后可以回收的内存总和

从图中可以看出运行时常量池溢出的情况,产生了大量的String和char[]实例

在char[]上右键可以得到上图所有char[]对象的被引用路径,可以看出这些char数组都是以String的形式存在ArrayList中,并且是由main这个线程运行的

可以看出是main线程中新建了一个数组,其中存了32w+个长度为6的char数组组成的String造成的内存溢出

关于MAT的详细使用可以从MAT官方教程学习更多

以上就是Java 内存安全问题的注意事项的详细内容,更多关于Java 内存安全问题的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Java中synchronized关键字的死锁和内存占用问题

    先看一段synchronized 的详解: synchronized 是 java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以

  • Java中典型的内存泄露问题和解决方法

    Q:在Java中怎么可以产生内存泄露?A:Java中,造成内存泄露的原因有很多种.典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况.最后会生成很多重复的对象.所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下. 复制代码 代码如下: import java.util.HashMap;import java.util.Map; public class MemoryLeak { pu

  • JAVA程序内存溢出问题原因分析

    本文较为详细的分析了JAVA程序内存溢出问题原因.分享给大家供大家参考.具体如下: 遇到一个线上系统报 java.lang.OutOfMemoryError: PermGen space 错误,需要定位一下问题.很久之前到时弄过这个,现在还真有点不记得了,但这个真的是一个非常有意思的问题,值得好好研究一下.首先第一反应当然是加上-XX:+PrintGCDetails参数来看具体的GC日志,但是由于程序是tomcat启动的,担心里面封装的东西太多不好定位,既然在windows下面,所以还是借助可视

  • 解决Java导入excel大量数据出现内存溢出的问题

    问题:系统要求导入40万条excel数据,采用poi方式,服务器出现内存溢出情况. 解决方法:由于HSSFWorkbook workbook = new HSSFWorkbook(path)一次性将excel load到内存中导致内存不够. 故采用读取csv格式.由于csv的数据以x1,x2,x3形成,类似读取txt文档. private BufferedReader bReader; /** * 执行文件入口 */ public void execute() { try { if(!path.

  • Java内存模型可见性问题相关解析

    这篇文章主要介绍了Java内存模型可见性问题相关解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 之前的文章中讲到,JMM是内存模型规范在Java语言中的体现.JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性.可见性和有序性. 本文就具体来讲讲JMM是如何保证共享变量访问的可见性的. 什么是可见性问题 我们从一段简单的代码来看看到底什么是可见性问题. public class VolatileDemo { boolean

  • Java DWR内存泄漏问题解决方案

    机器跑了一晚上,发现有崩溃现象,由于页面内有动态绘图功能,我怀疑是绘图原因,但是今天上午有人提醒我才想到,是不是间隔调用时DWR产生了内存泄漏问题? 网上查了一下貌似大家都在讨论这个问题,之前我也挺老手说过DWR有内存问题,可是没有遇到过.原来DWR在间隔调用这种情况下会有问题! 按照大家的说法,修改engine.js配置文件来解决问题,目前我也修改了一下,修改方法如下: 在 dwr.engine._sendData = function(batch) {} 内有,这样几行代码: // Get

  • 完美解决java读取大文件内存溢出的问题

    1. 传统方式:在内存中读取文件内容 读取文件行的标准方式是在内存中读取,Guava 和Apache Commons IO都提供了如下所示快速读取文件行的方法: Files.readLines(new File(path), Charsets.UTF_8); FileUtils.readLines(new File(path)); 实际上是使用BufferedReader或者其子类LineNumberReader来读取的. 传统方式的问题: 是文件的所有行都被存放在内存中,当文件足够大时很快就会

  • macOS上使用gperftools定位Java内存泄漏问题及解决方案

    这几天在排查一个堆外内存泄漏的问题时看到很多人都提到了gperftools这个神器,想要尝试一下结果发现它对macOS的支持不太友好.而且大多数教程是针对C++的,里面的一通编译链接的操作看得我个Java仔眼花缭乱的.所以我在这里整理一份mac和Java版的使用教程,免得大家再来踩坑了. 一.简介 gperftools是google提供的一套分析工具,包括堆内存检测heap-profiler,内存泄漏分析工具heap-checker和CPU性能监测工具cpu-profiler.众所周知堆外内存的

  • 基于Java 数组内存分配的相关问题

    可能Java 数组大家都很熟悉,最近我遇到了一个关于Java 数组内存分配的问题.呵呵.突然就发现许多书上"基本数据类型存储在栈内存当中,对象则保存在堆内存"这句话完全是错误的.下面是个简单的例子代码: 复制代码 代码如下: public class Test {    public static void main(String[] argv) {// 静态初始化数组String[] names = { "Michael", "Orson",

  • Java内存泄漏问题处理方法经验总结

    JVM问题,一般会有三种情况,目前遇到了两种,线程溢出和JVM不够用 1.线程溢出:unable to create new native thread 1.1问题描述: 系统在1月4号左右,突然发现会产生内存溢出问题,从日志上看,错误信息为: 导致系统不能使用,对外不能相应,但是观察gc等又处于正常情况,free 系统内存也正常.开始重启机器进行解决,真正的原因查找,过程比较坎坷,经历也比较痛苦. 1.2 问题解决 pstree查看线程数,发现系统线程数不断增长,直到OOM. 命令:pstre

  • 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互--在 Java 程序中调用 native code:在 native code 中嵌入 Java 虚拟机调用 Java 的代码. JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点: 利用 native code 的平台相关性,在平台相关的编程中彰显优势. 对 native code 的代码重用. native code 底层操作

随机推荐