Java类加载是指将编译好的class文件加载至JVM内存, 形成可供JVM使用的Java实例, 这个过程叫做类的加载。
image.png
1、类的加载过程
类的加载过程包括了加载
, 连接
, 初始化
3个阶段, 其中连接
阶段又分为验证
,准备
,解析
3个阶段, 总体上可以分为加载
、验证
、准备
、解析
、初始化
五个阶段。
这五个阶段中, 除过解析
阶段, 其他阶段的顺序都是固定的, 而解析
阶段则不一定, 有些时候晚于初始化
阶段发生, 这是为了支持Java中的运行时绑定。其他几个阶段都是按顺序开始, 但不一定按顺序进行或完成, 往往是交叉运行, 在一个阶段中调用下一个阶段。
1.1、 类的加载: 查找并加载类的二进制数据
加载时类加载过程的第一个阶段, 在加载阶段, 虚拟机主要完成3件事情:
- 根据类的全限定名来获取类的二进制数据。
- 将这个类的二进制字节流所代表的静态存储结构转化为方法区的运行时结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象, 作为方法区中这些数据的访问入口。
类的加载并不时该类被用到时才会被加载, JVM规范允许类加载器在预料某个类可能被使用时就加载该类。 如果加载时, 该类对应的class文件不存在或者错误, 类加载器必须在首次使用该类时报告一个类加载错误, 如果类从未被使用, 则不会报错。
加载class的方式:
- 从本地文件系统直接加载
- 通过网络下载.class文件
- 从zip或jar等归档文件中加载文件
- 从专有数据库中加载class文件
- 从java源文件中动态编译class文件
1.2、 验证: 确保被加载类的正确性
验证时连接
阶段的第一个阶段,主要时保证类的合法性, 确保Class字节流包含的信息是否符合当前虚拟机的要求, 并且不会危害虚拟机本身的的安全, 验证阶段大致会完成4个阶段的验证动作:
- 文件格式验证: 验证字节流是否符合Class文件的规范。
- 元数据验证: 验证字节流描述的信息是否符合Java语言规范。
- 字节码验证: 通过数据流和控制流分析, 确定程序语义是否合法,符合规范。
- 符号引用验证: 确保解析动作正确执行。
1.3、 准备: 为类的静态变量分配内存, 并初始化为默认值
准备阶段时正式为类变量分配内存并设置初始默认值的阶段, 这些内存都将在方法区分配。
这个阶段需要注意以下几点:
- 这个时候仅会为类变量(static)分配内存, 并且都在方法区中进行, 类的实例变量在
初始化
阶段在堆中为其分配内存并初始化。 - 这里设置的初始值一般一般时对应类型的零值, 如0、 0L、 null、 false等, 而不是在Java代码中被显式赋予的初始值。
假设有一个类变量定义为public static int value = 3
, 该变量在准备阶段后,其值依然为0, 而不是3, 因为这个时候并未执行任何Java代码。 - 对于基本类型来说, 对于类变量和全局变量,如果不为其显示的赋值, 其值未默认零值, 对于局部变量则必须使用前显示赋值,否则编译不通过。
- 对于
final
和static
同时修饰的变量, 必须在声明时就显示的赋值,否则编译不通过, 对于仅仅final
变量修饰的变量,可以在声明时显示赋值,也可以在类初始化时赋值,系统不会为其赋予默认值。 - 对于reference类型, 如果不为其显示的赋值, 则默认值都是
null
. - 对于数组类型, 如果初始化时没有被显示的为内部各个元素赋值, 则会被赋予默认零值。
1.4、 解析: 把类中的符号引用转为直接引用
解析阶段时将常量池类的符号引用转化为直接引用。解析阶段主要针对类
,接口
,字段
,类方法
,接口方法
,方法符号
,调用点
等7类符号引用进行。
- 符号引用: 符号引用就是一组符号来描述目标, 可以是任务字面量。
- 直接引用: 就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
1.5、类的初始化
初始化主要时为类的静态变量赋予正确初始值, JVM 负责对类初始化, 主要时对类变量初始化。 在Java中为类变量设定初始化有两种方式:
- 在声明时设定初始值。
- 在静态代码块(
static {}
)中为类变量指定初始值。
JVM初始化步骤:
- 假如这个类还没有被加载和连接, 则先进行加载和连接。
- 假如这个类的父类还没有被初始化, 则先初始化其父类。
- 假如类中有初始化语句, 则系统依次执行这些初始化语句。
类的初始化时机:
- 创建类的实例, 也就是new 的时候。
- 访问类或接口的静态变量, 或者对静态变量赋值。
- 调用类的静态方法。
- 反射
- 初始化某个类的子类, 其父类也会被初始化。
- Java虚拟机启动时被标明为启动类的类, 直接用java.exe运行某个主类。
2、类加载器
2.1、类加载器层次
image.png-
启动类加载器(
BootStrapClassLoader
),使用C++实现, 是虚拟机自身的一部分, 负责加载存放在jdk/jre/lib下的类库, 无法被Java程序直接引用。 -
扩展类加载器(
ExtClassLoader
), 负责加载jdk/jre/ext下的类库, 开发者可以直接使用扩展类加载器。 -
应用类加载器(
AppClassLoader
), 负责加载用户类路径(ClassPath)下指定的用户类, 开发者可以直接使用应用类加载器, 在开发着没有指定自定义类加载器的情况下, 程序默认的类加载器。
应用程序由以上3类加载器相互配合加载, 用户也可以尝试自定义加载器, 比如:
- 尝试从网络中加载类
- 尝试加载加密的类文件
2.2、类的加载
类的加载又种方式:
- 启动应用时由JVM加载
- Class.forName()方法动态加载
- ClassLoader.loadClass()方法动态加载
注意事项: - Class.forName会将类加载到JVM内存, 并初始化执行static块。
- ClassLoader.loadClass仅仅将类加载到内存,并不会初始化,只有在调用newInstance初始化实例时,才会执行static块。
- Class.forName带参函数可以控制是否执行static块。
2.3、寻找类的加载器
AppClassLoader -> ExtClassLoader -> BootStrapClassLoader
- 启动类加载器时所有类加载器的顶层父加载器。
- 扩展类加载器时应用类加载器的父加载器。
3、类的加载机制
类的加载机制有以下几个特点:
-
全盘负责: 当一个类加载器试图加载某个类时, 该类依赖和引用的其他Class都由该类加载器加载, 除非显示使用其他类加载器加载。
-
父类委托: 先让父类加载器试图加载该类, 只有当父类加载器无法加载该类时, 才尝试从自己的类路径种加载该类。
-
缓存机制:缓存机制确保所有被加载过的类都会被缓存, 再次使用该类时, 首先会尝试从缓存区寻找该Class, 只有缓存区种找不到时, 系统才会加载其二进制字节码, 并转化为Class对象,放入缓冲区。 所以Class被修改后, 必须重启JVM, 修改的程序才会生效。
-
双亲委派机制:当一个类加载器收到类加载的请求, 首先不会自己去尝试加载这个类, 而是委托父类加载器去完成, 依次向上, 所以所有的类加载请求最终都会传递给顶层的启动类加载器(
BootStrapClassLoader
), 只有当父类加载器无法加载时, 子类加载器才会尝试加载。
3.1、 双亲委派机制
1、 当AppClassLoader加载一个Class时, 首先会传递给ExtClassLoader去加载。
2、 当ExtClassLoader收到加载Class的请求时, 也不会尝试自己去加载, 会传递给BootStrapClassLoader去加载。
3、 如果BootStrapClassLoader加载失败, 则会由ExtClassLoader来加载。
4、 如果ExtClassLoader加载失败, 会由AppClassLoader来加载。
5、 如果AppClassLoader加载失败, 会报出异常ClassNotFoundException
。
3.2、 双亲委派优势
- 系统类防止内存中出现多份同样的字节码
- 避免重复加载类
4、 自定义类加载器
略
网友评论