JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制。类加载全过程如下:
加载、校验、准备、初始化和卸载这五个阶段的顺序是固定的,解析就不一定。为了支持动态绑定,解析过程可以发生在初始化阶段之后。这个过程表示的是按顺序开始,但不是指执行顺序,往往会交叉混合进行,在某个阶段可能调用或者激活另一个过程。
类立即初始化的五种情况:
- 使用关键字new实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
- 初始化类的时候,如果有父类,且父类没有被初始化过,会先对父类进行初始化
- 使用java.lang.reflect包的方法进行反射调用的时候,类没有进行初始化就会触发其先初始化。
- 虚拟机启动时,会先初始化要执行的主类(含有Main方法)
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化
加载
加载过程:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区的数据结构的入口
加载过程主要由类加载完成,对于HotSpot虚拟机而言,class对象比较特殊,存放在方法区而不是堆中。
验证
检查class文件的字节流中包含的信息是否符合当前虚拟机的要求以及会不会危害虚拟机的安全。检查过程:
- 文件格式验证:基于字节流验证,验证字节流是否符合当前的class文件格式的规范,能否被当前虚拟机处理。验证通过后,字节流就会存储在内存的方法区。
- 元数据验证:基于方法区的存储结构验证,对字节码进行语义验证,确保不存在不符合java规范的元数据信息
- 字节码验证:基于方法区的存储结构验证,通过对数据流和控制流的分析,确保被检验类的方法在运行时不会做出危害虚拟机的动作
- 符合引用验证:基于方法区的存储结构验证,发生在解析阶段,保证能够将符合引用成功的解析为直接引用,保证解析动作正常运行
准备
为类变量(static修饰的变量,不包含final修饰static变量)分配内存空间并且设置该类变量的初始值,final修饰的类变量在javac执行编译期间就会分配,这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量会在对对象实例化时随的对象一起分配在java堆中。
举栗子: public static int lmf=aa; 会先经过准备,在进行赋值。赋值过程是在类构造器的()方法中。可以查看jvm学习系列之java字节码结构。
解析
解析阶段主要将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述目标,可以是任何字面量,而直接引用则是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。通常而言一个符号引用在不同虚拟机实例翻译出来的直接引用是不同的。
java在编译过程只会生成class文件,无法知道引用类的实际地址,所以只能用符号引用来映射引用类。
比如:com.lmf.fish类引用了com.lmf.zool类,在编译阶段,fish无法知道zool的实际内存地址,所以只能用com.lmf.zool做为zool的真实内存的地址。在解析节点,jvm通过解析com.lmf.zool,来确定其对应真正的内存地址。如果该类尚未被加载过,会先加载。
解析类型: 类或者接口的解析、字段解析、类方法解析、接口方法解析
初始化
该阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中所有的类变量的赋值动作和静态语句块(static{})中的语句合并而成。()方法和实例构造方法不同不同,它不需要显式的调用父类的(),虚拟机会保证父类的()方法在子类的()方法之前执行完成。()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量赋值的操作,那么编译器就不会为这个类生成()方法。接口中不能使用静态语句块,但仍然有变量赋值的操作,所以可以生成()方法,但与类不同的执行接口()方法不需要先执行父接口的()方法,接口的实现类在初始化时也一样不会执行接口的()方法。
类加载器
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。
系统自带的类加载器:
启动类加载器(bootstrap classLoader):
由C/C++实现,负责加载<JAVA_HOME>\jre\lib目录下或者是-Xbootclasspath所指定路径下目录以及系统属性sun.boot.class.path制定的目录中特定名称的jar包到虚拟机内存中。在JVM启动时,通过Bootstrap ClassLoader加载rt.jar,并初始化sun.misc.Launcher从而创建Extension ClassLoader和Application ClassLoader的实例.需要注意的是,Bootstrap ClassLoader只会加载特定名称的类库,比如rt.jar.如果我们自定义的jar扔到<JAVA_HOME>\jre\lib是不会被加载.
扩展类加载器(Extension classloader):
只有一个实例,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA\_HOME>\lib\ext目录下或是被系统属性java.ext.dirs所指定路径目录下的所有类库。
应用程序类加载器(Application ClassLoader):
只有一个实例,由sun.misc.Launcher$AppClassLoader实现,负责加载系统环境变量ClassPath或者系统属性java.class.path制定目录下的所有类库,如果应用程序中没有定义自己的加载器,则该加载器也就是默认的类加载器.该加载器可以通过java.lang.ClassLoader.getSystemClassLoader获取.
线程上下文类加载器(Thread Context ClassLoader):
每个线程都有一个类加载器(jdk1.2后引入),称为Thread Context ClassLoader,如果线程创建时没有设置,会默认从父线程继承一个。如果在应用全局内都没有设置,则所有线程下文类加载器为Application ClassLoader。
如果想要自已去设置可以,使用如下方法:
线程上下文类加载器可以容许父类加载通过子类加载器加载所需要的类库,打破了双亲委派模型,利用该加载器可以实现代码热替换,热部署,android的热更新也是借鉴这个。
类加载器的双亲委派模型
当一个类加载器接收到一个类加载的请求,它首先会将该请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的类加载请求都会被传入顶层的启动类加载器(bootstrap classLoader)中,只有当父类加载器反馈无法这个类的加载请求时(它的搜索范围内不存在这个类),子类加载器才尝试加载。
优点:可以避免重复加载,父类加载了,子类无需重复加载。更加安全,解决了各个类加载器的基础类的统一方式,如果不采用此方式,用户随意定义类加载器来加载核心api,会带来相关隐患。
双亲委派模型源码实现:
- findLoadedCalss(name) 检查该类是否已经被加载过
- 如果没有被加载过,就交给父类加载
- 父类不存在,就交给启动类加载器加载
- 父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载
类加载的三种方式:
1.通过命令行启动应用时由jvm初始化加载含有main()方法的主类
2.通过class.forname()方法动态加载,会默认执行初始化块(static{}),但class.forname(name,initialize,loader)中initialize可指定是否要执行初始化块
3.通过classloader.loadClass()方法动态加载,不会执行初始化块
自定义类加载器
-
遵守双亲委派模型:继承classLoader,重写findclass()方法
2.破坏双亲委派模型:继承classLoader,重写loadclass()方法
如果需要手动控制类的加载,就可以自定义类加载器,也可以通过以下方式:
举个栗子,看如下一段简单的代码
类加载过程如下:
1.根据jvm内存配置,为jvm申请特定大小的内存空间
2.创建引导类加载器实例,初步加载系统类到内存方法区区域里
bootstrap classloader会读取{JRE\_HOME}/lib下的jar包和配置,然后将这些系统类加载到方法区。可使用参数-Xbootclasspath或者系统变量sun.boot.class.path来指定目录加载类。
一般在{JRE\_HOME}/lib会存放jvm运行时所需要的系统类
如果想具体看加载了哪些系统类,可以使用代码查看
加载完系统类,jvm内存如下布局:
引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有运行时常量池、类型信息、字段信息、方法信息、类加载器的引用对应class实例的引用等信息。
类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL,可以看下如下测试结果:
对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
3.创建jvm启动器实例Launcher,并取得类加载器
运行main方法,jvm虚拟机调用已经在方法区的类sun.misc.Launcher 的静态方法getLauncher()获取sum.misc.Launcher实例
Launcher使用了单例模式,一个jvm虚拟机内只有一个Launcher实例。内部还有俩个加载器。
除引导类加载器之外的加载器都能判断某一个类是否被引导类加载过,如果有被加载过会直接返回对应的Class 实例,如果没有返回null。虚线表示类加载器的这个有限的访问引导类加载器的功能。
当AppClassLoader加载类时,先尝试让父加载器ExtClassLoader进行加载,如果父加载器ExtClassLoader加载成功,则AppClassLoader直接返回父加载器ExtClassLoader加载的结果;如果父加载器ExtClassLoader加载失败,AppClassLoader则会判断该类是否是引导的系统类(即是否是通过Bootstrap类加载器加载,会调用Native方法进行查找);若要加载的类不是系统引导类,那么ClassLoader将会尝试自己加载,加载失败将会抛出“ClassNotFoundException”。
4.使用类加载classloader加载main类
main被编译成class二进制文件,该文件有一个常量池(Constant pool)的结构体来存储该class的常量信息。常量池中有CONSTANT\_CLASS\_INFO类型的常量,表示该class中声明了要用到那些类:
具体过程:
当appclass loader 加载main类 ,会去查看该类定义 ,发现该类 声明使用别的 类 :java.lang.Object java.lang.System、java.io.PrintStream、java.lang.Class,main要想正常运行,要保证这些类加载成功。所以appclassloader尝试加载这些类会先委托父加载器ExtClassLoader加载,ExtClassLoader发现这些类不在它加载范围,返回null,appclassloader发现发加载没办法加载,就会查询是否被BootstrapClassLoader ,如果被加载过就返回对应的Class实例。如果没有被加载过 appclass loader就会亲自负责加载到内存中。
5.使用main方法作为程序入口运行程序
6.方法执行完毕,jvm销毁,释放内存