2614 2017-10-01 2020-06-25
前言:学习Java虚拟机的第一步,是了解Java程序在运行过程中的内存结构。
一、Java内存区域
Java程序在运行过程中,其内存区域大致分为这么几大块,如下图
下面针对图中的各区域,进行详细说明。
二、线程隔离的数据区
如图所示,对Java内存区域细分,可分为这么两大块,分别是线程共享的内存区域和线程不共享的内存区域。其划分标准是该内存区域是否与线程同生共死,即以是否为单条线程的数据载体作为划分依据。我们先来看下线程不共享的内存区域。
1、程序计数器
程序计数器(Program Counter Register)是一块小区域,可以看作线程所执行的字节码的行号指示器。为了描述清晰,我们以代码为例,如下
// 简单地代码翻译成字节码理解
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
它的作用就是标识需要运行的代码,在这里可以理解为下面几个步骤
- 程序计数器先指到第一行,逻辑往下
- 接着指到第二行,逻辑往下
- 接着指到第三行,逻辑往上
- 接着指到第二行,逻辑继续往下
- 重复….
上面只是一个简单的例子,实际上,在字节码的层次上,代码逻辑上的分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。
需要注意的是如果某个线程正在执行Java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令地址;而如果正在执行的是Native方法,那么这个计数器为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2、Java虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。由于栈桢是虚拟机栈的栈元素,下面对栈桢的成员进行基本说明
- 局部变量表:存放了方法参数和方法内部定义的局部变量。
- 操作数栈:是一个后入先出(LIFO)栈,存放着待操作的元素。
- 动态链接:存放动态链接信息。
- 方法返回地址:存放着方法返回地址信息。
以代码为例,如下
public static int test(int i) {
int j = 1;
return i + j;;
}
我们可以这么看,当执行这个方法时,Java虚拟机栈对代表该方法的栈桢进行入栈,入栈后对栈桢进行操作;当这个方法运行结束时,即代表Java虚拟机栈对该栈桢操作完成,该栈桢出栈。
3、本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别是本地方法栈为Native方法服务。值得一提的是,在HopSpot虚拟机中(JDK默认的虚拟机),直接把本地方法栈和虚拟机栈合二为一了。
三、线程共享的数据区
线程共享的内存区域,意味着这些内存区域是为整个Java内存区域服务的,意味着,每条线程都可以访问。
1、Java堆
Java堆是Java虚拟机所管理内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有对象的实例都在这里分配内存,也被称为GC堆。Java堆是垃圾收集器管理的主要区域(后面再详细介绍)。
2、方法区
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也叫非堆(Non-Heap),另外在HopSpot虚拟机上,又称为永久代(Permanent Generation)(后面再详细介绍)。
3、运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期(.java到.class的过程)生成的各种字面量和符号引用,这部分内容将在类加载后存入方法区的运行时常量池。其中
- 字面量:如文本字符串、声明为final的常量值等
- 符号引用 :大致包含三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
以代码为例,如下
public class Test extends Test1 implement Test2 {
private String str = "123";
private Test3 test3;
public static void test(){}
}
那么在编译过后,这个类的所有元素(类声明、字段声明、方法声明),都是被详细保存进入了运行时常量池。
四、对象的本质
在了解完Java的各内存区域后,接下来,我们深入了解一下对象创建的大致过程。这里以HotSpot虚拟机为例。
1、分配内存
当Java虚拟机确定要创建一个对象时,首先要为新生对象分配内存空间。分配内存的任务等同于把一块确定大小的内存从Java堆中划分出来(类加载后便可完全确定)。之后,虚拟机需要将分配到的内存空间都初始化为零值(这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值)。
2、内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中
- 对象头:分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;另一部分是类型指针,即对象指向它的元数据的指针,虚拟机通过这个确定这个对象是哪个类的实例(类信息存储在方法区中),如下图。
- 实例数据:是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。
- 对齐填充:并不是必然存在的,只是一个为了迎合虚拟机对字节码的规范,起一个占位符的作用。
3、访问
建立对象是为了使用对象,我们的Java程序需要通过Java虚拟机栈上的reference数据来操作Java堆上的具体对象。在HotSpot虚拟机上,采用的是直接指针访问法,即reference中存储的就是对象地址,而对象地址里面存储着指向对象类型数据的指针。因此,一个reference是可以完全确定一个对象的,过程如下图
4、小结
通过上面,我们知道一个对象的创建,大致是这样的
- 首先Java虚拟机为对象划分确定数量的内存,并对这些内存进行初始化(赋零值)。
- 分配内存后,按照Java虚拟机的规定,把对象的信息写进相应的内存区域。
- 写完后,虚拟机栈可以通过这个对象的引用,来操作这个对象。
五、内存溢出
1、内存限制参数
参数 | 说明 |
---|---|
-Xms | 设置堆的最小值,如-Xms20m,设置堆最小值为20M |
-Xmx | 设置堆的最大值,当最小堆值等于最大堆值时,堆不会自动扩展, |
-Xmn | 设置新生代的容量,如-Xmn20m,HotSpot默认Eden:Survivor = 8:1,新生代:老年代=1:2 |
-Xss | 设置栈内存的容量,如-Xms20m,设为栈内存容量为20M |
-XX:+HeapDumpOnOutOfMemoryError | 出现内存溢出异常时,导出内存堆快照 |
本节的目的有两个:
- 通过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容。
- 对内存溢出有基本的认识,包括什么样的代码可能导致内存溢出,及如何解决内存溢出的问题。
以下代码结果基于JDK1.8,HotSpot虚拟机。由于条件限制,列举两个。
2、Java堆溢出
原理:限制Java堆的大小,通过不断地创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后,就会产生内存溢出异常。验证代码如下
/**
* VM Args:-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.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10172.hprof ...
Heap dump file created [27963640 bytes in 0.394 secs]
3、虚拟机栈溢出
原理:一个方法的执行,就代表一个栈桢出入虚拟机栈的过程。通过方法自身嵌套式的调用,可以使得栈桢深度超出Java虚拟机栈的深度,从而产生溢出。验证代码如下
/**
* VM Args:-Xss128k
* HotSpot虚拟机中不区分虚拟机栈和本地方法栈
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
}catch(Throwable e) {
System.out.println("stack length : " + oom.stackLength);
throw e;
}
}
}
// 结果如下
stack length : 1625
Exception in thread "main" java.lang.StackOverflowError
at first.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at first.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at first.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
六、说明
本系列博客(Java虚拟机系列)参考自《深入理解Java虚拟机》-周志明。感谢作者的知识分享、耕耘创作!
总访问次数: 294次, 一般般帅 创建于 2017-10-01, 最后更新于 2020-06-25
欢迎关注微信公众号,第一时间掌握最新动态!