概述
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new的操作去写对应的内存管理操作,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存一切都看起来很美好。不过,也正是因为我们把内存控制的权利交给了虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎么样使用内存的,那么排查错误将会变得特别的困难。
1、JVM的组成
-
类加载器(ClassLoader)
-
运行时数据区(Runtime Data Area)
-
执行引擎(Execution Engine)
-
本地库接口(Native Interface)
运行过程: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),
将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(NativeInterface)来实现整个程序的功能
2、java自动内存管理机制
1、运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。根据1.8 java虚拟机规范,jvm在运行时主要分为以下几个运行时内存区域:
堆和方法区是线程共享的数据区,虚拟机栈,本地方法栈,程序计数器是线程私有的数据区。
1.程序计数器
线程私有。可看作是 当前线程所执行的字节码的行号指示器 ,程序计数器的工作是通过改变这个计数值来读取下一条要执行的字节码指令。多线程是通过线程轮流切换并分配处理器执行时间来实现的,任何一个时刻,一个内核只能执行一条线程中的指令。 为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器 。这就是一开始说的“线程私有”。如果线程正在执行的方法是Java方法,计数器记录的是虚拟机字节码的指令地址;如果是Native方法,计数器值为空。
程序计数器是唯一一个在Java虚拟机规范中没有规OOM(OutOfMemoryError)情况的区域。
2. Java虚拟机栈
每个方法在执行时都会创建一个栈帧,线程私有,生命周期和线程相同。Java虚拟机栈描述的是Java方法的内存模型:每个方法在执行时都会创建一个栈帧,存储 局部变量表、操作数栈、动态链接、方法出口信息 ,每一个方法从调用到结束,就对应这一个栈帧在虚拟机栈中的进栈和出栈过程。局部变量表保存了各种基本数据类型(int、double、char、byte等
)、对象引用(不是对象本身)和returnAddress类型(指向了一条字节码地址)。栈的空间大小设置: -Xss 为jvm启动的每个线程分配的内存大小。
线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError;
虚拟机栈扩展时无法申请到足够的内存,抛出OutOfMemoryError。
下面代码演示一下栈溢出和内存溢出。
StackOverflowError
/**
* 栈超出最大深度:StackOverflowError
* VM args: -Xss128k
**/
public class StackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOF stackSOF = new StackSOF();
try {
stackSOF.stackLeak();
} catch (Throwable e) {
System.out.println("当前栈深度:" + stackSOF.stackLength);
e.printStackTrace();
}
}
}
异常信息:
java.lang.StackOverflowError
当前栈深度:30170
at cn.itcast.jvm.StackSOF.stackLeak(StackSOF.java:11)
at cn.itcast.jvm.StackSOF.stackLeak(StackSOF.java:11)
方法递归调用,造成深度过深,产生异常
OutOfMemoryError
/**
* 栈内存溢出: OOM
* VM Args: -Xss2m
**/
public class StackOOM {
private void dontStop(){
while (true){
}
}
public void stackLeakByThread(){
while(true){
Thread t = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
t.start();
}
}
public static void main(String[] args) {
StackOOM stackOOM = new StackOOM();
stackOOM.stackLeakByThread();
}
}
异常信息:
Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
单线程下栈的内存出现问题了,都是报StackOverflow的异常,只有在多线程的情况下,当新创建线程无法在分配到新的栈内存资源时,会报内存溢出。
3. 本地方法栈
上述虚拟机栈为JVM执行Java方法服务,本地方法则为执行Native服务。其他和虚拟机栈类似,也会抛出StackOverflowError、OutOfMemoryError。
4. Java堆
常说的“栈内存”、“堆内存”,其中前者指的是虚拟机栈,后者说的就是Java堆了。 Java堆是被线程共享的 。在虚拟机启动时被创建。Java堆是Java虚拟机所管理的内存中最大的一块。Java堆的作用是存放对象实例,Java堆可以处于物理上不连续的内存空间中,只要求逻辑上连续即可。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作"GC堆",从内存回收的角度看,现在收集器都基本采用分代回收的算法 所以Java堆还可以细分为:新生代、老年代。 在细致一点的有:Eden空间、From
Survivor空间、To Survivor空间。
堆的空间大小设置: -Xms java堆启动时内存 -Xmx java堆可扩展的最大内存。
内存泄漏 :Memory Leak 一个无用的对象,应该被回收,却因为某种原因一直未被回收。
内存溢出 :Memory Overflow 对象确实都应该活着,这个时候内存不够用了。
堆内存溢出代码示例:
/**
* 堆内存溢出演示
* VM Args: -Xms20m -Xmx20m
**/
public class HeapOOM {
public static void main(String[] args) throws InterruptedException {
List<byte[]> list = new ArrayList<byte[]>();
int i=0;
while (true){
list.add(new byte[1024]);
}
}
}
报错信息:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid18828.hprof ...
Heap dump file created [20924845 bytes in 0.060 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at cn.itcast.jvm.HeapOOM.main(HeapOOM.java:15)
- 方法区
也被称为永久代,是线程共享的区域, 1.8之后也被称为元空间。存储已被虚拟机加载的类信息、常量、静态变量、即是编译器编译后的代码等数据。方法区无法满足内存分配需求时,抛出OutOfMemoryError。JVM规范没要求这个区域需要实现垃圾收集,因为这个区域回收主要针对的是类和常量池的信息回收,回收结果往往难以令人满意。
运行时常量池:是方法区的一部分。Java语言不要求常量只能在编译期产生,换言之,在运行期间也能将新的常量放入。
方法区空间大小设置: -XX:PermSize -XX:MaxPermSize
1.8之后设置: -XX:MetaspaceSize -XX:MaxMetaspaceSize 启动内存和最大内存设置相等。
代码演示方法区内存溢出:
/**
* 方法区 OOM
* VM Args:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
**/
public class PermOOM {
public static void main(final String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(PermOOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,args);
}
});
enhancer.create();
}
}
}
报错日志:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:386)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at cn.itcast.jvm.PermOOM.main(PermOOM.java:24)
Process finished with exit code 1
Cglib动态代理可以动态创建代理类,这些代理类的Class会动态的加载入内存中,存入到方法区。所以当我们把方法区内存调小后便可能会产生方法区内存溢出。