ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

时间:2021-07-04 19:37:00

ClassLoader类加载器

Java 中的类加载器大致可以分成两类:

一类是系统提供的:

  • 引导类加载器(Bootstrap classloader):它用来加载 Java 的核心库(如rt.jar),是用原生代码而不是java来实现的,并不继承自java.lang.ClassLoader,除此之外基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
  • 扩展类加载器(Extension classloader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录(一般为%JRE_HOME%/lib/ext)。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(System classloader或 App classloader):它根据当前Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

另外一类则是由 Java 应用开发人员编写的:

开发人员可以通过继承java.lang.ClassLoader 类的方式实现自己的类加载器,重写 findClass()方法,以满足一些特殊的需求。

当一个类加载和初始化的时候,类仅在有需要加载的时候被加载,类加载都是在运行期间执行的;

比如我们自定义一个类People,然后用该类创建对象,就首先会加载改类到方法区。

假设你有一个应用需要的类叫作 Abc.class,首先加载这 个类的请求由 应用类加载器委托给它的父类加载器 Extension 类加载器, 然后再委托给 Bootstrap 类加载器;Bootstrap 类加载器 会先看看 rt.jar 中有没有 这个类,因为并没有这个类,所以这个请求又回到 Extension 类加载器,它会查 看 jre/lib/ext 目录下有没有这个类,如果这个类被 Extension 类加载器找到了, 那么它将被加载,而 应用类Application类加载器不会加载这个类;而如果这个类没有 被 Extension 类加载器找到,那么再由 应用类Application 类加载器从 classpath 中寻找, 如果没找到,就会抛出ClassNotFoundException异常。

如下图所示:

ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

双亲委托模型

从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。

在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象;若parent不能装载,则由parent的请求者去装载。

为什么双亲委托模型更加安全?

因为在此模型下用户自定义的类装载器不可能装载应该由父类加载器加载的可靠类(如Extension加载器(jre/lib/ext)和Bootstrap加载器(rt.jar)下的class),从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。

比如用户自定义自己的一个名为String的恶意类,想要替换rt.jar下面java.lang.String,加载时,由于双全委托模型,首先请求到App ClassLoader,然后再到Extension ClassLoader,再到Bootstrap ClassLoader,由于已经加载过java.lang.String, java.lang包的String类不会再替换。

也就是重要的核心类和公共类都被Bootstrap和Extension加载了,不会被恶意类来替换这两个加载器加载的类。

Java类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示。

 ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

加载

在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:

1.通过一个类的全限定名(包名.类名吧)来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口;

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。

在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程.
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

1

2

3

4

5

6

7

8

9

public class Test

{

static

{

i=0;

System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)

}

static int i=1;

}

一个例子

class SingleTon {

private static SingleTon singleTon = new SingleTon();

public static int count1;

public static int count2 = 0;

private SingleTon() {

count1++;

count2++;

}

public static SingleTon getInstance() {

return singleTon;

}

}

public class Test {

public static void main(String[] args) {

SingleTon singleTon = SingleTon.getInstance();

System.out.println("count1=" + singleTon.count1);

System.out.println("count2=" + singleTon.count2);

}

}

分析:

1:SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化
2:类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3:类初始化阶段,为类的静态变量赋值和执行静态代码块。singleton赋值为new SingleTon()调用类的构造方法
4:调用类的构造方法后count=1;count2=1
5:继续为count1与count2赋值,此时count1没有赋值操作,所以count1为1,但是count2执行赋值操作就变为0

Java反射中Class.forName和classloader的区别

Java中Class.forName和classloader都可以用来对类进行加载。

Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。

而ClassLoader 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

获得一个ClassLoader loader = ClassLoader.getSystemClassLoader();

Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。

static关键字父类子类静态代码块

https://www.cnblogs.com/theRhyme/p/7538020.html

根据例子总结:
结果是集合{父类static,子类static,父类大括号,父类构造函数,子类大括号,子类构造函数}的一个子集。

核心就是类加载static块优先初始化,子类初始化会触发父类的初始化。

一个类中,被加载过的静态代码不会被重复加载。

{}大括号里的是初始化块,这里面的代码在创建java对象时执行,而且在构造器之前!其实初始化块就是构造器的补充,初始化块是不能接收任何参数的,定义的一些所有对象共有的属性、方法等内容时就可以用初始化块了初始化!! 好处是可以提高初始化块的复用,提高整个应用的可维护性。

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

@Autowired static变量

@Component

public class A{

  @Autowired

  private static RestTemplate restTemplate;

}

在Springframework里,我们是不能@Autowired一个静态变量,使之成为一个Spring bean的。因为当类加载器加载静态变量时,Spring上下文尚未加载。所以类加载器不会在bean中正确注入静态类,并且会失败。

解决思路:不要在类加载器加载静态变量时通过@Autowired初始化该静态变量,可以在后续@Autowired初始化该静态变量,比如构造方法,@PostConstruct等

双亲委派模式破坏-JDBC

JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,那么JDBC是如何加载这些Driver实现类的?
通过Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类。
TODO待写

参考来源:

https://blog.csdn.net/RogueFist/article/details/79575665

http://blog.csdn.net/stypace/article/details/40613953

http://www.importnew.com/18548.html

http://www.cnblogs.com/mangosoft/p/6485790.html

https://www.cnblogs.com/theRhyme/p/7538020.html

https://blog.csdn.net/sinat_34976604/article/details/86723663

https://www.cnblogs.com/aspirant/p/8991830.html