layout: post
title: 深入JAVA虚拟机2_4OutOfMemoryError异常
categories: JVM JAVA
description: 深入JAVA虚拟机2_4OutOfMemoryError异常
keywords: JVM JAVA
注意:KOTLIN跟JAVA运行结果可能会不同,参照样例
https://www.jianshu.com/p/d9f90f3ee936
2_4_1. JAVA堆溢出测试
测试思路
java堆用于存储对象实例,只要不断创建对象,并保证
GC Roots到对象之间有可达路径来避免垃圾回收机制清
除这些对象,那么在对象数量到达最大堆的容量限制后
就会产生内存溢出异常
code2_3虚拟机参数
//限制java堆的大小为20mb[-Xms为堆的最小值,-Xmx为
堆的最大值]
//XX:+HeapDumpOnOutOfMemoryError可让虚拟机在出
现内存溢出异常时Dump出当前内存堆转储快照以便事后
进行分析
参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
code2_3 代码
package num2
/**
* Created by Joey_Tsai on 2018/3/5.
*/
class HeapOOM{
companion object {
class OOMObject{
}
}
}
fun main(args : Array<String>){
val list : MutableList<HeapOOM.Companion.OOMObject> = ArrayList<HeapOOM.Companion.OOMObject>();
var i = 0;
while (true){
list.add(HeapOOM.Companion.OOMObject());
println(++i);
}
}
内存泄漏 与 内存溢出
内存泄漏:编写的程序没有正确的释放内存
内存溢出:内存不足,无法正常给程序分配内存,导致内
存不足的原因有很多,内存泄漏只是其中的一种
解决方法
使用工具检查GC Roots查看引用链上的对象是否都有存
活的意义,若确实没有出现泄露的情况,应该调整堆内
存参数(-Xmx 和 Xms)的最大值和最小值
p.s
程序运行参数图
程序运行结果图
github链接(使用kotlin实现)
https://github.com/joeytsai03/JVMStudy/blob/master/src/num2/code2_3.kt
分 割 线
2_4_2. 虚拟机栈和本地方法栈溢出
1.虚拟机栈和本地方法栈OOM测试
测试思路
HotSpot虚拟机中不分虚拟机栈和本地方法栈,故-Xoss
参数(设置本地方法栈大小)存在但实际上是无效的,栈容
量由-Xss参数设定,关于虚拟机栈和本地方法栈java虚拟
机描述了两种异常:
1.如果线程请求的栈深度大于虚拟机所允许的最大深度,
将抛出StackOverflowError异常
2.如果虚拟机在扩展栈时无法申请到足够的内存空间,则
抛出OutOfMemoryError异常
code2_4虚拟机参数
参数 : -Xss128k
code2_4代码
package num2
/**
* Created by Joey_Tsai on 2018/3/5.
* VM:-Xss128k
*/
public class JavaVMStackSOF{
public var stackLength : Int = 1
public fun stackLeak() : Unit{
stackLength++;
stackLeak()
}
}
fun main(args:Array<String>){
val javaVMStackSOF : JavaVMStackSOF = JavaVMStackSOF()
try {
javaVMStackSOF.stackLeak()
}catch (e : Throwable ){
println("stack length : ${javaVMStackSOF.stackLength}")
throw e
}
}
java虚拟机栈 与 java堆 与 方法区
1.java虚拟机栈:线程私有,生命周期与线程相同,每个方
法在执行的过程中都会创建一个栈帧(Stack Frame)用于
存储局部变量表,操作数栈,动态链接,方法出口等信
息。每一个方法执行完成的过程,就对应一个栈帧在虚
拟机栈中入栈出栈的过程。
2.java堆:所有线程共享的一块内存区域,用于存放对象
实例,垃圾收集器管理的主要区域,很多时候也被称作
"GC堆"(Garbage Collected Heap)
3.方法区:所有线程共享的内存区域,它用于存储已被虚拟
机加载的类信息,常量,静态变量,即时编译器编译后
的代码等数据
实验结果
在单线程下,无论由于栈帧太大还是虚拟机栈容量太
小,当内存无法分配时虚拟机抛出的都是
StackOverflowError异常
p.s
程序运行参数图
程序运行结果图
github链接(使用kotlin实现)
https://github.com/joeytsai03/JVMStudy/blob/master/src/num2/code2_4.kt
2.多线程导致内存溢出异常
测试思路
如果测试时不限单线程,通过不断创建线程的方式倒是
可以产生内存溢出异常,这种情况下,为每个线程的栈
分配的内存越大,反而越容易产生内存溢出异常
使用工具
JProfiler用于查看线程使用情况
Idea安装JProfiler
code2_5虚拟机参数
参数: -Xss2M
code2_5代码
package num2
/**
* Created by Joey_Tsai on 2018/3/6.
*VM: -Xss200M
*/
class JavaVMStackOOM{
private fun dontStop() : Unit{
while (true){
}
}
public fun stackLeakByThread():Unit{
while (true){
val thread : Thread = Thread(Runnable(){
@Override
fun run(){
dontStop()
}
});
thread.start()
}
}
}
fun main(args : Array<String>){
val oom = JavaVMStackOOM()
oom.stackLeakByThread()
}
操作系统内存分配
譬如,在32位的windows系统中给每个线程分配的内存
限制为2g,虚拟机提供了参数来控制java堆和方法区这
两部分内存的最大值,剩余的内存为2GB减去Xmx(最大
堆容量),再减去MaxPermSize(最大方法区容量),程序
计数器消耗内存很小,可以忽略。
p.s
程序运行参数图
github链接(使用kotlin实现)
https://github.com/joeytsai03/JVMStudy/blob/master/src/num2/code2_5.kt
分 割 线
2_4_3 方法区和运行时常量池溢出
1.运行时常量池导致的内存溢出异常
测试思路
运行时常量池是方法区的一部分,jdk7开始逐步去除永久
代,String.intern()是一个Native方法,在jdk1.6及之前的版
本中,由于常量池分配在永久代中,我们可以通过
-XX:PermSize与 -XX:MaxPermSize限制方法区大小,从
而限制常量池容量大小
code2_6虚拟机参数
VM:-XX:PermSize=10M -XX:MaxPermSize=10M
code2_6代码
package num2
/**
* Created by Joey_Tsai on 2018/3/6.
* VM:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
class RuntimeConstantPoolOOM2_6{
}
fun main(args : Array<String>){
//使用List保持着常量池引用,避免Full GC回收常量池行为
val list : MutableList<String>?=ArrayList<String>();
//10MB的permSize在integer范围内足够产生OOM了
var i : Int = 0
while (true){
list?.add(i++.toString().intern())
println(i)
}
}
实验结果
运行时常量池溢出,在"OutOfMemoryError"后面跟着的提示信息
是"PermGen space",说明运行时常量池属于方法区(HotSpot虚拟机中的永
久代)的一部分,在jdk1.7中则不会得到相同结果,while将一直循环下去。
2.String.intern返回引用测试
code2_7代码
package num2
/**
* Created by Joey_Tsai on 2018/3/6.
*/
public class RuntimeConstantPoolOOM2_7{
}
fun main(args: Array<String>) {
val str1 : String = StringBuilder("计算机").append("软件").toString()
println(str1.intern() == str1)
val str2 : String = StringBuilder("ja").append("va").toString()
println(str2.intern() == str2)
}
JDK1.6中的intern() 与 JDK1.7中的intern()
在JDK1.6中,intern()会把首次遇到的字符串实例复制在永久代中,返回的
也是永久代中这个字符串实例的引用,而StringBuilder创建的字符串实例
在java堆上,所以必然不是同一个引用,将返回false。而JDK1.7中的
intern()实现不会再复制实例,只是在常量池中记录首次出现的引用,因此
intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对
于str2返回false是因为'java'这个字符串在执行StringBuilder.toString()之前
已经出现过,字符串常量池已经有它的引用,不符合首次出现原则,而'计
算机软件'这个字符串则是首次出现返回true。