一个类的生命周期
-
类生命周期7个阶段
-
1.加载loading
加载到虚拟机的内存中,即运行时数据区
-
2.验证verification
-
3.准备preparation
-
4.解析resolution
- 其中2、3、4合起来又称为连接Linking
-
5.初始化initialzation
-
6.使用using
-
7.卸载unloading
垃圾回收
-
-
阶段顺序
- 阶段的顺序是不定的,能够确定顺序的是①加载、②验证、③准备、⑤初始化、⑦卸载,而④解析就不一定了,因为java是有一个运行时绑定(动态绑定)的特征,解析有可能在初始化之前,也有可能在初始化之后
-
加载的时机
-
虚拟机规范没有规定加载的时机,所以是可以自由把控的,但是要求虚拟机在类加载阶段需要做完以下几件事
-
1)通过一个类的全限定名来获取定义次类的二进制字节流
不仅仅是通过class文件,也可以通过网络,或者通过jar包或zip包,又或者通过数据库获取二进制字节流
-
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
-
类加载过程
加载
- 加载阶段三件事
- 在上面加载时机中
- 加载与运行时数据区
验证
-
1.文件格式验证,验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。只有文件格式验证是通过文件io流验证的,后续的元数据验证、字节码验证、符号引用验证都是在内存中进行的
-
是否以魔数 OxCAFEBABE 开头。
-
主、次版本号是否在当前 Java 虚拟机接受范围之内。
-
常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
-
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
-
CONSTANT Utf8 info 型的常量中是否有不符合 UTF-8 编码的数据。
-
Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。
-
…
以上的部分还只是一小部分,没必要进行深入的研究。
-
总结:文件校验格式是基于二进制流数据,如果验证过了,后面的元数据验证、字节码验证、符号引用验证就不再跟操作字节流相关,而是跟内存中的方法区相关,因为class文件已经被加载到方法区上了
-
-
2.元数据验证:对字节码描述的信息进行语义分析,是否符合java语言规范
- ①这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
- ②这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
- ③如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- ④类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都-致,但返回值类型却不同等)。
- …
以上的部分还只是一小部分,没必要进行深入的研究。 - 元数据验证是验证的第二阶段,主要目的是对类的元数据信息进行语义校验,保证不存在与《Java 语言规范》定义相悖的元数据信息。
-
3.字节码验证:最复杂的一一个阶段, 主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中”这样的情况。
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
- …
以上的部分还只是一小部分,没必要进行深入的研究。 - 如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的。
-
4.符号引用验证(Object object中的object):最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段一解析阶段中发生。符号引用验证可以作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的可访问性( private、 protected. public、 )
- 是否可被当前类访问。
- …
- 符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,将会抛出异常。
-
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、 但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
- 静态变量,为static变量的值进行了初始化,int等为0,boolean为false
- 注意,此处是通过基本数据类型的零值表,因为这时候尚未开始执行任何 Java 方法,如果给静态变量赋值,会在后续初始化阶段赋值
解析
- 符号引用与直接引用:解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。这是解析的主体,如果没有变成直接引用,会抛异常
- 符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
- 直接引用的对象都存在于内存中,你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的女朋友,类比为直接引用。
- 解析大体可以分为:
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
- 解析阶段常见异常
- ava.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
- java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
- java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)
初始化
- 初始化阶段,虚拟机规范则是严格规定了有且只有 6 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的
Java 代码场景是:- 使用 new 关键字实例化对象的时候。
- 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候。
- 2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。
- 5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
- 6)当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的
实例1
父类
-
package ex7.init; /** * @author King老师 * 父类 */ public class SuperClazz { static { System.out.println("SuperClass init!"); } public static int value=123; public static final String HELLOWORLD="hello king"; public static final int WHAT = value; }
子类
-
package ex7.init; /** * @author King老师 * 子类 */ public class SubClaszz extends SuperClazz { static{ System.out.println("SubClass init!"); } }
测试类
-
package ex7.init; /** * @author King老师 *初始化的各种场景 * 通过VM参数可以观察操作是否会导致子类的加载 -XX:+TraceClassLoading **/ public class Initialization { public static void main(String[]args){ Initialization initialization = new Initialization(); initialization.M1();//打印子类的静态字段 // initialization.M2();//使用数组的方式创建 // initialization.M3();//打印一个常量 // initialization.M4();//如果使用常量去引用另外一个常量 } public void M1(){ //如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化(但是子类会被加载) System.out.println(SubClaszz.value); } public void M2(){ //使用数组的方式, 不会触发初始化(触发父类加载,不会触发子类加载) SuperClazz[]sca = new SuperClazz[10]; } public void M3(){ //打印一个常量,不会触发初始化(同样不会触类加载、编译的时候这个常量已经进入了自己class的常量池) System.out.println(SuperClazz.HELLOWORLD); } public void M4(){ //如果使用常量去引用另外一个常量(会不会初始化SuperClazz 1 不会走2) System.out.println(SuperClazz.WHAT); } }
-
调用M1方法,打印子类的静态字段,触发了父类的初始化,同时加载了子类信息,但是没有对子类进行初始化
-
查看字节码文件,是通过getstatic调用的子类静态字段,对应规则里面的第1条和第3条
-
0 getstatic #5 <java/lang/System.out> 3 getstatic #6 <ex7/init/SubClaszz.value> 6 invokevirtual #7 <java/io/PrintStream.println> 9 return
-
-
调用M2方法,创建父类的数组,并不会触发初始化,只会触发父类加载,也不会触发子类加载,查看字节码,并没有触发初始化的那四条指令
-
调用M3方法,调用父类中的static常量,父类和子类都不会加载
- 因为SuperClazz.HELLOWORLD字段被final修饰了,编译的时候这个常量就已经被放到了Initialization这个类的常量池中,父类和子类都不会加载
-
调用M4方法,调用父类中的static常量,与M3方法一样,为什么父类不但加载了,还初始化了?
- 查看字节码的时候,发现WHAT的调用指令用的是getstatic,而是M3中调用HELLOWORLD用的是ldc指令,而ldc指令是负责把数据值或String常量值从常量池中推送至栈顶
初始化的线程安全
- 加了static的静态代码块
- 父类SuperClazz的字节码文件中,方法中会有一个clinit方法,这是static的静态代码块生成的,如果同时多个线程来对这个类进行初始化,虚拟机会确保只有第一个线程来进行初始化,把其余线程暂停,直到第一个线程初始化完毕,确保线程安全
类加载器
什么是类加载器
- 类加载过程的前五步都是由类加载器完成
JDK提供的三层类加载器
启动类加载器 Bootstrap Class Loader
- 第一层
- 在jdk1.8.0_101/jre/lib目录下,核心类库resource.jar和rt.jar等等都是由第一层加载的
- -Xbootclasspath:路径,但是不能覆盖rt.jar的
拓展类加载器 Extension Class Loader
- 第二层
- 在jdk1.8.0_101/jre/lib/ext目录下,有一些拓展jar包
应用程序类加载器 Application Class Loader
-
第三层
-
package ex7; /** * @author King老师 *类加载器 **/ public class ClassLoader { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); //启动类加载器 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展类加载器 System.out.println(ClassLoader.class.getClassLoader());//应用程序类加载器 } }
- 打印结果分别是null、extClassLoader、AppClassLoader,因为第一个类加载器不是java写的,而是c/c++写的,所以找不到
- ClassLoader是自己写的类
-
对于类加载器来说,在jvm中,对任意一个类是有唯一性校验的,比如String,放在JVM内存方法区中,而确定唯一性方法除了这个类的全限定名,还需要它的类加载器才能唯一确定这个类
自定义类加载器
类加载中的问题
双亲委派模型
双亲委派模型
-
jvm是按需动态加载 采用双亲委派模型 ^ Bootstrap --------|----加载lib/*.jar(包括rt.jar、jce.jar等) | 启动类加载器 | | | 向| |向 上| Extension---------|下---加载jre/lib/ext/*.jar或者由 | | -Djava.ext.dirs 询| 拓展类加载器 |尝 问| |试 是| |是 否| Application-------|否----加载classpath指定内容 已| 应用程序类加载器 |可 加| |加 载| |载 | Custom------------|------自定义的ClassLoader指定内容 | 自定义类加载器 V
-
除了顶层Bootstrap类加载器,其余的类加载器会有一个父子关系
-
假设自己写了个String,在application中,首先向上面类加载器询问是否加载过String,一直往上面询问,一直到Bootstrap,此时反馈已经加载过了,application就不会加载了,如果反馈没有加载过,则application才会去加载,双亲委派就是每次加载一个类都会问它的父类加载器是否加载过,虽然英文翻译过来是双亲,实际只有父亲的概念
-
由于双亲委派和bootstrap,导致java具有天然的稳定性,所以java中很多class是无法修改的,不用担心这些类被别人修改后重新加载进来
jdk中ClassLoader类
-
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
-
除了加锁,首先找到要加载的类,然后判断当前类加载器的父类加载器parent是否存在,如果存在,则有父加载器来加载这个类
-
但是父类加载器和子类加载器并没有继承关系,这是通过组合模式在当前类加载器中定义了一个parent父类加载器的字段
双亲委派模型的好处
- 稳定
- 核心类库不能被修改,并且有了一种优先级别
Tomcat类加载机制
Tomcat类加载器层次结构
-
启动类加载器 ^ | 拓展类加载器 ^ | 应用程序加载器 ^ | Common类加载器 ↗ ↖ ↗ ↖ Catalina类加载器 Shared类加载器 (Catalina.sh中指定的启动类) (加载Tomcat通用类) ($CATALINA_HOME/lib) ^ | WebApp类加载器 (WEB-INF/classes) (WEB-INF/lib) ^ | Jsp类加载器
-
JVM中的三层类加载器保持不变
-
Common类加载器只是一个概念模型
- Catalina类加载器 ,加载Catalina.sh中指定的启动类,因为tomcat这个软件自身里面也有很多启动类
- Shared类加载器,因为tomcat可以发布多个war包,比如shop1.war和shop.war,这两个war包共享的类就可以放在Shared类加载器来加载
- WebApp类加载器,shop1和shop2是一个项目,是不同的版本,同一个方法有不同的实现,为了实现隔离,需要有各自的类加载器,中间也就不符合双亲委派模型了
如何破坏双亲委派
-
上面的WebApp类加载器
-
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。
这个加载器用来隔离不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。 -
在Webappclassloaderbase类的loadClass方法中,并没有把加载类的工作委托给父类去做
-
SPI与OSGI
什么是SPI
-
service provider interface,服务提供者接口
-
是一套机制
-
-
接口实现是在具体的jar包
-
下面JDBC中的Driver类,在jdk的source包中是有的,一个标准接口,但是没有实现
-
需要引入jar包mysql-connector-java-8.0.11.jar,同时在jar包下,META-INF/services目录下,有一个文件名为java.sql.Driver的文件,其中文件内容为com.mysql.cj.jdbc.Driver
-
JDBC
-
package ex7; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DBUtil { public static final String URL = "jdbc:mysql://localhost:3306/delay_order?serverTimezone=GMT%2b8"; public static final String USER = "root"; public static final String PASSWORD = "789456"; public static void main(String[] args) throws Exception { //1.加载驱动程序 Class.forName("com.mysql.cj.jdbc.Driver"); //2. 获得数据库连接 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); //3.操作数据库,实现增删改查 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT order_no, order_note FROM order_exp"); //如果有数据,rs.next()返回true while(rs.next()){ System.out.println(rs.getString("order_no")+" 订单内容:"+rs.getString("order_note")); } } }
-
通过Class.forName(“com.mysql.cj.jdbc.Driver”),显式地声明了驱动对象,但是即使删除了 Class.forName 这一行代码,也能加载到正确的驱动类,什么都不需要做,非常的神奇,为什么?
- 通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。
- SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。
线程的上下文类加载器
-
其中DriverManager中loadInitialDrivers这个静态方法里面
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator();
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
这里的类加载器是使用的线程上下文类加载器,不是WebAppClassLoader
-
Launcher中有一段代码
try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader);
破坏双亲委派
- 通过线程上下文类加载器破坏了双亲委派模型,实际是在ApplicationClassLoader中加载的
OSGI(了解)
- JDK1.9提出了JPMS,标准的模块系统
- 重点是模块化,与微服务类似,要使用的就安装,安装完就启动,不用了就停止,永远不用了就卸载
- 模块化的思路是按需加载