JVM原理及调优(一)
一、了解JVM
1. 1 JVM的基本结构
java虚拟机由五部分组成分别为:Java堆(Heap)、方法区(Method)、虚拟机栈(VM Stack)、本地方法区(Native MethodcStack)和程序计数器(Program Counter Register)。
在日常工作中我们经常将JVM 粗略概括为堆栈(Java堆和方法区),但事实上其组成上远比这复杂。(PS:JDk1.8的JVM)
1.1.1 Java堆
java堆是JVM管理的内存中最大的一块,该区域是所有内存共享的,在虚拟机启动时被创建。The heap is the runtime data area from which memory for all class instances and arrays is allocated。(java 虚拟机规范中原文) 此区域是用来存储各类生成的对象及数组等,在JVM8以后把运行时的常量池、静态变量也移到这里存储。
java堆可以细分为年轻代(Young Generation)、老年代(old Gen),而年轻代又可以分为Eden区、From Survivor、To Survivor 比例为 8:1:1。根据java虚拟机规范中的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续就可以,就像磁盘空间。在实现上,java堆既可以固定大小,又可以拓展,当前的java 虚拟机都是按照可拓展来实现的(通知设置-Xmx和-Xms),如果堆中有没有完成内存分配的实例,并且堆也无法再拓展,就会抛出OOM(OutOfMemoryError),因此java堆也是垃圾收集管理的主要区域。
下面我们就通过代码模拟java堆溢出:
import java.util.ArrayList;
import java.util.List;
/**
* 测试java堆溢出
* VM:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
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堆内存溢出是实际应用中常见的内存溢出情况。要解决这个区域的异常,一般先通过工具分析(比如Eclipse Memory Analzer)对Dump出来的文件进行分析,确认是内存泄露还是内存溢出。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。查明对象是通过怎么样的路径与GC roots相关联并导致垃圾回收无法自动回收它们,定位到相关联的代码。
如果是内存溢出,那就应当调整虚拟机的内存大小。
1.2 方法区
在1.8之前方法区中分为也被称为“永久代”,用于存储类信息、常量、静态变量、即时编译后的代码等数据,内存分配上也是使用jvm 中的内存。到了1.8之后,将常量、静态变量的存储移至java堆,使用‘元空间(MetaSpace)’的实现,不在共用JVM内存,直接使用系统内存,该区域也是线程共享的。
下面通过一段代码来看看方法区的内存溢出
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 方法区内存溢出
* VM: -XX:MaxMetaspaceSize=5M
*
*/
public class MetaOOM {
static class OOMObject{
}
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 o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,objects);
}
});
enhancer.create();
}
}
}
其运行结果为
该例子中使用运行时产生大量的类来填满方法区,直到溢出。该区域可以通过 -XX:MaxMetaspaceSize 设置大小,在不指定大小的情况小,虚拟机会耗尽所有可用的系统内存。
在实际应用中,如spring、Hibernate等在对类进行增强时都会使用到CGlib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的class可以载入内存。除了上述情况会导致方法区内存溢出,还有使用大量jsp或者动态产生jsp文件的应该(因为jsp第一次运行时需要编译为java类)。
1.3 java虚拟机栈
java虚拟机栈是线程私有的,它的生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈的入栈和出栈过程。
局部变量表存放了编译期可知的各种基本类型(boolean、byte、char、int、long、double、float)、对象引用和returnAddress类型。
当线程请求的栈深度大于虚拟机所允许的深度时,将抛出StackOverflowError异常;当拓展时无法申请到足够的内存时会抛出OOM异常。
/**
* 测试虚拟机栈溢出
* VM -Xss128k
*/
public class StackOFE {
private int stacklen=1;
public void stackleak(){
stacklen++;
stackleak();
}
public static void main(String[] args)throws Throwable {
StackOFE stackOFE=new StackOFE();
try{
stackOFE.stackleak();
}catch (Throwable e){
System.out.println("stack length:" +stackOFE.stacklen);
throw e;
}
}
}
运行结果为: