Java虚拟机类加载机制

https://www.cnblogs.com/aspirant/p/7200523.html
Java 类加载机制(阿里面试题)

1 类加载器与类的”相同“判断、

详情见《深入理解Java虚拟机》

类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

通俗一点来讲,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。

2 类加载器种类

  • 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
  • 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
  • 应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
  • 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类

3 双亲委派模型

类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的;除了启动类加载器,每个类都有其父类加载器(父子关系由组合(不是继承)来实现);
所谓双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试自己加载。

双亲委派好处

  • 避免同一个类被多次加载;
  • 每个加载器只能加载自己范围内的类;

4 类加载过程

类加载机制分为三个步骤:加载,连接(验证、准备、解析),初始化;

类的生命周期
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。

类的生命周期

1 加载:三步

通过一个类的全限定名来获取定义此类的二进制字节流

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2 连接:
1)验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器clinit()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

3)解析 P220
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口字段类方法接口方法方法类型方法句柄调用点限定符7类符号引用进行。

3 初始化
开始执行类中定义的Java程序代码(字节码);
在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }

在准备阶段value1和value2都等于0;
在初始化阶段value1和value2分别等于5和66;

在初始化阶段,则根据程序代码去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程;
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问;

public class Test
{
    static
    {
        i=0;//给变量赋值可以正常编译通过
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}

()方法与实例构造器()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类()方法执行之前,父类的()方法方法已经执行完毕,本文开篇7中举例代码中,结果会打印输出:SSClass就是这个道理。

由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产()方法。

接口中不能使用静态语句块,但仍然有变量(类变量)初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。

接口的成员特点:
A:成员变量 只能是常量。默认修饰符 public static final
B:成员方法 只能是抽象方法。默认修饰符 public abstract

虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

package jvm.classload;
 
public class DealLoopTest
{
    static class DeadLoopClass
    {
        static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init DeadLoopClass");
                while(true)
                {
                }
            }
        }
    }
 
    public static void main(String[] args)
    {
        Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
 
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待):

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass

需要注意的是,其他线程虽然会被阻塞,但如果执行client()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入client()方法。同一个类加载器下,一个类型只会初始化一次。

备注:
1:通过数组定义来引用类,不会触发此类的初始化;见:8
2:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;见:9

何时触发类的初始化?具体见P211

  1. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,调用一个类的静态方法时;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化;
  3. 初始化一个类的时候吗,其父类未初始化,需先触发父类的初始化;
  4. 当jvm启动时,需要指定一个要执行的主类(main()方法),jvm会先初始化这个主类;
  5. 使用动态语言支持时,一个实例使用了方法句柄且没有 初始化,则需要先触发初始化。

5 自定义类加载器

要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流

6 JAVA热部署实现

热部署:在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。
类加载后生成class文件,如果直接修改虚拟机的源代码,重新加载class文件,破坏性太大;提倡的方法是创建自己的 classloader 来加载需要监听的类,控制类加载的时机,从而实现热部署。

Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。

热部署步骤:
1、销毁自定义classloader(被该加载器加载的class也会自动卸载);
2、更新class
3、使用新的ClassLoader去加载class

7 类加载:子类调用父类的静态变量,子类不会被初始化

对于静态字段,只有直接定义这个字段的类才会被初始化;例如:

public class SSClass
{
    static
    {
        System.out.println("SSClass");
    }
}   
public class SuperClass extends SSClass
{
    static
    {
        System.out.println("SuperClass init!");
    }
 
    public static int value = 123;
 
    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
public class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
 
    static int a;
 
    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
public class NotInitialization
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}

运行结果:

SSClass
SuperClass init!
123

为什么没有输出SubClass init?解释一下:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

8 特殊:通过数组定义来引用类,不会触发此类的初始化

public class NotInitialization
{
    public static void main(String[] args)
    {
        SuperClass[] sca = new SuperClass[10];//不会初始化SuperClass
    }
}

9 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

class ConstClass
{
    static
    {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

public class NotInitialization
{
    public static void main(String[] args)
    {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

输出

hello world

你可能感兴趣的:(Java虚拟机类加载机制)