JVM类加载机制
虚拟机把描述类的数据从class文件加载到内存,然后对数据进行校验、转换解析及初始化,最终形成可以被虚拟机使用的java类型,这就是JVM类加载的过程。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
在这七个过程中,加载、验证、准备、初始化、卸载这5个阶段的顺序是一定的,而解析过程则不一定:它在某个情况下可以在初始化阶段之后再开始,这是为了支持Java语言语言的运行时绑定。
1、加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口。
类加载器的层次结构为:
- 启动类加载器:加载系统环境变量下JAVA_HOME/lib目录下的类库。
- 扩展类加载器:加载JAVA_HOME/lib/ext目录下的类库。
- 应用程序类加载器(系统类加载器):加载用户类路径Class_Path指定的类库。(我们可以在使用第三方插件时,把jar包添加到ClassPath后就是使用了这个加载器)
- 自定义加载器:如果需要自定义加载时的规则(比如:指定类的字节流来源、动态加载时性能优化等),可以自己实现类加载器。
双亲委派模型:当一个类加载器收到类加载请求时,不会直接加载这个类,而是把这个加载请求委派给自己父加载器去完成。如果父加载器无法加载时,子加载器才会去尝试加载。
采用双亲委派模型的原因:避免同一个类被多个类加载器重复加载。
1、自底向上检查类是否已经加载,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次
2、直顶向下尝试加载类,当发现这个类没有的时候会先去让自己的父类去加载,父类没有再让儿子去加载
String应该是被Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。
2、验证
验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 文件格式验证:验证Class文件魔数、主次版本、常量池、类文件本身等等。
- 元数据验证:主要是对字节码描述的信息进行语义分析,包括是否有父类、是否是抽象类、是否是接口、是否继承了不允许被继承的类(final类)、是否实现了父类或者接口的方法等等。
- 字节码验证:是整个验证过程中最复杂的,主要进行数据流和控制流分析,如保证跳转指令不会跳转到方法体之外的字节码指令、数据类型转换安全有效等。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(连接第三阶段-解析阶段进行符号引用转换为直接引用),符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类异常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
3、准备
准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
4、解析
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那直接引用的目标必定已经在内存中存在。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行解析。
5、初始化
真正开始执行Java程序代码,该步执行<clinit>方法根据代码赋值语句,对 类变量和其他资源 进行初始化赋值。
程序代码的运行顺序:父类静态内容——子类静态内容——父类构造——子类构造——子类方法 。
双亲委派模型的好处,什么时候需要破坏?
- 双亲委派模型保证了Java程序的稳定运行
- 可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
- 也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,我们的问题是DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖,源码发现类加载器使用的是线程上下文类加载器,这是打破双亲委托机制的关键。