类加载
在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
提供了更大的灵活性,增加了更多的可能性
类加载器深入剖析
- Java虚拟机与程序的生命周期
- 在如下几种情况下,Java虚拟机将结束生命周期
- 执行了
System.exit()
方法 - 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 执行了
类的加载、连接与初始化
- 加载:查找并加载类的二进制数据
- 连接
——验证:确保被加载的类的正确性
——准备:为类的静态变量分配内存,并将其初始化为默认值
——解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用转换为直接引用 - 初始化:为类的静态变量赋予正确的初始值
类的加载
类的加载:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.Iang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构
- 类的加载的最终产品是位于内存中的Class对象
- Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有的数据库中提取.class文件
- 将Java源文件动态编译为.class文件(动态代理)
Java程序对类的使用方式可分为两种
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序
首次主动使用
时才进行初始化
,且仅初始化一次
主动使用(七种)
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静
态变量赋值 - 调用类的静态方法
- 反射(如
Class.forName("com.test.Test")
) - 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK1.7开始提供的动态语言支持:
java.lang.invoke.MethodHandle
实例的解析结果REF _getStatic
,REF _putStatic
,REF _invokeStatic
句柄对应的类没有初始化,则初始化
为了辅助理解,下面我写几个例子,这些例子也是求职笔试中容易考的
例子一:
public class Main {
public static void main(String[] args) {
System.out.println(Demo.msg);
}
}
class Demo{
public static String msg = "Hello World";
static {
System.out.println("静态代码块");
}
Demo(){
System.out.println("构造方法");
}
}
结果:
静态代码块
Hello World
分析:
主动使用类型:2
调用了类的静态属性,使其初始化
例子二:
public class Main {
public static void main(String[] args) {
System.out.println(new Demo().msg);
}
}
class Demo{
public static String msg = "Hello World";
{
System.out.println("代码块");
}
static {
System.out.println("静态代码块");
}
Demo(){
System.out.println("构造方法");
}
}
结果:
静态代码块
代码块
构造方法
Hello World
分析:
主动使用类型:1
创建了一个类的实例
例子三:
public class Main {
public static void main(String[] args) {
System.out.println(Demo.msg);
}
}
class Demo{
public static final String msg = "Hello World";
{
System.out.println("代码块");
}
static {
System.out.println("静态代码块");
}
Demo(){
System.out.println("构造方法");
}
}
结果:
Hello World
分析:
没有进行主动使用
属性msg被关键字final所修饰,在编译期间可以确定其值,
所以常量在编译阶段就会存入到调用这个常量的方法所在的类的常量池中,
本质上,调用类并没有直接饮用到定义常量的类,
因此不会触发定义常量的类初始化
例子四:
public class Main {
public static void main(String[] args) {
System.out.println(Child.msg);
}
}
class Parent{
public static String msg = "Hello World";
static {
System.out.println("Parent静态块");
}
{
System.out.println("parent代码块");
}
Parent(){
System.out.println("Parent构造方法");
}
}
class Child extends Parent{
static {
System.out.println("Child静态块");
}
{
System.out.println("Child代码块");
}
Child(){
System.out.println("Child构造方法");
}
}
结果:
Parent静态块
Hello World
分析:
主动使用类型:2
对于静态字段来说,只有直接定义了该字段的类才会被初始化
例子五:
public class Main {
public static void main(String[] args) {
System.out.println(new Child().msg);
}
}
class Parent{
public static String msg = "Hello World";
static {
System.out.println("Parent静态块");
}
{
System.out.println("parent代码块");
}
Parent(){
System.out.println("Parent构造方法");
}
}
class Child extends Parent{
static {
System.out.println("Child静态块");
}
{
System.out.println("Child代码块");
}
Child(){
System.out.println("Child构造方法");
}
}
结果:
Parent静态块
Child静态块
parent代码块
Parent构造方法
Child代码块
Child构造方法
Hello World
分析:
主动使用类型:1
当一个类在初始化时,要求其父类全部都已经初始化完毕
例子六:
public class Main {
public static void main(String[] args) {
System.out.println(Parent.msg);
}
}
class Parent{
public static final String msg = "Hello World";
static {
System.out.println(msg);
}
{
System.out.println("parent代码块");
}
Parent(){
System.out.println("Parent构造方法");
}
}
结果:
Hello World
分析:
没有进行主动使用
属性msg被关键字final所修饰,在编译期间可以确定其值,
所以常量在编译阶段就会存入到调用这个常量的方法所在的类的常量池中,
本质上,调用类并没有直接饮用到定义常量的类,
因此不会触发定义常量的类初始化
例子七:
import java.util.UUID;
public class Main {
public static void main(String[] args) {
System.out.println(Parent.msg);
}
}
class Parent{
public static final String msg = UUID.randomUUID().toString();
static {
System.out.println("parent静态代码块,msg="+msg);
}
{
System.out.println("parent代码块");
}
Parent(){
System.out.println("Parent构造方法");
}
}
结果:
parent静态代码块,msg=ddbd3b20-bce3-45ae-a287-74e798b13720
ddbd3b20-bce3-45ae-a287-74e798b13720
分析:
主动使用:2
属性msg被关键字final所修饰,但是在编译期间不可以确定其值,
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,
这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
例子八:
这是静态内部类实现单例模式
public class Main {
public static void main(String[] args) {
System.out.println(SingleTon.getInstance());
System.out.println(SingleTon.getInstance());
}
}
class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
例子九:
public class Main {
public static void main(String[] args) {
Demo[] demos = new Demo[5];
System.out.println(demos.getClass());
System.out.println(demos.getClass().getSuperclass());
}
}
class Demo{
static {
System.out.println("Demo静态代码块");
}
}
结果:
class [Ltop.sufa.Demo;
class java.lang.Object
分析:
对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为class [Ltop.sufa.Demo;
这种形式。动态生成的类型,其父类就是Object。
JavaDoc经常将构成数组的元素称为Component,其实就是将数组降低一个维度后的类型
创建数组对象,并不会对数组组成的元素`主动使用`,也就不会`初始化`
例子十:
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1=" + Singleton.counterl);
System.out.println("counter2=" + Singleton.counter2);
}
}
class Singleton {
public static int counterl;
private static Singleton singleton = new Singleton();
private Singleton() {
counterl++;
counter2++;
System.out.println("counter1=" + Singleton.counterl);
System.out.println("counter2=" + Singleton.counter2);
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
结果:
counter1=1
counter2=1
counter1=1
counter2=0
分析:
主动使用类型:3
调用了类的静态方法,进行初始化,在连接准备阶段,
对所有变量赋初始值,在初始化阶段对有显式赋值的属性赋值真实值,
初始化是顺序执行的,如果上面的代码`public static int counter2 = 0;`
放在`private static Singleton singleton = new Singleton();`的上面,
则结果:
counter1=1
counter2=1
counter1=1
counter2=1
例子十:
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("top.sufa.Demo");
}
}
class Demo {
static {
System.out.println("this is static Demo");
}
}
结果:
this is static Demo
分析:
主动使用类型:4
Class.forName("top.sufa.Demo") 主动使用
Java加载器的类型
有两种类型的类加载器
- Java虚拟机自带的加载器
- 根类加载器(Bootstrap)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
- 用户自定义的类加载器
- java.Iang.ClassLoader的子类
- 用户可以定制类的加载方式
加载器规范
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
类加载器类型
1. Bootstrp loader
Bootstrp
加载器是用C++
语言写的,它是在Java
虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath
参数指定的路径以及%JAVA_HOME%/jre/classes
中的类。
2. ExtClassLoader
Bootstrp loader
加载ExtClassLoader
,并且将ExtClassLoader
的父加载器设置为Bootstrp loader
,ExtClassLoader
是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader
,ExtClassLoader
主要加载%JAVA_HOME%/jre/lib/ext
,此路径下的所有classes
目录以及java.ext.dirs
系统变量指定的路径中类库。
3. AppClassLoader
Bootstrp loader
加载完ExtClassLoader
后,就会加载AppClassLoader
,并且将AppClassLoader
的父加载器指定为 ExtClassLoader
。AppClassLoader
也是用Java
写成的,它的实现类是 sun.misc.Launcher$AppClassLoader
,另外我们知道ClassLoader
中有个getSystemClassLoader
方法,此方法返回的正是AppclassLoader.AppClassLoader
主要负责加载classpath
所指定的位置的类或者是jar文档,它也是Java
程序默认的类加载器。
若有一个类加载器能够成功加载Test类,那么这个类加载器被称为
定义类加载器
,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器
获取ClassLoader方法
获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获取当前类的ClassLoader
clazz.getClassLoader()
获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
classLoader双亲委托与类加载隔离
classLoader双亲委托与类加载隔离
类的命名空间
- 每个类加载器都有自己的命名空间,命名空间有该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
类加载器的双亲委托模型的好处:
- 可以确保Java核心库的类型安全:所有Java应用都至少会引用java.long.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,互相不可见的(正是命名空间在发挥着作用)。
借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的 - 可以确保Java核心类库所提供的类不会被自定义的类所代替
- 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器类加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。
在运行期,一个Java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同定义的。如果同样名字(即相同的完全限定名)的类是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此。