我是菜鸟:虚拟机执行子系统

类文件结构

java平台无关性的原因:虚拟机以及虚拟机使用统一的存储结构-字节码。

JVM类加载机制

JVM把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。这就是jvm的类加载机制。
Java中类的加载,连接和初始化都是在程序运行期间完成。(好处:增加了应用程序的灵活性;缺点:增加性能开销)

类的加载时机

类的生命过程:
加载, 验证,准备, 解析,初始化,使用和卸载。其中验证,准备和解析叫做连接阶段。其中加载,验证,准备,初始化和卸载必须是按照顺序,而解析可以在初始化后再开始。
这里写图片描述
对于类的加载,验证,准备和解析的时刻没有明确的规定,但是对类的初始化有着明确的规定:
1. 遇到new, getstatic, putstatic和invokestatic这4条字节码指令的时候。若类没有初始化,则先进行初始化。(对应在java代码中, new, 读取或者设置一个类的静态字段以及调用一个类的静态方法。)
2. 使用java.lang.reflect对类进行反射调用时;
3. 若初始化一个类的时候,其父类还未初始化,那么先初始化其父类;
4. 当虚拟机启动的时候,用户需要指定一个需要执行的主类,虚拟机会先初始化这个类。
具体的内容包括:
. 创建一个Java类的实例。如
MyClass obj = new MyClass()
. 调用一个Java类中的静态方法。如
MyClass.sayHello()
. 给Java类或接口中声明的静态域赋值。如
MyClass.value = 10
. 访问Java类或接口中声明的静态域,并且该域不是常值变量。如
int value = MyClass.value
. 在顶层Java类中执行assert语句。
初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。

加载

在加载的时候,虚拟机完成了如下步骤:
1. 获取此类的2进制字节流;
2. 将静态存储结构转化为方法区运行时数据结构;
3. 在java堆中生成代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。

验证

确保输入的字节流符合当前jvm的要求,主要包括如文件格式的验证,元数据验证,字节码验证和符号引用验证。

准备阶段

正式为类变量分配内存,并设置类变量(static变量,不包括实例变量)初始值的阶段。这些类存都在方法区中进行设置。对于非final的变量,其初始值都将先设置为0值,而对于final变量,在改阶段就直接设置为用户定义的值。

解析过程

虚拟机将常量池类的符号引用替换为直接引用。(感觉就是讲逻辑的地址转为物理地址的意思。)

初始化阶段

根据程序员制定的主观计划去初始化类变量和其它资源,就是执行类构造器方法clinit()的过程。

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立它在java虚拟机中的唯一性。也就是说即使同一个class文件,但使用的类加载器不同,那么这2个类必定不相等。

类加载器的分类

  1. bootstrap classloader: 负责将存放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定路径中的,并且被jvm识别的类库加载到虚拟机内存中。
  2. Extension classloader: 负责加载JAVA_HOME\lib\ext目录中的,或则被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;
  3. Application classloader: 这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称之为系统类加载器。它负责加载用户路径(ClassPath)上指定的类库。
    除了Bootstrap classLoader外,其余的类都应当有自己的父类加载器。工作过程:如果一个类的加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个类加载器都是如此。只有当父类加载器反馈自己无法完成这个加载请求时候,子类加载器才回去尝试自己加载。(java类随着它的类加载器具备了一种优先级的层次关系。)
    实现过程:先检查是否已经被加载,若没有加载则请求父类加载器的loadClass()方法,若父加载器为空则使用启动类加载器作为父加载器。如果父类加载失败,则调用自己的findClass() 方法去进行加载。

虚拟机字节码执行引擎

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素。其中存放了局部变量,操作数栈,动态连接和方法的返回地址等信息。一个栈帧需要分配多少内存,取决于具体的虚拟机。

局部变量表

是一组变量存储空间,用于存放方法参数和方法内部定义的局部变量。(方法所需要的最大局部变量表的容量是由编译为Class文件的时候,在方法Code属性中的max_locals数据项中存放。)
局部变量表的容量以变量槽(slot)为最小单位。而每个slot的大小没有明确的规定,只是说明每个slot都应该能够存放boolean,byte,returnAddress等数据类型(32位以内的数据类型。)。对于64位的long and double type,虚拟机会以高位在前的方式为其分配两个连续的slot空间。
虚拟机通过索引定位的方式使用局部变量表。
在方法的执行时候,虚拟接是通过使用局部变量表完成实参和形参的传递过程。对于一个非static方法,那么局部变量表中的第0位索引的slot默认是用于传递方法所属对象实例的引用(this关键字),其余参数按照参数李彪的顺序来排列。参数表分配完毕后,再根据方法体内部定义的变量顺序和作用于分配其余的slot。
局部变量表中的slot是可以重用的。

操作数栈

操作数栈的最大深度也在编译的时候写入到code属性的max_stacks数据项中。在方法执行过程中,会有各种字节码指令想操作数占中写入和提取内容。(逆波兰式的应用?)

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用为了支持方法调用过程中的动态连接。将class文件中常量池中的符号引用在每一次的运行期间转化为直接引用,这部分叫做动态连接。(另外的部分在类加载阶段或者在第一次使用的时候转为直接引用的过程叫做静态解析。)

方法返回地址

方法返回的方式:正常完成出口 和 异常完成出口。其中前者用 调用者的PC计数器的值作为返回地址,后者则要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。

编写自己的类加载器

以下内容来自博文:java深度历险二

在Java应用开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名 Java类的隔离等。创建自己的类加载器并不是一件复杂的事情,只需要继承自java.lang.ClassLoader类并覆写对应的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:

defineClass():这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
findClass():这个方法用来根据名称查找并加载Java类。
loadClass():这个方法用来根据名称加载Java类。
resolveClass():这个方法用来链接一个Java类。
这里比较 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到过,在Java类的链接过程中,会需要对Java类进行解析,而解析可能会导致当前Java类所引用的其它Java类被加载。在这个时候,JVM就是通过调用当前类的定义类加载器的loadClass()方法来加载其它类的。findClass()方法则是应用创建的类加载器的扩展点。应用自己的类加载器应该覆写findClass()方法来添加自定义的类加载逻辑。 loadClass()方法的默认实现会负责调用findClass()方法。

前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是封装在loadClass()方法中的。如果希望修改此策略,就需要覆写loadClass()方法。
下一篇讲述 java中方法的调用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值