关于java.lang.OutOfMemoryError知多少(二)

一、java.lang.OutOfMemoryError之Permgen space

1、概述

Java应用程序只允许使用有限的内存。在应用程序启动时指定特定应用程序可使用的内存的确切大小,进而将java内存进行区域划分【注意jdk版本不同会导致区域有所差异】


关于java.lang.OutOfMemoryError知多少(二)_第1张图片
jvm堆.png
关于java.lang.OutOfMemoryError知多少(二)_第2张图片
完整JVM区域.png

所有这些区域的大小,包括permgen区域,都是在JVM启动期间设置的。如果您不自己设置大小,则使用特定于平台的默认值。

2、原因

一旦出现java. lang.OutOfMemoryError:PermGen空间消息,则表明永久期的内存区域已被耗尽。
为了理解上述的现象,首先我们需要了解PermGen(永久代)的作用是什么?
出于实际的目的,永久生成主要由加载的类声明组成,并存储到PermGen中,包括类的名称和字段,方法有方法字节码、常量池信息、对象数组和与类关联的类型数组,以及时间编译器优化。
从上面可以推断,PermGen大小的需求取决于加载的类的数量和类声明的大小。
因此,我们可以说java . lang.OutOfMemoryError的主要原因: 太多的类或太大的类被加载到永久代(PermGen空间)

3、实例

1、简单实例
如上所述,PermGen空间使用与加载到JVM中的类的数量密切相关。

import javassist.ClassPool;

public class MicroGenerator {
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 100_000_000; i++) {
      generate("eu.plumbr.demo.Generated" + i);
    }
  }

  public static Class generate(String name) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    return pool.makeClass(name).toClass();
  }
}

在本例中,源代码在循环中迭代并在运行时生成类【javassist库可以处理类生成的复杂性】,实例代码将不听生成新的类,并将它们的定义加载到Permgen空间中,直到空间被充分利用直至java.lang.OutOfMemoryError被抛出
2、部署实例
从服务器卸载应用程序时,当前的classloader以及加载的class在没有实例引用的情况下,持久代的内存空间会被GC清理并回收。如果应用中有类的实例对当前的classloader的引用,那么Permgen区的class将无法被卸载,导致Permgen区的内存一直增加直到出现Permgen space错误。
不幸的是,许多第三方库以及糟糕的资源处理方式(比如:线程、JDBC驱动程序、文件系统句柄)使得卸载以前使用的类加载器变成了一件不可能的事。反过来就意味着在每次重新部署过程中,应用程序所有的类的先前版本将仍然驻留在Permgen区中,你的每次部署都将生成几十甚至几百M的垃圾。
以线程和JDBC驱动来说说。很多人都会使用线程来处理一下周期性或者耗时较长的任务,这个时候一定要注意线程的生命周期问题,你需要确保线程不能比你的应用程序活得还长。否则,如果应用程序已经被卸载,线程还在继续运行,这个线程通常会维持对应用程序的classloader的引用,造成的结果就不再多说。多说一句,开发者有责任处理好这个问题,特别是如果你是第三方库的提供者的话,一定要提供线程关闭接口来处理清理工作。
比如一个使用JDBC驱动程序连接到关系数据库的示例应用程序。当应用程序部署到服务器上的时:服务器创建一个classloader实例来加载应用所有的类(包含相应的JDBC驱动)。根据JDBC规范,JDBC驱动程序(比如:com.mysql.jdbc.Driver)会在初始化时将自己注册到java.sql.DriverManager中。该注册过程中会将驱动程序的一个实例存储在DriverManager的静态字段内。

// com.mysql.jdbc.Driver源码
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}
// // // // // // // // // //
// 再看下DriverManager对应代码
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();

public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        throw new NullPointerException();
    }
}

当从服务器上卸载应用程序的时候,java.sql.DriverManager仍将持有那个驱动程序的引用,进而持有用于加载应用程序的classloader的一个实例的引用。这个classloader现在仍然引用着应用程序的所有类。如果此程序启动时需要加载2000个类,占用约10MB永久代(PermGen)内存,那么只需要5~10次重新部署,就会将默认大小的永久代(PermGen)塞满,然后就会触发java.lang.OutOfMemoryError: PermGen space错误并崩溃。

4、解决方案

