2614 2017-10-01 2020-06-25

前言:学习Java虚拟机的第一步,是了解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

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!