文章目录
一、类加载
1.类的加载概念
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。Class文件是一个以8位字节为单位的二进制流
区分一个文件的类型并不是靠扩展名去进行区分,靠魔数来区分当前的文件 是否是.class,头4个字节称之为魔数 OxCAFEBABY。
2.什么时候才会启动类加载器
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
3.从哪个地方去加载.class文件
(1)本地磁盘
(2)网上加载.class文件(Applet)
(3)从数据库中
(4)压缩文件中(ZAR,jar等)
(5)从其他文件生成的(JSP应用)
4.类的生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:加载、验证、准备、解析、初始化、使用、卸载。验证、准备、解析 3 个阶段统称为连接。
(1)加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
①通过一个类的全限定名来获取其定义的二进制字节流
②将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
③在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
*什么情况下会开始类加载过程的第一个阶段?
-
主动引用
-
a.遇到new/getStatic/putStatic/invokeStatic这个4条字节码指令,如果类
-
没有进行初始化,则会先触发类的初始化
-
b.反射
-
c.类存在继承关系,如果初始化一个类,发现父类没有被初始化,触发父类的初始化
-
d.虚拟机启动之后,都会有一个要执行的主类,虚拟机触发主类的初始化
-
e.java.lang.invoke.MethodHandle实例时候,会所对应的类进行初始化
-
被动引用
-
a.通过子类引用父类的静态字段,子类是不会被初始化的
-
b.创建一个自定义类型的数据,不会触发该类的初始化
-
c.常量在编译阶段会直接放入常量池,并没有引用定义常量池的类,所以不会触发该类的初始化
-
在加载阶段,JVM都做了什么事情?
-
a. 通过类的全限定名来获取定义类的二进制字节流
-
b. 二进制字节流所代表的静态结构转化为方法区运行时的动态的数据结构
-
c. 在内存中生成一个代表该类的class对象(唯一性),可以作为访问方法区的入口
(2)验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
①文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
②元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
③字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。
④符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
(3)准备
准备阶段主要为类变量(或称“静态成员变量”)分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
还有其他的默认值。
init value
注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。
(4)解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。
什么是符号引用和直接引用呢?
①符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
②直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
(5)初始化
这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
< clinit >() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。
< clinit >() 方法不需要显式调用父类构造器,虚拟机会保证在子类的 () 方法执行之前,父类的 < clinit >() 方法已经执行完毕。由于父类的 < clinit >() 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
< clinit >() 方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 < clinit >() 方法。
接口中不能使用静态代码块,但接口也需要通过 < clinit >() 方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的 < clinit >() 方法不需要先执行父类的 < clinit >() 方法,只有当父接口中定义的变量使用时,父接口才会初始化。
虚拟机会保证一个类的 < clinit >() 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 < clinit >() 方法。
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.shengsiyuan.Test”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。
二、类加载器
(1)三种类加载器如下
Bootstrap ClassLoader(启动类加载器)
Bootstrap ClassLoader : JAVA_HOME/jre/lib下的jar包
其下的jar包都是JVM运行时所必需的jar包
本身是一个类,本身也需要被加载到JVM上才能够去使用
如果某一个类的类加载器是BootstrapClassLoader,那么该类的getClass
Loader()方法返回null
ExtClassLoader(扩展类加载器)
- ExtClassLoader: JAVA_HOME/jre/ext下的所有jar包
*AppClassLoader(应用类加载器)
主要加载的是开发者在应用程序中编写的类 CLASSPATH路径下的所有jar文件
(2)三种类加载器之间的关系
Java在需要使用类别的时候,才会将类别加载,Java的类别载入是由类别载入器(Class loader)来达到的,预设上,在程序启动之后,主要会有三个类别加载器:Bootstrap Loader、ExtClassLoader与AppClassLoader。
Bootstrap Loader是由C++撰写而成,预设上它负责搜寻JRE所在目录的classes或lib目录下的.jar档案中(例如rt.jar)是否有指定的类别并加载(实际上是由系统参数sun.boot.class.path指定);预设上ExtClassLoader负责搜寻JRE所在目录的lib/ext 目录下的classes或.jar中是否有指定的类别并加载(实际上是由系统参数java.ext.dirs指定);AppClassLoader则搜寻 Classpath中是否有指定的classes并加载(由系统参数java.class.path指定)。
Bootstrap Loader会在JVM启动之后载入,之后它会载入ExtClassLoader并将ExtClassLoader的parent设为Bootstrap Loader,然后BootstrapLoader再加载AppClassLoader,并将AppClassLoader的parent设定为 ExtClassLoader。
在加载类别时,每个类别加载器会先将加载类别的任务交由其parent,如果parent找不到,才由自己负责加载,如果自己也找不到,就会丢出 NoClassDefFoundError。
每一个类别被载入后,都会有一个Class的实例来代表它,每个Class的实例都会记得是哪个ClassLoader加载它的,可以由Class的getClassLoader()取得加载该类别的ClassLoader。
(3)类加载的三个重要方法
- loadClass 负责以双亲委派的方式区加载类
- findClass 根据类的名字找到class对象(class文件)
- defineClass 负责从class字节码文件中加载class对象
(4)双亲委派模型
1.定义
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
2.双亲委派模型的工作过程
①如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
②每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
③只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
3. 作用
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类java.lang.Object,它由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并用自定义的类加载器加载,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
4.为什么使用双亲委派模型
①双亲委派模型最大的好处就是让Java类同其类加载器一起具备了一种带优先级的层次关系。这句话可能不好理解,我们举个例子。比如我们要加载java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类。如果没有双亲委派模型,完全可能搞出多个不同的Object类。
②自上而下每个类加载器都会尽力加载.