阅读文本大概需要3分钟。
0x01:Java虚拟机栈和本地方法栈溢出
由于在Hotspot虚拟机中中不区分虚拟机栈和本地方法栈,因此通过-Xoss修改参数是无效的,可以通过修改-Xss设定。
- 如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。
这两种异常有一些重叠的部分:当栈空间无法继续分配时,到底是内存太小,还是已经使用的栈空间过大,其本质只是对同一件事情的两种不同描述。
可以通过以下方法验证:
- 在使用-Xss参数减少栈内存容量,结果抛出Stack OverflowError异常,异常出现时输出的堆栈深度相应缩小。
- 定义了大量的本地变量,增大此方法栈中本地变量表的长度,结果抛出Stack OverflowError异常时输出的堆栈深度相应缩小。
可以通过递归调用的方式进行测试:
代码语言:javascript复制public void stackLeak() {
stackLeak();
}
通过不断建立线程的方式可以生产内存异常异常,但是产生的内存异常异常和栈空间是否足够大并不存在任何关联,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出。
操作系统为虚拟机分配的内存是有限制的,如果虚拟机进程本身消耗的内存计算在内,剩余的内存就由虚拟机栈和本地方法栈瓜分了,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
如果是建立线程过多导致内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
可以通过死循环创建线程的方式模拟“由于线程过多导致的内存溢出”:
代码语言:javascript复制while(true){
Thread t = new Thread(new Runable(){
......
});
}
0x02:Java堆内存溢出
可以通过不停的创建对象来造成堆内存溢出
代码语言:javascript复制public static void main(String[] args) {
List list = new ArrayList<>();
while(true) {
list.add(new ObjectBIg())
}
}
使用-XX: HeapDumpOnOutOfMemoryError可以在虚拟机在出现内存溢出异常时Dump出当前的内存堆转存储快照以便后续进行分析。
对Dump快照进行分析,需要区分出到底是内存泄漏Memory Leak还是内存异常Memory Overflow。
如果是内存泄漏,进一步通过工具对GC Root的引用链进行分析。
如果不是内存泄漏,就是内存中的对象确实都还必须存活,那就应该修改虚拟机参数Xmx Xms,同时判断是否可以通过调大物理内存的方式解决。然后从代码角度检测是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的消耗。
0x03: 方法区和运行时常量池溢出
由于运行时常量池属于方法区的一部分,因此两个区域放在一块执行。
String.intern()是一个Native方法,它的作用是如果字符串常量池中已经包含了此String对象的字符串,则返回代表池中这个字符串的String对象;否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
可以通过以下代码测试运行时常量池溢出:
代码语言:javascript复制public class Test {
public static void main(String[] args) {
int i =0;
List<String> list = new ArrayList();
while(true) {
list.add(String.valueOf(i ).intern());
}
}
}
可以在抛出的异常后面发现“Perm space”信息。
可以使用String.intern()测试运行时常量池:
代码语言:javascript复制public class Test1 {
public static void main(String[] args) {
String str1 = new StringBuilder("111").append("-222").toString();
System.out.println(str1.intern()==str1);
String str2= new
StringBuilder("jav").append("a").toString();;
System.out.println(str2.intern()==str2);
}
}
结果:
true
false
JDK1.7中的intern实现不会复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。
对str2比较返回false是因为“java”字符串在执行StringBuilder.toString()之前已经出现过了,字符串常量池中已经有它的引用了,不符合“首次出现”的原则。
方法区用于存放Class相关的信息,如类名、访问修饰符、常量池、字段描述、方法描述等,对于这些区域的测试,基本的思路是运行时产生大量的类填充方法区,直到溢出。
可以借助GCLib直接操作字节码运行时产生大量的动态类:
代码语言:javascript复制public class Test1 {
public static void main(final String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMOBject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(objects,args);
}
});
enhancer.create();
}
}
static class OOMOBject{
}
}
除了GCLib字节码增强和动态语言之外,常见的还有大量JSP或者动态生成JSP文件的应用、基于OSGi的应用等
另外:程序计数器是JVM唯一不会发生内存溢出的区域。