JVM学习——解析Java虚拟机运行时数据区
1 运行时数据区概览
来一张图大概看一下JVM运行时数据区的情况,下面我将仔细的介绍各个区域
2 运行时数据区
2.1 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程(每个线程都有自己的程序计数器)所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程执行的是一个java方法,那么这个线程记录的就是正在执行的虚拟机字节码指令的地址。如果是Native方法,那么计数器为空(Undefined)。
2.2 Java虚拟机栈
Java虚拟机栈也是线程私有的,生命周期与线程相同。具体结构如图所示:
2.2.1 局部变量表
存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和remoteAddress类型(指向了一个字节码指令的地址)
其中64位的long和double类型的数据会占用2个局部变量 空间(slot),其余的数据类型只占用一个空间
局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
2.2.2 操作数栈
用于在方法运行时可以存放以及获取操作数(比如一个加法指令,需要将两个数据压入栈中,进行加法操作后又需要将结果压入操作数栈)。其所需要的最大栈深度也是在编译期间定下的,在方法刚开始运行的时候,操作数栈是空的。
2.2.3 动态链接
在方法区的常量池中会存储方法的符号引用,每个栈帧中都会存储一个引用,用于指向常量池中该方法对应的符号引用,字节码指令中方法的调用就是通过符号引用来进行的。在类加载阶段的解析阶段,部分符号引用会被解析为直接引用,称为静态解析(条件为在调用前就有一个可确定的调用版本)。在方法运行过程中,另一部分的符号引用会被实时解析成直接引用,称为动态链接
2.2.4 返回地址
方法的运行过程中,可能会正常退出,也可能会异常退出,不论是哪种退出方式,在退出后都会要保证其上层调用者可以知道方法退出的位置,以便于程序继续执行,方法的返回地址就是用于确定退出位置的。
2.3 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们的区别在于虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。(HotSpot中将本地方方法栈与虚拟机栈合二为一)
2.4 Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。对象实例与数组都要在堆上分配(也不是那么绝对了。逃逸分析)。是垃圾搜集器管理的主要区域。
2.5 方法区
方法区也是一个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.5.1 运行时常量池
运行时常量池(相对于Class文件常量池的另外一个重要特征是具备动态性,并不一定是编译期才能产生,String的intern()方法)是方法区的一部分。
class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息也是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
3 从JVM角度思考类的各种变量的存放位置
3.1 成员变量
3.1.1 类变量(static修饰)
在类加载的过程中的准备阶段
正式为类变量分配内存并设置类变量的初始值
这些变量所使用的内存都将在方法区进行分配
3.1.2 实例变量
实例变量将在对象实例化时随着对象一起分配到Java堆上
3.2 局部变量
存放在Java栈帧中的局部变量表中,每个栈帧中的局部变量可能不同。JVM通过索引的方式来访问变量表中的变量。
3.3 一些思考
3.3.1 静态方法与非静态方法
在学习Java反射的时候存在着这样一个例子
public class A {
public static void staticMethod() {
System.out.println("我是静态方法");
}
public void normalMethod() {
System.out.println("我是一般方法");
}
}
public class ReflectTest {
public static void main(String[] args) throws Throwable {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?> aClass = classLoader.loadClass("model.A");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
A a = (A) declaredConstructor.newInstance();
Method setA = aClass.getMethod("setId",int.class);
Method staticMethod = aClass.getMethod("staticMethod");
//未传入对象都可以调用静态方法
staticMethod.invoke(null);
Method normalMethod = aClass.getMethod("normalMethod");
//必须明确指定调用哪个对象的非静态方法
normalMethod.invoke(a);
}
}
那出现这种状况的原因是什么呢?非静态方法与静态方法有一个很重大的不同:
非静态方法有一个隐含的传入参数,该参数是JVM给它的,这个参数是什么呢?参数具体就是指向对象实例的,存储在栈上的指向实例的地址。有了这个地址,非静态方法才能找到在堆上实例对象中的实例变量的值。所以在调用非静态方法时,必须指定实例。
静态方法不需要这个参数,它是属于类的,而不是属于某个对象的,在加载类时,就会为静态方法分配内存,不依赖于对象,先于对象存在,直接通过类名.静态方法及可访问。
3.3.2 this
this不能访问类变量。this指向的是对象,在Java堆中的对象有实例变量,所以可以用this来指向相应的变量。但是类变量存放在方法区,所以this不能找到类变量。