目录
Class.forName()和ClassLoader.loadClass()的区别
1.类加载的时机
加载的时机是指发生第一个阶段加载的时候
1).遇到new(用new实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时
2).使用Java.lang.reflect包的方法对类进行反射调用时,如果此时类没有进行init,会先init。
3).当初始化一个类时,如果其父类没有进行初始化,先初始化父类
4).jvm启动时,用户需要指定一个执行的主类(包含main的类)虚拟机会先执行这个类
5).当使用JDK1.7的动态语言支持的时候,当java.lang.invoke.MethodHandler实例后的结果是REF-getStatic/REF_putstatic/REF_invokeStatic的句柄,并且这些句柄对应的类没初始化的话应该首先初始。
6).当一个接口定义了JDK8新加入的默认方法(default),如果有这个接口的实现类发生初始化,那么接口要在其前面发生初始化
2.类加载的过程
2.1加载
目的
1)通过一个类的全限定类名来获取定义此类的二进制字节流
2)将这个字节流 中 代表着静态存储结构(也就是static标识的方法常量等)转化位方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
补充:加载阶段中获取类的二进制字节流的动作是开发人员可控性最强的阶段,一般来说是由jvm内置的引导类加载器来完成的,可以重写类加载器控制获取字节流的方式
2.2验证
目的
1)这一步是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求
补充:1.验证这一步十分重要,保护虚拟机不受到恶意代码攻击,这一步在类加载的整个过程中耗费了相当大的代码量和性能时间
2.检验动作:文件格式验证:包括编码格式,class文件的版本号是否符合当前jvm,主要目的是验证字节流能否存在方法区中,后面三步都是在方法区中验证不会有字节流的读写操作
元数据验证 保证不存在与《java语言规范》定义中不符合的元数据信息(比如这个类不是抽象类是否实现了父类要求实现的所有方法)
字节码验证 对类的方法进行 确保方法在运行时不会做出危害虚拟机的事
符号引用验证 类,字段方法是否可以被访问(private protected)
2.3准备
目的
1)正式为类中定义变量(这里指static修饰的变量)分配内存并设置类变量初始化阶段
补充:例如类中有这样一行代码 public static int value = 123; 那么在准备阶段完成过后会对value初始化并赋值0,赋值操作要到初始化才执行,这里只是初始化内存
2.4解析
目的
1)将常量池内的符号引用替换为直接引用的过程(递归查找父类是否有与目标匹配的方法,直到找到java.lang.Object)
有着类或接口的解析、字段解析、方法解析和接口方法解析
2.5初始化
目的
才开始真正执行程序,进行初始化赋值(也就是类中的 Java 代码)。例如2.3中准备的举例,这一步就要将123的值附在value静态变量上了
3.常见面试题
Java类加载机制是什么样
如上
双亲委派模型
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式(向上递归和向下递归)
双亲委派图借鉴: https://blog.csdn.net/codeyanbao/article/details/82875064
破坏双亲委派模型
上面提到过双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。
1.双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前--即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
2.双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?
这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
3.双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
Class.forName()和ClassLoader.loadClass()的区别
Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);
第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。
一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。
ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);
第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,
不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行