java类加载

类加载器负责将类(.class)文件(位于磁盘或网络上)加载到内存中,并为之生成对应的java.lang.Class对象。

在JVM中,一个类用其全限定类名(包名和类名)和其类加载器作为唯一标识。如pg.Person类由类加载器kl

加载,则该Person类在JVM中对应的Class对象表示为(Person、pg、kl)。

类加载器

Bootstrap ClassLoader 负责加载java核心类,如String、System核心类库。

Extension ClassLoader 负责加载JRE的扩展目录(%JAVA_HOME%jrelibext)。 通过这种方式,可以为java扩展核心类以外的功能,只要把自己开发的类打包成JAR文件放入扩展目录即可。

System ClassLoader 也被成为应用类加载器,负责在JVM启动时加载来自java命令的-classpath选项或CLASSPATH环境变量所指定的JAR包和类路径。

 类加载机制

全盘负责:当一个类加载器加载某个类时,该类所依赖和引用的其他类也将由该类加载器加载。

双亲委派:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

(采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。)

 缓存机制:缓存所有被加载过的类,当程序需要使用某个类时,类加载器先从缓冲区搜寻该类,只有当缓冲区不存在该Class对象时,类加载器才会读取类并转换成Class对象,存入缓存区。

(目前jvm主要使用的是双亲委派与缓存机制)

 

类的加载过程  

JVM将类加载过程分为三个步骤:加载(Load),链接(Link)和初始化(Initialize),链接又分为三个步骤,如下图所示:

加载(load)

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

链接

验证:确保被加载类的正确性。

准备:为类的静态变量分配内存,并将其初始化为默认值,即在方法区中分配这些变量所使用的内存空间。

解析:把类中的符号引用转换为直接引用。

(符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。)

初始化

初始化主要是为类的静态变量赋予正确的初始值。有两种方式为类的变量指定初始值:声明类变量时指定的初始值;使用静态初始化块为类变量指定初始值。

        那为什么我要有验证这一步骤呢?首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。

        准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

类初始化的触发时机

  • 创建类的实例。
  • 调用某个类方法或访问某个类变量。
  • 使用Class.forName(String str)加载类。
  • 初始化某个类的子类,该子类的所有父类都会被初始化。
  • 直接使用java.exe 命令运行某个主类(含有主方法main())。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
原文地址:https://www.cnblogs.com/deltadeblog/p/8329810.html