JVM类加载、连接和初始化
内容纲要
概述
- 类从被加载到JVM开始,到卸载出内存,整个生命周期如图
- 加载:查找并加载类文件的二进制数据
- 连接:就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含如下几个步骤:
- 验证:确保被加载类的正确性
- 准备:为类的静态变量分配内存,并初始化它们
- 解析:把常量池中的符号引|用转换成直接弓|用
- 初始化:为类的静态变了赋初始值
JVM类加载
类加载要完成的功能
- 通过类的全限名来获取该类的二进制字节流
- 把二进制字节流转化为方法区的运行时数据结构
- 在堆上创建一 个java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供了访问方法区内数据结构的接口
加载类的方式
- 最常见的方式:本地文件系统中加载、从jar等归档文件中加载
- 动态的方式:将java源文件动态编译成class
- 其它方式:网络下载、从专有数据库中加载等等
类加载器
- Java虚拟机自带的加载器包括如下几种
- 启动类加载器(BootstrapClassLoader)
- 平台类加载器(PlatformClassLoader)(JDK8没有,对应扩展类加载器,替换JDK8中的扩展加载器的原因:1.ExtensionClassLoader主要加载jar中的lib/ext下的jar包,扩展不安全 2.扩展功能或者需求,jdk已经有了模块化)
- 应用程序类加载器(APPClassLoader)
- JDK8:扩展类加载器(ExtensionClassLoader)
- 用户自定义的加载器,是java.lang.ClassLoader的子类,用户可以定制类的加载方式;只不过自定义类加载器其加载的顺序是在所有系统类加载器的最后
加载器的关系
类加载器说明
- 启动类加载器:用于加载启动的基础模块类,比如:java.base、java.management、 java.xml 等等,启动类加载器是jvm虚拟机自身核心的加载器,不允许外拨使用或者改变
- 平台类加载器:用于加载- -些平台相关的模块,比如:java.scripting、java.compiler、 java.corba等等
- 应用程序类加载器:用于加载应用级别的模块,比如:jdk.compiler、jdk.jartool、 jdk.jshell 等等;还加载classpath路径中的所有类库
- JDK8 :启动类加载器:负责将<JAVA_ HOME>/lib ,或者-Xbootclasspath参数指定的路径中的,且是虚拟机识别的类库加载到内存中( 按照名字识别,比如rt.jar ,对于不能识别的文件不予装载)
- JDK8 :扩展类加载器:负责加载<JRE_ HOME> /lib/ext ,或者java.ext.dirs系统变量所指定路径中的所有类库
- JDK8 :应用程序类加载器:负责加载classpath路径中的所有类库
- Java程序不能直接引|用启动类加载器,直接设置classLoader为null ,默认就使用启动类加载器
- 类加载器并不需要等到某个类“首次主动使用”的时候才加载它, Jvm规范允许类加载器在预料到某个类将要被使用的时候就预先加载它
- 如果在加载的时候.class文件缺失,会在该类首次主动使用时报告LinkageError错误,如果- -直没有被使用 ,就不会报错
双亲委派模型
- JVM中的ClassLoader通常采用双亲委派模型,要求除了启动类加载器外,其余的类加载器都应该有自己的父级加载器。这里的父子关系是组合而不是继承,工作过程如下>JKD8:
- 一个类加载器接收到类加载请求后,首先搜索它的内建加载器定义的所有“具名模块”
- 如果找到了合适的模块定义, 将会使用该加载器来加载
- 如果class没有在这些加载器定义的具名模块中找到,那么将会委托给父级加载器,直到启动类加载器
- 如果父级加载器反馈它不能完成加载请求,比如在它的搜索路径下找不到这个类,那子的类加载器才自己来加载,在自己的classpath下找存不存在这样的类,存在就装载进来,如果不存在则向子加载器传递
- 在类路径下找到的类将成为这些类加载器的无名模板
- JDK8
- 一个类加载器接收到类加载请求后,自己先不去加载这个类,而是委派的父类加载器,父类又委派的父类的父类加载器,直到父类加载器反馈他不能完成加载请求(他的搜索路径下找不到这个类)这个时候子的类加载器才来自己加载
双亲委派模型说明
- 双亲委培模型对于保证Java程序稳定运作很重要
- 公用且具有一致性
- 安全
- 实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法中, 如果自定义类加载器的话,推荐覆盖实现findClass()方法
- 如果有一个类加载器能加载某个类。称为定义类加载器,所有能成功返回该类的Class的类加载器都被称为初始类加载器
- 如果没有指定父加载器,默认就是启动加载器
- 每个类加载器都有自己的命名空间,命名空间由该加载器及其所有父加载器所加载的类构成,不同的命名空间,可以出现类的全路径名相同的情况
- 运行时包由同一个类加载器的类构成,决定两个类是否属于同一个运行时包,不仅要看全路径名是否一样,还要看定义类载器是否相同。只有属于同一个运行时包的类才能实现相互包内可见
破坏双亲委派模型
- 双亲模型有个问题:父加载器无法向下识别子加载器加载的资源
- 为了解决这个问题,引入了线程上下文类加载器,可以通过Thread的setContextClassL oader()进行设置
- 另外一种典型情况就是实现热替换,比如OSG的模块化热部署,它的类加载器就不再是严格按照双亲委派模型, 很多可能就在平级的类加载器中执行了(JDK9开始)
类连接
验证
- 类文件结构检查:按照JVM规范规定的类文件结构进行
- 元数据验证:对字节码描述的信息进行语义分析,保证其符合Java语言规范要求
- 字节码验证:通过对数据流和控制流进行分析,确保程序语义是合法和符合逻辑的。这里主要对方法体进行校验
- 符号弓|用验证:对类自身以外的信息,也就是常量池中的各种符号引用,进行匹配校验
解析
- 所谓解析就是把常量池中的符号引用转换成直接弓|用的过程包括:符号引用:以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关
- 直接引用:直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的
- 主要针对:类、接口、字段、类方法、接口方法、方法类型方法句柄、调用点限定符
类的初始化
- 类的初始化就是为类的静态变量赋初始值,或者说是执行类构造器<clinit>方法的过程
- 如果类还没有加载和连接,就先加载和连接
- 如果类存在父类,且父类没有初始化,就先初始化父类
- 如果类中存在初始化语句,就依次执行这些初始化语句
- 如果是接口的话:
- 初始化一个类的时候,并不会先初始化他实现的接口
- 初始化一个接口是,并不会初始化他的父接口
- 只有当程序首次使用里面的变量或者是调用接口方法的时候,才会导致接口初始化
- 调用Classloader类的loadClass方法来装载一个类,并不会初始化这个类,不是对类的主动使用
类的初始化时机
- Java程序对类的使用方式分成:主动使用和被动使用, JVM必须在每个类或接口"首次主动使用”时才初始化它们;被动使用类不会导致类的初始化,主动使用的情况:
- 创建类实例
- 访问某个类或者接口的静态变量
- 调用类的静态方法
- 反射某个类
- 初始化某个类的子类,而父类还没有初始化
- JVM启动的时候运行的主类
- 定义了default方法的接口,当接口实现类初始化时
类的卸载
- 当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载
- Jvm自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的
共有 0 条评论