jvm类的加载
介绍:
什么是类的加载?
类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区内的数据结构。
类的加载的最终产品是位于堆区中的class对象,class对象封装了类在方法区内的数据结构,并且提供了访问方法区内的数据结构的接口。
在什么时候启动类加载?
类的加载并不需要某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被预先使用时就预先加载它,如果在预先加载过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误,如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
从哪个地方加载类?
- 从本地系统中直接加载。
- 通过网络下载.class文件。
- 从zip、jar等归档文件中加载.class文件。
- 从专有数据库中提取.class文件。
- 将java源代码编译为.class文件。
类加载机制:
加载主要是将.class文件(也可以是zip包)通过二进制流读入到jvm中,在加载阶段JVM需要完成3件事情。
加载:
加载主要是将.class文件(也可以是zip包)通过二进制流读入到jvm中,在加载阶段JVM需要完成3件事情。
- 通过classloader在classpath中获取XXX.class文件,将其以二进制流的方式读入内存。
- 将字节流代表的静态存储结构,转化为方法区的运行时存储结构。
- 在内存中生成一个该类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
连接:
验证:
主要是确保加载进来的字节流符合JVM规范,验证阶段会有4个检验动作:
- 文件格式验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里主要被魔数、主版本号、常量池等等的校验。
- 元数据验证:验证是否符合java语言规范,主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
- 字节码验证:确保程序语义合法,符合逻辑,是整个验证过程最复杂的阶段。主要是通过数据流和控制流分析,确保程序语义是合法的、符合逻辑。在元数据验证那个阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。
- 符号引用验证:确保下一步的解析能正常执行,它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。
准备:
准备是连接阶段的第二步,主要为静态变量在方法区分配内存,并设置默认初始值。
- 类变量会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中。
- 这里的初始值指的是数据类型默认值,而不是代码中被显式赋予的值,但是如果同时被static和final修饰准备阶段后就已经赋值了,普通赋值位于其他阶段。
解析:
解析是连接阶段的第三步,是虚拟机将常量池内的符合引用替换为直接引用的过程。
- 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好。
- 直接引用:直接引用可以是指向目标的指针、相对偏移量或者是一个能直接引用或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:
这是类加载机制的最后一步,在这个阶段,java代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值,在初始化阶段,程序员可以根据自己的需求来赋值了。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明变量是指定初始值。
- 使用静态代码块为类变量指定初始值。
JVM初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
类的初始化时机:
只有对类的主动使用时才会导致类的初始化,主动使用包括以下6种:
- 创建类的实例,也就是new的时候。
- 访问某个类或接口的静态变量,或者对静态变量赋值。
- 调用类的静态方法。
- 反射操作。
- 初始化某个类,则其父类也会被初始化。
- 虚拟机启动时被标明为启动类的类,直接用java.exe来运行某个类。
类加载器:
自带类加载器:
java语言系统自带有3个类加载器:
- BootStrap ClassLoader:跟类(启动,引导)加载器。它负责加载java的核心类。他比较特殊,因为它是由原生c++代码实现的,并不是java.lang.ClassLoader的子类。
- Extension ClassLoader:扩展类加载器。它负责加载jre的扩展目录(%JAVA_HOME%/jre/lib/ext)中的jar包的类,我们可以通过把自己开发的类打成jar包放入扩展目录来为java提供核心类以外的新功能。
- System ClassLoader(Application ClassLoader):系统类加载器。它负责再jvm启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为jvm自带的 ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果我们编写了自己的ClassLoader,便可以做到以下几点:
- 在执行非置信代码前,自动验证数字签名。
- 动态的创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库和网络中。
自定义类加载器:
Custom ClassLoader:通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要定义的ClassLoader,如Tomcat,jboss都会根据j2ee规范自行实现ClassLoader。
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。有几点需要注意:
- 这里传递的文件名是类的全限定名。
- 重写findClass而不重新loadClass,重新loadClass会破坏双亲委派模式。
类的三种加载方式:
- 通过命令行启动应用时由jvm初始化加载含有main()方法的主类。
- 通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initlize,loader)中的initlize可指定是否要执行初始化块。
- 通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
Class.forName()和ClassLoader.loadClass()的区别:
- Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
- ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
- Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
JVM类加载机制:
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
双亲委派模型:
- 当AppClassLoader加载一个Class时,他首先不会自己去尝试加载这个类,而是把这个加载请求委托给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个Class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如再$JAVSA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
意义:
- 系统类防止内存中出现多份的同样的字节码。
- 保证Java程序安全稳定运行。
结束生命周期:
在以下几种情况,Java虚拟机将结束生命周期:
- 执行了System.exit()方法。
- 程序正常执行结束。
- 程序在执行过程中,遇到了异常或错误而异常终止。
- 由于操作系统出现错误,而导致Java虚拟机进程终止。
本文由博客一文多发平台 OpenWrite 发布!