声明: 《深入理解Java虚拟机 JVM高级特性与最佳实践 第2版》。以下内容来自书中第二章。
1. JVM概述
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行 ——百度百科
可以将JVM理解成一台机器,这个机器可以用来执行java程序。这样java代码就可以实现一次编写,到处运行了。在不同的机器上配置相应的JVM即可。 在机器上,JVM实际上也是一个程序。
2. JVM内存分配
2.1 运行时数据区域
根据《Java 虚拟机规范(Java SE 7 版)》的规定,Java虚拟机所有管理的内存将会包括以下几个运行时数据区域,如下图所示:
简单说一下各个数据区域的作用:
线程私有的数据区域:线程私有区域都是与线程同生共死的,即生命周期与线程相同。
线程共享的数据区域:生命周期与JVM相同
程序计数器:每个线程都有的"小本本",线程不是连续不断工作的。再次运行时它就要看看“小本本”里的内容才知道自己该从什么地方继续做下去。
在JVM中的多线程是通过轮流切换的方式执行的,在任何时刻一个CPU(相当于多核CPU的一个核)只能运行一个线程。如果某个进程的某个线程占用CPU很长的时间,那么其他的线程会一直等下去吗?为了让用户不会有:”哇!我这个傻X计算机怎么这么卡啊!“的错觉。CPU就给了每个线程分配了一个CPU时间,当线程的CPU时间用完之后,他就会把CPU的计算资源让出来给其他线程,等到下次轮到它的时候它再执行。所以每个线程都需要一个私有的程序计数器来记录自己执行到哪儿了。它实际可以理解成一个记录程序执行的字节码的行号的指示器。 计数器只会占用内存中很小的一部分空间。
Java虚拟机栈:程序员口中常说的“堆栈”大抵说的就是这个区域中的局部变量表。虚拟机栈是用来描述Java方法执行时的内存模型。这种描述是通过存储方法开始执行(入栈)到方法结束(出栈)过程中的局部变量表、操作数栈、动态链接及方法的出口等信息来是实现的。方法在执行时会创建一个栈帧,用来存储前面说的各种信息,方法完成,栈帧出栈。
局部变量表存放了编译期间可知的各种基本数据类型和引用类型,以前上课的时候说的堆栈的时候,老师可能会画这样的一张图:
Variable | Value |
---|---|
variable1 | 13 |
variable2 | 2.3 |
variable3 | 'a' |
variable4 | true |
zhangsan(Student的一个实例) | 指向堆中张三实例的地址空间 |
说的就是这个局部变量表。局部变量表所需要的内存空间在编译期间就分配完成。
以上可以对栈的作用做个小小的结论:
- 只有在方法调用时,才为当前栈分配一个帧,然后将该帧压入栈。
- 帧中存放了方法的局部变量表,当方法执行完之后,对应的帧则从栈中弹出。
JVM规范中,对这个数据区域规定了两种异常状况:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError:虚拟机栈动态扩展时无法申请都足够的内存空间
本地方法栈:与虚拟机栈相似,不过虚拟机栈是为JVM执行java方法,而本地方法栈则是为虚拟机使用到的Native方法服务。
本地方法栈也可能会出现StackOverflowError和OutOfMemoryError。
Java堆:对大多数应用来说,这个区域是JVM所管理的最大的内存空间,也是GC重点关注的区域。该区域被线程共享,存在的目的就是为了存放对象实例,几乎所有的对象实例和数组都要在堆上分配。如果Java堆中没有空间可以用来实例化对象,而且也没法再申请新的内存时,该区域会抛出OutOfMemoryError。
方法区:线程共享区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器后的代码等数据。这个区域也有人称其为永久代。方法区无法满足内存分配需求时也会抛出OutOfMemoryError。 运行时常量池也是方法区中的一部分,class文件会包括类的版本,字段,方法,接口等描述信息,也会有个常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载之后进入到方法区的运行时常量池中存放。该区域具有动态性,即常量不一定是编译期间就确定的,在运行期间也可以有新的常量产生,进入运行时常量池中。
3. 异常代码实战
实战一下数据提供的代码,旨在对运行时数据区域内存分配和使用有更深的理解,当出现相关异常的时候能够快速地定位到异常区域和异常代码。
首先了解一下IDEA如何配置JVM启动时的参数。Run—>EditConfigurations
JVM相关配置参数说明可以通过在CMD中通过java -X
查看
Java 堆溢出
Java堆是用来存储对象实例和数组的,只要的不停的创建对象,且确保对象不会被回收,当对象的数量达到堆最大的容量限制后,就会产生内存溢出异常了。下面的JVM参数设置了Java堆内存的大小为20MB,不可扩展(将最大值-Xms和最小值-Xmx设为一样即可避免堆自动扩展)。通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照,以便我们后面的分析。
JVM参数配置
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\heapdump
代码如下:
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//不停地创建对象,直到OOM
while(true){
list.add(new OOMObject());
}
}
}
程序运行结果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\\heapdump\java_pid8948.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at per.ling.JVMPractice.HeapOOM.main(HeapOOM.java:13)
Heap dump file created [28866091 bytes in 0.206 secs]
使用JDK自带的内存映像分析工具jvisualVM来分析程序dump出来的堆转储快照。装入文件后点击类,我们可以看到类名和它对应的实例个数及占用的内存空间。
打开该文件可以看到OOMObject这个类有810326个实例,占用内存13M左右。这里我们可以看到类实例相关的情况,查看概要我们还可以看到相关的线程及可能出现异常的代码块。
如果是内存泄露的话,我们可能还需要观察一下相关的GC Roots。
Java的内存溢出有很多中情况,刚兴趣可以搜一下,学习一波。
虚拟机栈溢出
虚拟机栈可能抛出的异常:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError:虚拟机堆栈动态扩展时无法申请都足够的内存空间
JVM参数如下:
-Xss128k
-XX:+HeapDumpOnStackOverflowErrow
-XX:HeapDumpPath=E:\\heapdump
代码如下:
public class StackOOM {
private static long stackLength = 0L;
public static void main(String[] args) {
try {
stackLeak();
} catch (Throwable e){
System.out.println("The length of statck is " + stackLength);
throw e;
}
}
private static void stackLeak() {
stackLength++;
stackLeak();
}
}
控制台输出如下:
The length of statck is 41351
Exception in thread "main" java.lang.StackOverflowError
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
小结:
StackOverflowerError主要是针对运行时的栈而言,而OutOfMemoryError(内存溢出)针对的是整个内存区域。后者出现的原因主要是申请内存时没有更多的内存空间导致的,而导致这样的原因有很多。
详情可以参见: Java常见的几种内存溢出及解决方案