类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
类加载负责执行类加载,去磁盘进行识别,识别完后加载到内存
类加载器的种类:
从上往下
-
启动类加载器:用来加载java核心类库,无法被java程序直接引用,加载的是
JAVA_HOME/jre/lib
; -
扩展类加载器:用来加载java的扩展库, java的虚拟机实现会提供一个扩展库目录,加载的是
JAVA_HOME/jre/lib/ext
; -
应用类加载器:它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的,加载的是
CLASSPATH
; -
自定义类加载器:平时用的不多,由java语言实现,继承自AppClassLoader;
双亲委派
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。
好处:
-
可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
-
为了安全,保证类库API不会被修改。
例如:用户自己写一个String类的话,执行main方法会报错,因为同名的String类已经被加载了,且这个类里面没有main方法,所以报错,这样就保证了不会让别人修改java的核心API库。
能不能自己写一个类,也叫java.lang.String
可以,但是即使你写了这个类,也没有用,这个问题涉及到加载器的委托机制,层层调用,最底下才是我们自定义的类,Java虚拟机会将java.lang.String类的字节码加载到内存中。
为什么只加载系统通过的java.lang.String类而不加载用户自定义的java.lang.String类呢?
因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类, 加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理,一直往上走,最后在启动类加载器中加载了String类。所以,用户自定义的java.lang.String不被加载,也就是不会被使用。
破坏双亲委派
第一破坏:
我们可以重写ClassLoader的loadClass方法,但是双亲委派的逻辑就是存在于这个方法,重写就会破坏
所以JDK1.2之后引入了findClass方法,重写这个方法而不是loadClass
第二次破坏:
各种数据库,JDK提供接口给他们,他们按照接口实现自己的类库,但调用JDK接口时会引起,接口中的类会引起第三方类库的加载,不符合自上而下的加载机制,出现父类加载器请求子类加载器去完成类加载的动作,
SPI spi机制是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在JDBC中就使用到了SPI机制。
原生的JDBC中 Driver 驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库厂商去实现的。原生的JDBC中的类是放在 rt.jar 包的,是由启动类加载器进行类加载的,在JDBC中的 Driver 类中需要动态加载不同数据库类型的 Driver 类,而 mysql-connector-.jar 中的 Driver 类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,于是乎,这个时候就引入SPI,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
2、Tomcat Tomcat为什么不使用默认的双亲委派模型?
Tomcat 作为一个 web 容器,存在以下使用场景:
1、部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个程序能使用不同版本的依赖
2、应用程序的类库都是独立的,保证相互隔离;
3、部署在同一个 web 容器中相同的类库相同的版本可以共享;
4、容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来;
2.2、Tomcat 如何实现自己的类加载机制? Tomcat 自己实现了自己的类加载器:
CommonLoader:Tomcat最基本的类加载器,加载路径中的 class 可以被Tomcat容器本身以及各个 Webapp 访问;
CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;
SharedClassLoader:各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp可见;
JspClassLoader:每一个JSP文件对应一个Jsp类加载器。
从图中的委派关系中可以看出:
CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,从而实现了公有类库的共用; CatalinaClassLoader 和 Shared ClassLoader 自己能加载的类则与对方相互隔离; WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离; JasperLoader 的加载范围仅仅是这个JSP文件所编译出来的那一个 .Class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JasperLoader 来实现JSP文件的热插拔功能。
类加载 过程
虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
加载--验证--准备--解析--初始化--使用--卸载
-
加载:通过全类名获取类的二进制流,然后吧二进制数据流解析成静态数据结构 存储 在方法区,并在堆中生成一个便于用户调用的java.lang.Class类型的对象
-
验证:1.验证class文件的格式是够正确。2.检查是否存在自己要使用的其他类或者方法。
-
准备:为类的静态变量分配内存,并初始化值
-
解析:将类,接口、字段和方法符号引用转为直接引用。
方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
比如:假设一个类A被编译成class文件,并且A引用了B,在编译阶段,A就需要一个字符串去记录B的地址,字符串就叫符号引用,在运行时,A类被加载,但是B未被加载,那么将加载B,此时A的符号引用会被替换成B的实际地址,这时就是直接引用
-
初始化:就是对类的静态变量,静态代码块执行初始化操作。具体初始化顺序,看下面。
-
使用:当我们调用静态类成员信息,比如静态字段、静态方法的时候就是使用了,还有用 new 关键字创建实例也是使用。
-
卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
类初始化 顺序
主要是下面几个要点、规则
-
先父再子。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
-
自上而下。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
-
懒惰性质。不是所有的类都会在程序启动时立即初始化,而是根据实际需要进行初始化。例如:如果直接用子类调用父类的属性,父类会初始化,子类不会。