前情回顾
在本专栏的前12篇博客中, 我们主要大致介绍了什么是JVM, 并且详细介绍了class文件的格式。 对于深入理解Java, 或者深入理解运行于JVM上的其他语言, 深入理解class文件格式都是必须的。 如果读者对class文件的格式不是很熟悉, 在阅读本博客下面的文章之前, 建议先读一下前面的12篇博客, 或者参考其他资料, 熟悉class文件的格式。
在深入理解Java虚拟机到底是什么 这篇博客中, 我们有提到过, JVM就是一个特殊的进程, 我们执行的java程序, 都运行在一个JVM进程中, 这个进程的作用就是加载class文件, 并且执行class文件中的代码。 当然, 从一个class文件的加载, 到准备好可执行之前, 还有一段很长的路要走, 以后的文章会详细介绍这个过程。 既然虚拟机作为一个虚拟的计算机, 来执行我们的程序, 那么在执行的过程中, 必然要有地方存放我们的代码(class文件); 在执行的过程中, 总会创建很多对象, 必须有地方存放这些对象; 在执行的过程中, 还需要保存一些执行的状态, 比如, 将要执行哪个方法, 当前方法执行完成之后, 要返回到哪个方法等信息, 所以, 必须有一个地方来保持执行的状态。 上面的描述中, “地方”指的当然就是内存区域, 程序运行起来之后, 就是一个动态的过程, 必须合理的划分内存区域, 来存放各种数据。 所以, 在本文中, 将会详细介绍JVM的运行时数据区。
JVM体系结构和运行时数据区概述
要理解JVM的运行时数据区, 必须先要理解JVM的体系结构, 因为虚拟机的体系结构基本上解释了“为什么会有这些运行时数据区” 。 在深入理解Java虚拟机到底是什么 这篇文章中也简单的提到过JVM的体系机构, 这里再详细的讲解一下。 JVM的体系结构如下:
由此可见, 运行时数据区的划分, 是和JVM的体系结构相关的。 本文主要介绍运行时数据区的划分, 对体系结构不做深入的讲解。 简单概括一下, 类加载器子系统用于将class文件加载到虚拟机的运行时数据区中(准确的说应该是方法区) 。 可以认为执行引擎是字节码的执行机制, 一个线程可以看做是一个执行引擎的实例。 下面介绍运行时数据区:
JVM运行时数据区
方法区
在字面意思上, “方法区”这个词会让人产生误解。因为方法区存放的不只是方法, 它存放的是类型信息。我们在写程序的时候, 几乎总是在和类, 对象打交道, 我们知道根据一个类可以创建对象。 一般来说, 我们操纵的是对象, 访问对象的属性, 调用对象的方法等, 但是我们要思考这样一个问题, 虚拟机根据什么信息知道如何创建对象的呢? 当然是根据这个对象的类型信息, 但是这个类型信息在哪里呢?现在我们知道是在方法区中。 那么类型信息是被谁加载到方法区中的呢?由上面的体系结构图, 我们可以知道是类加载器子系统?那么所谓的类型信息, 都包含什么信息呢?这些信息又是如何存放的呢?这里的类型信息, 可以笼统的认为就是我们前面讲解过的一个class文件,类加载器子系统将会提取class文件里面的类型信息,并将这些类型信息存放到方法区中。 至于方法区中如何存放一个类型数据, 是和JVM的具体实现相关的。 但是不管如何实现, 一个类的类型信息总是会包含如下信息:类的全限定名
当前类的直接父类的全限定名
这个类是接口类型, 类类型, 还是枚举类型
类的访问修饰符信息
当前类型的超接口的全限定名
当前类型的常量池
字段信息
方法信息
如果对class文件格式比较熟悉的话, 可以看出, 这些信息都是在class文件中描述过的。 由于我们无法看到类型信息具体是如何存储的, 但是大致可以将类型信息看做一个class文件, 这有助于我们的理解。下面再次列出class文件结构的表格,读者可以对比class文件中的内容到类型数据上, 该表中的各种数据已经在前面的博客中详细讲解过:
类型数据中,除了这些基本信息外, 类型信息还包括以下两个方面:
一个到类的ClassLoader对象的引用
一个到表示该类的Class实例对象的引用
静态变量存储区
由于之前的博客中详细介绍过class文件的格式, 对上面的一些基本信息我们可能比较熟悉, 但是对这两种信息就比较陌生了。 其实说来也简单,每个class都是被一个类加载器加载到方法区的, 类型信息中的到类的ClassLoader对象的引用, 表明了当前的类是被哪个类加载器加载的, 这个信息同时也标示了当前的类型的名称空间。
每当一个class文件被成功的加载到方法区中, JVM总会创建一个Class对象, 来唯一标示这个类。 这个Class对象可以看做是类加载过程的产物, 由于它描述了整个类型信息, 而Java中的反射也是针对的类型信息, 所以这个Class对象是反射的基石, 大多数反射API都是根据Class对象来实现的。
而静态变量也是存在于类型信息中, 可以这么说, 类型信息中, 会有专门的区域存放类的静态变量。 与存在于对象中的实例变量不同, 静态变量存在于类型数据中, 每个类型只有一份,所以也叫类变量。
方法区是一个相对来说比较固定的内存区, 因为它存放的是类型信息, 而类型信息在被加载到方法区中之后, 除了必要的连接和初始化, 一般不会有较大改动,一般情况下, JVM也不会卸载类型信息, 所以方法区也可以称为JVM的静态区。 一个类型的生命周期一般就是整个程序的生命周期。 这也是为什么要慎用静态变量的原因所在, 因为静态变量随类型信息存放在方法区中, 生命周期很长, 如果使用不当, 很容易造成内存泄露。 一个JVM实例中只存在一个方法区, 方法区中的所有类型数据被所有线程共享。