学习类加载机制笔记
类加载机制
学习类加载机制笔记
1.8之前
1.8之后
虚拟机类加载机制
把Class文件加载到内存,并对数据进行校验,转换解析,初始化,最终形成可以被JVM直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
1.什么是类的加载?
2.类的生命周期?
3.类加载器是什么?
4.双亲委派机制是什么?
类加载的时机
生命周期
- 加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接。
以下情况对类进行初始化
-
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
-
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
-
当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
-
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果BEF_getStatic、BEF_putStatic、BEF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
类加载的过程
加载
-
完成3件事
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
类的来源
- 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
- 从网络中获取,这种场景最典型的应用就是applet
- 运行时计算生成,这种场景使用得最多的就是动态代理基础
- 由其它文件生成,典型场景就是JSP应用,即由JSP文件生成赌赢的Class类
- 从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
验证
-
验证是连接阶段的第一步,这一阶段的目的就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
-
验证项
-
文件格式验证
-
基于二进制字节流进行的,只有通过该阶段的验证后,这段字节流才被允许进入方法区进行存储。下面三个阶段是基于方法区的存储结构进行的验证。之后不会再读取和操作字节流了。
1.8就把方法区改用元空间了。类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
-
-
元数据验证
该阶段主要对元数据信息进行语义分析,是否满足《Java语言规范》
- 是否有父类
- 这个类的父类是否继承了不允许继承的类(被final修饰的)
- 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的方法
- 。。。
-
字节码验证
最复杂的一个阶段,主要目的是:通过数据流分析和控制流分析,确定语义是否合法,符合逻辑。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的行为。
- 这个过程的高度复杂性会消耗大量的时间,为此JDK6之后,把尽可能多的校验工作移动到javac里去了。在Class文件中的方法体代码里增加StackMapTable属性。在字节码校验阶段检查这个表里的类型是否正确,节约了时间。问题:依然可能被篡改,虚拟机设计者需要考虑的问题。JDK7之后采用了这种类型价差的方式来校验了。
-
符号引用验证
- 发生在解析阶段,也就是将符号引用转化为直接引用的时候
- 符号引用验证是对类自身以外的各类信息进行匹配性校验,即:
- 该类是否缺少或者被禁止访问某些外部类,方法,字段等资源。涉及权限,字段,等
-
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配。
- 为类变量和静态变量分配内存并初始化为零值。
public static String value ="hello"
其零值是null。- 如果是常量,也就是说javac的时候生成的类的class文件中有ConstantValue属性,那么就直接初始化其值了。
public static final int value =111
,那么value就是111
- 这些变量被分配到方法区上【方法区是堆内存上的一个逻辑概念】
解析
-
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定是已经加载到虚拟机内存的内容。
- 直接引用:
- (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- (3)一个能间接定位到目标的句柄 直接引用与虚拟机实现的内存布局相关。
-
解析动作分类
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的Java程序代码
-
初始化:为类的静态变量赋予正确的初始值
-
也就是执行类中的
<clinit>()
方法-
这个方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的
-
//这个代码说明:静态语句块中只能访问到定义在静态语句块之前的变量 //定义在在静态语句块之后的变量,却可以在静态语句块中赋值,但是不能访问! public class Test{ static int K = 9; static{ i = 0;//这句话可以通过编译 sout(i);//这句话会报“非法向前引用 sout(K);//这句话可以通过编译 } static int i = 1; }
-
父类的
()方法肯定会在子类的 ()方法之前执行完毕 -
Java虚拟机中第一个被执行的
()方法的类型肯定是Object的 -
()方法对类和接口来说不是必须的,如果类中没有静态变量和静态代码块就没有这个方法。 -
接口中不能使用静态语句块
-
如果接口中有变量初始化的赋值操作,那就要执行接口的
()方法了。如果这个变量是来自父接口的话,那还要执行父接口的 ()方法。 -
接口的实现类在初始化时候,不需要执行接口的
()方法
-
-
、
类加载器
-
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
-
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
双亲委派模型
分类
- 启动类加载器Bootstrap ClassLoader
这个类加载器使用C++语言实现,是虚拟机自身的一部分
- 其他类加载器
这些类加载器由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
-
三层类加载器
- 启动类加载器
- 加载存放在
$JAVA_HOME/lib
或者-Xbootclasspath指定的目录下的能够被虚拟机识别的类库,按照指定的名字进行识别的 - 启动类加载器无法被java程序直接引用
- 如果需要把加载请求委派给引导类加载器去处理,直接使用null即可
- 加载存放在
- 扩展类加载器
$JAVA_HOME/lib/ext
目录或者java.ext.dirs指定的环境变量目录- 开发者可以直接使用扩展类加载器来加载Class文件
- 应用程序类加载器
- 是ClassLoader类中的getSystemClassLoader()的返回值,也被叫做系统类加载器
- 加载用户类路径(Classpath)下的所有类库,程序的默认类加载器
- 启动类加载器
-
双亲委派模型的工作过程
-
一个类加载器收到类加载请求-->请求委派给父类加载器去完成-->继续向上委派
-
父类加载器如果加载成功则返回加载成功
-
父类加载器如果反馈无法完成加载,则由子加载器尝试加载
//双亲委派模型的实现 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
-
-
破坏双亲委派模型
- 双亲委派模型不是一个强制性约束的模型
- JNDI-->由bootstrap ClassLoader加载,其目的就是对资源进行查找和管理,需要调用由其他厂商实现并部署在应用程序的classpath下的JNDI服务提供者接口(SPI),这样一来,启动类加载器肯定不会加载这些代码。java团队引入一个线程上下文类加载器,这个加载器可以通过Thread类中的setContextClassLoader进行设置。如果未设置,则会从父线程继承一个,如果应该程序全局都没有设置,则默认未应用程序类加载器。这是一种父类加载器请求子类加载器完成类加载的行为。JNDI JDBC JCE JAXB JBI都是这样的策略。
- 未消除上述出现多个provider的时候,JDK1.6提供了ServiceLoader类,以META/services中的配置信息,辅以责任链模式,这才算给SPI的加载提供了一种相对合理的解决方案
- IBM的SOGi可以实现模块热部署:OSGi自定义类加载器,每个程序模块(OSGi中称作Bundle)都有一个自己的类加载器。当需要更换一个模块的时候,就把模块和类加载器一起替换掉,以实现代码的热替换。
- OSGi的类加载系统不是双亲委派,而是网状结构。
- 当收到类加载请求的时候,OSGi讲按照下面的顺序进行类搜索
- 将以java.*开头的类委派给父类加载器加载【符合双亲委派】
- 否则,将委派列表名单内的类委派给父类加载器加载【符合双亲委派,后面都是在平级的类加载器中进行的】
- 否则,讲Import列表中的类委派给Export这个类的Bundle的类加载器加载
- 否则,查找当前Bundle的classpath,使用自己的类加载器加载
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载
- 否则,类查找失败。