1、解决初始化时OutOfMemoryError
当应用程序启动期间由于PermGen耗尽而导致OutOfMemoryError错误时,解决方案很简单。应用程序只是需要更多的空间来将所有的类加载到PermGen区域,所以我们只需要增加它的大小。为此,更改您的应用程序启动配置,并添加(或增加)- xx:MaxPermSize参数:
java -XX:MaxPermSize=512m com.yourcompany.YourClass
2、解决重新部署时OutOfMemoryError
分析dump文件:首先,找出引用在哪里被持有;其次,给你的web应用程序添加一个关闭的hook,或者在应用程序卸载后移除引用。你可以使用如下命令导出dump文件:
jmap -dump:format=b,file=dump.hprof
3、解决运行时OutOfMemoryError

  • 首先需要检查是否允许GC从PermGen卸载类,JVM的标准配置相当保守,只要类一创建,即使已经没有实例引用它们,其仍将保留在内存中,特别是当应用程序需要动态创建大量的类但其生命周期并不长时,允许JVM卸载类对应用大有助益,你可以通过在启动脚本中添加以下配置参数来实现:-XX:+CMSClassUnloadingEnabled
    默认情况下,这个配置是未启用的,如果你启用它,GC将扫描PermGen区并清理已经不再使用的类。但请注意,这个配置只在UseConcMarkSweepGC的情况下生效,如果你使用其他GC算法,比如:ParallelGC或者Serial GC时,这个配置无效。所以使用以上配置时,请配合:-XX:+UseConcMarkSweepGC
  • 如果你已经确保JVM可以卸载类,但是仍然出现内存溢出问题,那么你应该继续分析dump文件,使用以下命令生成dump文件:
    jmap -dump:file=dump.hprof,format=b
  • 当拿到生成的堆dump文件,并利用像Eclipse Memory Analyzer Toolkit这样的工具来寻找应该卸载却没被卸载的类加载器,然后对该类加载器加载的类进行排查,找到可疑对象,分析使用或者生成这些类的代码,查找产生问题的根源并解决它。

二、java.lang.OutOfMemoryError之Metaspace

1、概述
关于java.lang.OutOfMemoryError知多少(二)_第3张图片
jvm构成.png

关于java.lang.OutOfMemoryError知多少(二)_第4张图片
完整JVM区域.png

在JVM启动期间指定所有这些区域的大小,包括metaspace区域。如果您自己不确定大小,则使用特定于平台的默认值。
一旦出现 java.lang.OutOfMemoryError: Metaspace message信息表明Metaspace 空间已被耗尽。

2、原因

从Java 8开始,Java中的内存模型发生了很大的变化。引入了一种叫做Metaspace的新的记忆区域,并移除Permgen。变化的原因大致:

  • 所需的permgen尺寸很难预测。提供内存不足或过度配置导致资源浪费都会触发 java.lang.OutOfMemoryError: Permgen size信息
  • GC性能的改进,支持在没有GC暂停的情况下并发类数据的分配,以及元数据上的特定迭代器
  • 支持进一步优化,如G1并发卸载
    诸如名称和类的字段、方法类的字节码方法,常量池,JIT优化等这些位于jdk8之前版本的PermGen区域里,而现在位于Metaspace区域。
    Metaspace大小需求既依赖于加载的类的数量,也依赖于类声明的大小。因此,太多的类或太大的类被加载到Metaspace是触发java.lang.OutOfMemoryError:Metaspace信息的主要原因。
3、实例
public class Metaspace {
    static javassist.ClassPool cp = javassist.ClassPool.getDefault();

    public static void main(String[] args) throws Exception{
        for (int i = 0; ; i++) { 
            Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
            System.out.println(i);
        }
    }
}

程序运行中不停的生成新类,所有的这些类的定义将被加载到Metaspace区,直到空间被完全占用并且抛出java.lang.OutOfMemoryError:Metaspace。当使用-XX: -XX:MaxMetaspaceSize=64m启动时【 Java 1.8.0_05】,大约加载70000多个类时就会死机。

4、解决方案

1、增加其大小,更改启动配置增加如下参数,增大Metaspace区空间
-XX:MaxMetaspaceSize = 512m,
2、删除此参数来完全解除对Metaspace大小的限制(默认是没有限制的)
默认情况下,对于64位服务器端JVM,MetaspaceSize默认大小是21M(初始限制值),一旦达到这个限制值,FullGC将被触发进行类卸载,并且这个限制值将会被重置,新的限制值依赖于Metaspace的剩余容量。如果没有足够空间被释放,这个限制值将会上升,反之亦然。在技术上Metaspace的尺寸可以增长到交换空间,而这个时候本地内存分配将会失败。

三、其他

最后给各位推荐一个阿里JVM大神寒泉子的公众号(lovestblog)及其开发的JVM参数小程序(JVMPcoket)

你可能感兴趣的:(关于java.lang.OutOfMemoryError知多少(二))