你们好,相信大部分Javaer在code时常常会遇到本地代码运行正常,但在生产环境偶尔会莫名其妙的报一些关于内存的异常,StackOverFlowError,OutOfMemoryError异常是最多见的。今天就基于上篇文章JVM系列之Java内存结构详解讲解的各个内存区域重点实战分析下内存溢出的状况。在此以前,我仍是想多余累赘一些其余关于对象的问题,具体内容以下:java
文章结构编程
- 对象的建立过程
- 对象的内存布局
- 对象的访问定位
- 实战内存异常
关于对象的建立,第一反应是new关键字,那么本文就主要讲解new关键字建立对象的过程。多线程
Student stu =new Student("张三","18");复制代码
就拿上面这句代码来讲,虚拟机首先会去检查Student这个类有没有被加载,若是没有,首先去加载这个类到方法区,而后根据加载的Class类对象建立stu实例对象,须要注意的是,stu对象所需的内存大小在Student类加载完成后即可彻底肯定。内存分配完成后,虚拟机须要将分配到的内存空间的实例数据部分初始化为零值,这也就是为何咱们在编写Java代码时建立一个变量不须要初始化。紧接着,虚拟机会对对象的对象头进行必要的设置,如这个对象属于哪一个类,如何找到类的元数据(Class对象),对象的锁信息,GC分代年龄等。设置完对象头信息后,调用类的构造函数。
其实讲实话,虚拟机建立对象的过程远不止这么简单,我这里只是把大体的脉络讲解了一下,方便你们理解。并发
刚刚提到的实例数据,对象头,有些小伙伴也许有点陌生,这一小节就详细讲解一下对象的内存布局,对象建立完成后大体能够分为如下几个部分:函数
对象头: 对象头中包含了对象运行时一些必要的信息,如GC分代信息,锁信息,哈希码,指向Class类元信息的指针等,其中对Javaer比较有用的是锁信息与指向Class对象的指针,关于锁信息,后期有机会讲解并发编程JUC时再扩展,关于指向Class对象的指针其实很好理解。好比上面那个Student的例子,当咱们拿到stu对象时,调用Class stuClass=stu.getClass();的时候,其实就是根据这个指针去拿到了stu对象所属的Student类在方法区存放的Class类对象。虽说的有点拗口,但这句话我反复琢磨了好几遍,应该是说清楚了。^_^工具
实例数据:实例数据部分是对象真正存储的有效信息,就是程序代码中所定义的各类类型的字段内容。布局
对齐填充:虚拟机规范要求对象大小必须是8字节的整数倍。对齐填充其实就是来补全对象大小的。post
谈到对象的访问,还拿上面学生的例子来讲,当咱们拿到stu对象时,直接调用stu.getName();时,其实就完成了对对象的访问。但这里要累赘说一下的是,stu虽然一般被认为是一个对象,其实准确来讲是不许确的,stu只是一个变量,变量里存储的是指向对象的指针,(若是干过C或者C++的小伙伴应该比较清楚指针这个概念),当咱们调用stu.getName()时,虚拟机会根据指针找到堆里面的对象而后拿到实例数据name.须要注意的是,当咱们调用stu.getClass()时,虚拟机会首先根据stu指针定位到堆里面的对象,而后根据对象头里面存储的指向Class类元信息的指针再次到方法区拿到Class对象,进行了两次指针寻找。具体讲解图以下:
测试
内存异常是咱们工做当中常常会遇到问题,但若是仅仅会经过加大内存参数来解决问题显然是不够的,应该经过必定的手段定位问题,究竟是由于参数问题,仍是程序问题(无限建立,内存泄露)。定位问题后才能采起合适的解决方案,而不是一内存溢出就查找相关参数加大。优化
概念
- 内存泄露:代码中的某个对象本应该被虚拟机回收,但由于拥有GCRoot引用而没有被回收。关于GCRoot概念,下一篇文章讲解。
- 内存溢出: 虚拟机因为堆中拥有太多不可回收对象没有回收,致使没法继续建立新对象。
在分析问题以前先给你们讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,那么咱们怎么知道JVM运行时的各类信息呢,Dump机制会帮助咱们,能够经过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,而后经过外部工具(做者使用的是VisualVM)来具体分析异常的缘由。
下面从如下几个方面来配合代码实战演示内存溢出及如何定位:
/** VM Args: //这两个参数保证了堆中的可分配内存固定为20M -Xms20m -Xmx20m //文件生成的位置,做则生成在桌面的一个目录 -XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,做则生成在桌面的一个目录 //文件生成的位置,做则生成在桌面的一个目录 -XX:HeapDumpPath=/Users/zdy/Desktop/dump/ */
public class HeapOOM {
//建立一个内部类用于建立对象使用
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//无限建立对象,在堆中
while (true) {
list.add(new OOMObject());
}
}
}复制代码
Run起来代码后爆出异常以下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/zdy/Desktop/dump/java_pid1099.hprof ...
能够看到生成了dump文件到指定目录。而且爆出了OutOfMemoryError,还告诉了你是哪一片区域出的问题:heap space
打开VisualVM工具导入对应的heapDump文件(如何使用请读者自行查阅相关资料),相应的说明见图:
分析dump文件后,咱们能够知道,OOMObject这个类建立了810326个实例。因此它能不溢出吗?接下来就在代码里找这个类在哪new的。排查问题。(咱们的样例代码就不用排查了,While循环太凶猛了)
老实说,在栈中出现异常(StackOverFlowError)的几率小到和去苹果专卖店买手机,买回来后发现是Android系统的几率是同样的。由于做者确实没有在生产环境中遇到过,除了本身做死写样例代码测试。先说一下异常出现的状况,前面讲到过,方法调用的过程就是方法帧进虚拟机栈和出虚拟机栈的过程,那么有两种状况能够致使StackOverFlowError,当一个方法帧(好比须要2M内存)进入到虚拟机栈(好比还剩下1M内存)的时候,就会报出StackOverFlow.这里先说一个概念,栈深度:指目前虚拟机栈中没有出栈的方法帧。虚拟机栈容量经过参数-Xss来控制,下面经过一段代码,把栈容量人为的调小一点,而后经过递归调用触发异常。
/** * VM Args: //设置栈容量为160K,默认1M -Xss160k */
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:751
Exception in thread "main" java.lang.StackOverflowError
能够看到,递归调用了751次,栈容量不够用了。
默认的栈容量在正常的方法调用时,栈深度能够达到1000-2000深度,因此,通常的递归是能够承受的住的。若是你的代码出现了StackOverflowError,首先检查代码,而不是改参数。
这里顺带提一下,不少人在作多线程开发时,当建立不少线程时,容易出现OOM(OutOfMemoryError),这时能够经过具体状况,减小最大堆容量,或者栈容量来解决问题,这是为何呢。请看下面的公式:
线程数*(最大栈容量)+最大堆值+其余内存(忽略不计或者通常不改动)=机器最大内存
当线程数比较多时,且没法经过业务上削减线程数,那么再不换机器的状况下,你只能把最大栈容量设置小一点,或者把最大堆值设置小一点。
写到这里时,做者原本想写一个无限建立动态代理对象的例子来演示方法区溢出,避开谈论JDK7与JDK8的内存区域变动的过渡,但细想想,仍是把这一块从始致终的说清楚。在上一篇文章中JVM系列之Java内存结构详解讲到方法区时提到,JDK7环境下方法区包括了(运行时常量池),其实这么说是不许确的。由于从JDK7开始,HotSpot团队就想到开始去"永久代",你们首先明确一个概念,方法区和"永久代"(PermGen space)是两个概念,方法区是JVM虚拟机规范,任何虚拟机实现(J9等)都不能少这个区间,而"永久代"只是HotSpot对方法区的一个实现。为了把知识点列清楚,我仍是才用列表的形式:
下面做者仍是经过一段代码,来不停的建立Class对象,在JDK8中能够看到metaSpace内存溢出:
/** 做者准备在JDK8下测试方法区,因此设置了Metaspace的大小为固定的8M -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m */
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
//无限建立动态代理,生成Class对象
enhancer.create();
}
}
static class OOMObject {
}
}复制代码
在JDK8的环境下将报出异常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
这是由于在调用CGLib的建立代理时会生成动态代理类,即Class对象到Metaspace,因此While一下就出异常了。
提醒一下:虽然咱们平常叫"堆Dump",可是dump技术不只仅是对于"堆"区域才有效,而是针对OOM的,也就是说无论什么区域,凡是可以报出OOM错误的,均可以使用dump技术生成dump文件来分析。
在常常动态生成大量Class的应用中,须要特别注意类的回收情况,这类场景除了例子中的CGLib技术,常见的还有,大量JSP,反射,OSGI等。须要特别注意,当出现此类异常,应该知道是哪里出了问题,而后看是调整参数,仍是在代码层面优化。
直接内存异常很是少见,并且机制很特殊,由于直接内存不是直接向操做系统分配内存,并且经过计算获得的内存不够而手动抛出异常,因此当你发现你的dump文件很小,并且没有明显异常,只是告诉你OOM,你就能够考虑下你代码里面是否是直接或者间接使用了NIO而致使直接内存溢出。
好了,"JVM系列之实战内存溢出异常"到这里就给你们介绍完了,Have a good day .欢迎留言指错。
往期入口: