Java虚拟机之类加载机制原理详解

1 JVM基本概念

1.1 概述

   JVM是可运行Java代码的虚拟计算机,JVM是运行在操作系统之上的,它与硬件没有直接的交互。JVM说到底,其实就是在物理机上运行的程序,同时他依托物理机而拥有了完善的硬件架构和指令系统。

1.2 运行过程

  Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 。也就是如下:
   • Java源文件—->编译器—->字节码文件
   • 字节码文件—->JVM—->机器码
   每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

1.3 JVM的体系结构

在这里插入图片描述

2 Java类加载机制

2.1 什么是类加载器

   类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流,实现这个动作的动能的模块叫做类加载器。

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

  类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。
  即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
  通俗一点来讲,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
  这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
  两个类相同的前提:1 包名+类名一致,2类加载器一致。

2.3 类加载器种类

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

2.3 双亲委派模型

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

  双亲委派好处
     避免同一个类被多次加载;
     每个加载器只能加载自己范围内的类;

2.4 类加载过程

  类加载分为三个步骤:加载,连接,初始化;
  如下图 , 是一个类从加载到使用及卸载的全部生命周期:
在这里插入图片描述

2.4.1 加载

  1、通过类的全限定名来获取类的二进制字节流(用户可操作,自定义类加载器(实现通过一个类的全限定名获取类的二进制字节流的动作放在jvm外部实现的模块))

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

  3、在内存中生成代表这个类的Java.lang.class类对象,作为这个数据访问的入口。

   注:数组类本身不是通过类加载器创建,而是有jvm直接创建

2.4.2 链接

验证:
  防止危害jvm安全,目的是确保class文件中字节流中包含的信息符合当前虚拟机的要求。主要有四个方面的验证:

         1、文件格式验证,是否以魔数开始,版本信息是否为jvm接受,常量池中是否有不支持的类型。
         2、元数据验证:进行语法分析,是否每个类都有父类,是否有语法错误,是否继承自final。
         3、字节码验证:语义分析,通过数据流和控制流分析,确保语义是合法的。
         4、符号引用验证:是否能找到对应的类。发生在讲符号引用转化为直接引用时,在解析中产生。

准备:
  为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内);
  注意:static+ final修饰的变量在准备阶段之后就是用户指定的值。

解析:
  将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。

2.4.3 初始化

  在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:

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

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

  1 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是clinit方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
  2 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
  3 如果超类还没有被初始化,那么优先对超类初始化,但在clinit方法内部不会显示调用超类的clinit方法,由JVM负责保证一个类的clinit方法执行之前,它的超类clinit方法已经被执行。
  4 JVM会保证类的clinit方法加锁。 JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
  5 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类clinit方法;

2.5 类初始化的时机详解( 参考链接)

2.5.1 主动引用

  1 遇到new、getstatic(获取静态变量)、putstatic(赋值静态变量)或invokestatic(调用静态方法)这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。注意:被final修饰、已在编译期把结果放入常量池的静态字段除外;

  2 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  4 当虚拟机启动时,用户需要指定一个要执行的主类( 包含main()方法的那个类),虚拟机会先初始化这个主类。

  5 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle(动态方法调用)实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。

  这五种情况称为对一个类进行主动引用。其余情况都不会触发初始化,称为被动引用。

2.5.2 被动引用

  1 通过子类引用父类的静态字段,不会导致子类初始化;

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

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

2.6 反射中Class.forName()和ClassLoader.loadClass()的区别

  Class.forName(className)方法,内部实际调用的方法是Class.forName(className,true,classloader);第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。

  ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

引申:java获取反射机制的三种方式
1.通过new对象实现反射机制,
2.通过路径实现反射机制
3.通过类名实现反射机制

    //方式一(通过建立对象)
	Student stu = new Student();
	Class classobj1 = stu.getClass();
	System.out.println(classobj1.getName());
	
	//方式二(所在通过路径-相对路径)
	Class classobj2 = Class.forName("fanshe.Student");
	System.out.println(classobj2.getName());
	
	//方式三(通过类名)
	Class classobj3 = Student.class;
	System.out.println(classobj3.getName());

2.7 Java父子类初始化加载顺序

注意,网上的大多说法都是错的,加载顺序不是固定的,由代码顺序来决定
1 父类静态成员变量 父类静态代码块 (这两个优先加载,但加载顺序根据代码顺序决定,下面都是如此)
2 子类静态成员变量 子类静态代码块 (加载顺序根据代码顺序决定)
3 父类非静态成员变量 父类非静态代码块(加载顺序根据代码顺序决定)
4 父类构造函数
5 子类非静态成员变量 子类非静态代码块(加载顺序根据代码顺序决定)
6 子类构造函数
父类和子类的静态方法和非静态方法只有在被调用时才会加载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值