Skip to content

Latest commit

 

History

History
167 lines (75 loc) · 7.35 KB

JVM(2)类的加载.md

File metadata and controls

167 lines (75 loc) · 7.35 KB

类加载机制

在 jvm 中,Java类的加载过程分为5个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这5个过程。

加载过程

加载

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

注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)

此过程完成 3 件事

  • 通过全限定名获取此类的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成此类的Class对象(Hotspot虚拟机,class对象存储在方法区),作为方法区这个类的各种数据访问入口

总结:获取二进制字节流,类的静态信息存储至方法区,在内存中生成一个class类对象

验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

大致分 4 阶段:文件格式验证、元数据验证、字节码验证、符号引用验证

准备

正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间

数据类型默认零值,final修饰过的直接赋值

比如一个类变量定义为:

public static int v = 8080;

实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器方法之中。

public static final int v = 8080;

在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程

符号引用就是 class 文件中的:1. CONSTANT_Class_info、2. CONSTANT_Field_info、3. CONSTANT_Method_info 等类型的常量。

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

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

初始化

初始化阶段是类加载最后一个阶段。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。就是执行类构造器()的方法过程,

方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕**,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。**

卸载

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  • 加载该类的ClassLoader已经被GC。
  • 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法

类初始化的时机

  • 使用new 实例化对象时
  • 调用静态变量时(常量除外)、静态方法
  • 通过反射调用
  • 初始化一个类如果父类没有初始化,先触发父类的初始化
  • 执行main方法的启动类

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

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  2. 定义对象数组,不会触发该类的初始化。

  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

  4. 通过类名获取 Class 对象,不会触发类的初始化。

  5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

  6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

**接口和类的的初始化区别:**只需要真正使用到的父接口初始化,不需要所有父接口都初始化

类加载方式

静态加载:编译时刻加载的类是静态加载类,new关键字来实例对象,编译时执行,由JVM初始化加载

动态加载:运行时刻加载的类是动态加载类,反射方式加载就是动态加载;通过Class.forName()方法动态加载或者通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()的区别

  • Class.forName:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
  • classloader只将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。

类加载器

JVM 提供了 3 种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):

    负责加载 JAVA_HOME\lib 目录中的,或通过-bootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类

  • 扩展类加载器(Extension ClassLoader)

    负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader)

    负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

双亲委派

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

双亲委派模型优点:

==避免同一个类被多次加载;每个加载器只能加载自己范围内的类,保证java程序的稳定运行==,比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象

思考:可以不可以自己写个String类?

答案:不可以,因为根据类加载的双亲委派机制,会去加载父类,父类发现冲突了String就不再加载了;