目录
类加载流程
1、加载
2、验证
3、准备
4、解析
5、初始化
双亲委派模型
常见异常
NoClassDefFoundError
ClassNotFoundException
ClassCastException
线程上下文类加载器
JIT
编译器
谁被编译了?
触发条件
当我们第一次使用该类的时候,如果该类还未被加载到内存,则系统会通过加载-连接-初始化来实现这个类的初始化。其中连接又分为:验证,准备和解析三步。所以一个类被加载到jvm其生命周期包括以下7个阶段。类加载包括前5个阶段。具体流程如下:
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。具体流程如下:
因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也是有的,是默认值。
类加载(初始化)时机
流程如下图所示:
这里我们可以通过getParent()来返回该类的父类加载器。
public class ClassLoadTest {
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
得到的结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@312b1dae
null
Process finished with exit code 0
正常的期望值是:系统类加载器的父类 -> 扩展类加载器 -> 启动类加载器,但是这里扩展类的父类不是启动类加载器?看了下源码:
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就通过启动类加载器加载该类,
//通过调用本地方法native findBootstrapClass0(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通过代码可以知道,如果发现某个类没有被加载,且当前的父加载类为空则会调用启动类加载器去加载,当启动类加载器也加载失败的时候,才会调用自定义的加载器。所以扩展类设置为null和设置启动类为父类是一样的,返回为null也就正常类。
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。从虚拟机的角度来说,只存在两种类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法
双亲委派模型的好处:
另外:(1)父加载器中加载的类对于所有子加载器可见 (2)子类之间各自加载的类对于各自是不可间的(达到隔离效果)
我们知道类加载器采用的是双亲委派原则,类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。 java.lang.ClassNotFoundException是被loadClass()抛出的, java.lang.NoClassDefFoundError是被defineClass()抛出。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
loadClass()是来启动类的加载过程的,其源代码在前面我们已经分析过,即当父加载器不为null同时不能完成加载请求的情况下会抛出ClassNotFoundException异常,而导致父加载器无法完成加载的一个原因很简单就是找不到这个类,通常这种情况是传入的类的字符串名称书写错误,如调用class.forName(String name)或者loadClass(String name)时传入了一个错误的类的名称导致类加载器无法找到这个类。
defineClass()是用来完成类的加载工作的,此时已经表明类加载的启动已经完成了,即当前执行的类已经编译,但运行时找不到它的定义时就会抛出NoClassDefFoundError异常,这种情况通常出现在创建一个对象实例的时候(creating a new instance),如在类X中定义了一个类Y,如在类X中定义如下语句ClassY y=new ClassY; 程序运行成功之后(此时X与Y的字节码文件已经存在),如果将类Y的字节码文件删除了重新运行上述代码,则会在运行时候抛出NoClassDefFoundError异常。当然这只是为了说明抛出这种异常的原因,一般不会出现删除该类字节码情况,实际上是其它原因导致类似删除的效果导致的,如JAR重复引入,版本不一致导至。因为jar中都是一些已经编译好的Class文件,如果存在多个版本那么在加载的时候就不知道应该调用哪一个版本(相当于删除字节码的效果),此种情况一般出现在引入第三方SDK的时候。
总结:
ClassNotFoundException | NoClassDefFoundError |
从java.lang.Exception继承,是一个Exception类型 | 从java.lang.Error继承,是一个Error类型 |
当动态加载Class的时候找不到类会抛出该异常 | 当编译成功以后执行过程中Class找不到导致抛出该错误 |
一般在执行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的时候抛出 | 由JVM的运行时系统抛出 |
ClassNotFoundException发生在装入阶段。
当应用程序试图通过类的字符串名称,使用常规的三种方法装入类,但却找不到指定名称的类定义时就抛出该异常。
NoClassDefFoundError: 当目前执行的类已经编译,但是找不到它的定义时,发生连接阶段:
也就是说你如果编译了一个类B,在类A中调用,编译完成以后,你又删除掉B,运行A的时候那么就会出现这个错误
加载时从外存储器找不到需要的class就出现ClassNotFoundException
连接时从内存找不到需要的class就出现NoClassDefFoundError
在jvm的世界里,确定是否是相同的一个类需要判断两个条件:类的全名 + 类加载器。如果一个相同的类,com.test.Sample,在通过不同的加载器加载后,如果相互赋值,则会出现ClassCastException的异常。
第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。
第二次:模型自身缺点导致:
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办? 这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码啊!那该怎么办? 为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。 |
第三次:为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
线程上下文加载器的使用场景
JIT是just in time,即时编译技术。使用该技术,可以加速java程序的运行速度,是一种优化手段。
对于 Java 代码,刚开始都是被编译器编译成字节码文件,然后字节码文件会被交由 JVM 解释执行,所以可以说 Java 本身是一种半编译半解释执行的语言。为了进一步提高代码的执行速度,HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译,提高优化。
HotSpot虚拟机内置了两个即时编译器:
C1编译器是一个简单快速的三段式编译器,主要关注“局部性能优化”,放弃许多耗时较长的全局优化手段 ;
过程:class -> 1. 高级中间代码 -> 2. 低级中间代码 -> 3. 机器代码
C2是专门面向服务器应用的编译器,是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数时的优化强度。
两类热点代码
判断一段代码是不是热点代码,是不是需要触发JIT编译,这样的行为称为:热点探测(Hot Spot Detection),有几种主流的探测方式:
基于计数器的热点探测(Counter Based Hot Spot Detection)
虚拟机会为每个方法(或每个代码块)建立计数器,统计执行次数,如果超过阀值那么就是热点代码。缺点是维护计数器开销。
基于采样的热点探测(Sample Based Hot Spot Detection)
虚拟机会周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那么就是热点代码。缺点是不精确。
基于踪迹的热点探测(Trace Based Hot Spot Detection)
Dalvik中的JIT编译器使用这种方式
hotspot默认使用计数器的热点探测:
目前主流商用JVM都采用编译器和解释器并存的架构,但主流商用虚拟机,都同时包含这两部分。
当程序需要迅速启动然后执行的时候,解释器可以首先发挥作用,编译器不运行从而省去编译时间,立即执行程序;
在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。
参考资料:
《深入理解jvm虚拟机》
https://blog.csdn.net/yangcheng33/article/details/52631940
https://blog.csdn.net/moakun/article/details/81257897
https://blog.csdn.net/weixin_39222112/article/details/81316511
https://blog.csdn.net/qq_26963433/article/details/78048561
https://www.cnblogs.com/xiaoxian1369/p/5498817.html
https://www.cnblogs.com/insistence/p/5901457.html
http://www.cnblogs.com/aspirant/p/8991830.html
https://blog.csdn.net/chen364567628/article/details/52561588
https://www.cnblogs.com/tinytiny/p/3200448.html
http://www.cnblogs.com/charlesblc/p/5993804.html