《深入理解Java虚拟机》读书笔记(三)--堆栈异常代码示例

简介: 《深入理解Java虚拟机》读书笔记(三)--堆栈异常代码示例

Java堆溢出

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

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


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

运行结果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]

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

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:

1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。


《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。


为了验证这点,我们可以做两个实验,先将实验范围限制在单线程中操作,尝试下面两种行为是否能让HotSpot虚拟机产生OutOfMemoryError异常:

第一种情况:

·使用-Xss参数减少栈内存容量。


结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

/**
*VM Args:-Xss128k
*@author zzm
*/
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;
    }
  }
}
stack length:2402
Exception in thread "main" java.lang.StackOverflowError
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:20)
 at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21) 
 at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)

对于不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。譬如上述方法中的参数-Xss128k可以正常用于32位Windows系统下的JDK 6,但是如果用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180K,而在Linux下这个值则可能是228K,如果低于这个最小限制,HotSpot虚拟器启动时会给出如下提示:


The Java thread stack size specified is too small. Specify at least 228k


第二种情况:


·定义了大量的本地变量,增大此方法帧中本地变量表的长度。


结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

public class JavaVMStackSOF {
  private static int stackLength = 0;
  public static void test() {
  long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41, unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71, unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81, unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89, unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99, unused100;
  stackLength ++; test();
  unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 =
  unused26 = unused27 = unused28 = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;
  }
  public static void main(String[] args) { 
    try {
      test();
    }catch (Error e){
      System.out.println("stack length:" + stackLength); throw e;
    }
  }
}

运行结果:

stack length:5675
Exception in thread "main" java.lang.StackOverflowError
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:27)
 at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28) 
 at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)

实验结果表明:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常。可是如果在允许动态扩展栈容量大小的虚拟机 上,相同代码则会导致不一样的情况。


可见相同的代码在Classic虚拟机中成功产生了OutOfMemoryError而不是StackOverflowError异 常。如果测试时不限于单线程,通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常的,具体如下所示。但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。甚至可以说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。


原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,


那剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽,下面演示了这种情况。

/**
*VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
*@author zzm
*/
public class JavaVMStackOOM {
private void dontStop() { while (true) {
}
}
public void stackLeakByThread() { while (true) {
  Thread thread = new Thread(new Runnable() { @Override
    public void run() { 
      dontStop();
    }
  });
    thread.start();
    }
  }
  public static void main(String[] args) throws Throwable { 
    JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread();
  }
}

重点提示一下,如果要尝试运行上面这段代码,记得要先保存当前的工作,由于在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上,无限制地创建线程会对操作系统带来很大压力,上述代码执行时有很高的风险,可能会由于创建线程数量过多而导致操作系统假死。


在32位操作系统下的运行结果:


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


出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所 在。如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说大多数情况下)到达1000~2000是完全没有问题,对于正常的方法调用(包括不能做尾递归优化的递归调用),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢

出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过“减少内存”的手段来解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到,这一点读者需要在开发32位系统的多线程应用时注意。也是由于这种问题较为隐蔽,从JDK 7起,以上提示信息中“unable to create native thread”后面,虚拟机会特别注明原因可能是“possibly out of memory or process/resource limits reached” 。


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

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。前面曾经提到HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代的背景故事,在此我们就以测试代码来观察一下,使用“永久代”还是“元空间”来实现方法区,对程序有什么实际的影响。


String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量,具体实现如代码清单2-7所示,请读者测试时首先以JDK 6来运行代码。

/**
*VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M
*@author zzm
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
  // 使用Set保持着常量池引用,避免Full GC回收常量池行为Set<String> set = new HashSet<String>();
  // 在short范围内足以让6MB的PermSize产生OOM了
    short i = 0; 
    while (true) {
      set.add(String.valueOf(i++).intern());
    }
  }
}

结果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

从运行结果中可以看到,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息 是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的一部分。


而使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果,无论是在JDK 7中继续使用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制在6MB,也都不会重现JDK 6中的溢出异常,循环将一直进行下去,永不停歇[1]。出现这种变化,是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版本,限制方法区的容量对该测试用例来说是毫无意义的。这时候使用-Xmx参数限制最大堆到6MB就能够看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出:


本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致,代码清单2-10越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe 的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()。

/**
*VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*@author zzm
*/
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);
    }
  }
}

结果:

Exception in thread "main" java.lang.OutOfMemoryError 
at sun.misc.Unsafe.allocateMemory(Native Method)
 at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

目录
相关文章
|
7天前
|
JavaScript NoSQL Java
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
152 96
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
|
1月前
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
272 11
|
1月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
1月前
|
存储 运维 数据挖掘
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
77 3
|
2月前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
78 2
|
2月前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
2月前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
142 5
|
2月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
94 5
|
3月前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####

热门文章

最新文章