虚拟机的第一步就是类加载,理解类加载过程帮助我们写出更好的代码,搭建更稳定的框架
类加载过程
1加载:主要有三个阶段(类加载阶段和连接阶段的部分内容是交叉进行的,比如验证部分)
1.1通过类的全限定名获得定义此类的二进制字节流
1.2将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
1.3在内存(HotSpot对应方法区)中生成代表这个类的Class对象,作为方法区这个类的访问入口
2验证:主要为了确保class文件字节流中包含的信息符合当前虚拟机要求,并且不会危害虚拟机自身的安全。主要包含,文件格式验证,元数据验证,字节码验证,符号引用验证
3准备:主要为类变量(static声明)分配内存并设置初始值(归零),常量(static final)则直接到声明值
4解析:将常量池内的符号引用替换为直接引用
5初始化:类初始化的最后一步,主要合并类变量的初始值设置与static代码块的代码到初始化方法<clinit>,此方法是线程安全的(单例的静态内部类的线程安全就是依据此)
类加载器
类加载器的作用:在加载的第一个阶段获得二进制字节流这个过程是通过类加载器完成的
在判定两个类是否相等的有个大前提就是必须是同一个类加载器加载的。
类加载器的功能实际上应该是JVM做的,但是它为了更灵活的让开发者介入,这里就将这部分功能交给开发者了,在JDK中就是依据此介入口拓展出来了双亲委派模型。
双亲委派模型
双亲委派模型:优先使用父加载器(组合实现)加载,如果父加载器加载失败,则自身尝试加载。
Java中一般有3种类加载器:
1启动类加载器:主要负责加载<java_home>/lib目录下的jar包,如rt.jar
2拓展类加载器:主要负责加载<java_home>/lib/ext目录下的jar包
3应用程序类加载器:它是由ClassLoader中的getSystemClassLoader方法返回的,也成系统类加载器,加载用户class path下的类库
其实这里很明显可以看到的优点就是下层类加载器是不能修改上层已经定义过的全限定名下的类,举个例子就是如果你在应用程序中定义了一个java.lang.String实际上是不可能加载进来的,从这点看也是它最大的优点,安全,并且将整个代码依赖部分进行了分层划分,更清晰。同时它也带来了缺点,上层类加载器范围内的代码不能加载到下层类加载器范围内的class,也就是加载类的过程只能在下层类加载器中进行这在有些时候是不能接受的,下面在说打破双亲委派模型的时候会说用线程上下文类加载器可以打破这个情况。
双亲委派模型的实现,就是在ClassLoader类中的load class方法,可以看到都优先从父类查找class
破坏双亲委派模型:主要有三次
1JDK 1.0->1.2 , loadClass() -> findClass(),这是设计上的修改。
2模型缺陷:JNDI服务,SPI扩展类是由厂商自己实现,而启动类加载又不可能认识这些类。只好引入线程上下文类加载器Thread Context ClassLoader. 该类加载器可以通过setContextClassLoader()设置,如果创建线程时未设置,将会从父线程继承。如果在应用的全局范围内都没有设置,那就默认是AppClassLoader. 有了这个,JNDI服务就可以去加载所需的SPI扩展代码,也就是父类加载器请求子类加载器去完成类加载的动作。这其实也就违背了双亲委派模型的一般性原则,但无可奈何。
这个很有意思,尤其是这个线程上下文类加载器。第一次看的时候很难理解,后来想明白一个类加载器的范围只是加载对应文件夹下的类才想明白,而JDK的类肯定是启动类加载器,他们对应的扫描范围肯定不在应用类加载器,那么必须让JDK中的运行代码能获得应用类加载器,也就是通过这个线程上下文类加载器传递的,实际上这里就已经打破了双亲委派查找而是向下查找了。
3程序动态性的追求: “热替换” , OSGI. JSR-291
模块化的必然,1.9之后就已经彻底摈弃了双亲委派,大家都是平行依赖,通过描述依赖进行类加载的边界划分。
总结
对于JVM的设计而言它将类加载中的寻找class文件二进制流步骤移交给JAVA开发者来做,由此拓展而来的双亲委派模型算是JDK1.8之前的最基础的模型了,后续的1.9模块化彻底的打破了这种委派机制。任何一个类都是通过类加载器加载到虚拟机中的,要理解类加载的过程以及类加载器在其中的功能对于分析问题也很重要,尤其是一些顶层框架涉及到实例化对象时,这时选择合适的类加载器是非常重要的,否则就容易出现ClassNotFoundException错误而自己却一头雾水无从下手。