0032 Java学习笔记-类加载机制-初步

时间:2023-11-11 20:00:38

JVM虚拟机

  • Java虚拟机有自己完善的硬件架构(处理器、堆栈、寄存器等)和指令系统
  • Java虚拟机是一种能运行Java bytecode的虚拟机
  • JVM并非专属于Java语言,只要生成的编译文件能匹配JVM对载入编译文件格式要求,任何语言都可以交由JVM运行,比如Scala、Groovy、Fantom等,见Java虚拟机*
  • JVM虚拟机除了Sun开发的HotSpot外,还有BEA、IBM、微软、等公司都有开发。见《深入理解Java虚拟机(第二版)》
  • 查看自己用的JVM:cmd->java -version。我的是“Java HotSpot 64-Bit Server VM(build 25.92-b14 mixed mode)”。

JVM和类

  • 当调用java命令运行一个java程序时,就启动了一个Java虚拟机进程,不论该程序多么复杂,占用多大的内存,始终处于该进程中
  • JVM进程合适终止?
    • 程序运行结束
    • 程序执行过程中,遇到未捕获的异常或者错误
    • 程序运行中调用了System.exit()或者Runtime.getRuntime().exit(),退出了虚拟机
    • 程序所在平台强制结束了JVM进程
  • 虚拟机何时加载一个类?
    • 第一次使用该类时
    • 预加载

类的加载、连接、初始化

  • 当系统要使用某个类的时候,会将该类初始化,初始化依次包括加载、连接、初始化三步,一般说类的加载或类的初始化就包含了这三个步骤。
  • 类的加载
    • 概念:类加载器将.class字节码文件读入内存,并创建一个对应的java.lang.Class对象。加载进内存的每个类都有至少一个与之对象的Class对象
    • 类加载器有哪些?
      • Bootstrap ClassLoader:根类加载器,只有这个加载器不是用java语言写的;并且只有这个加载器不是ClassLoader的子类的实例
      • Extension ClassLoader:扩展类加载器
      • System ClassLoader:系统类加载器
      • 自定义类加载器:继承ClassLoader抽象类
    • 可以从哪些地方加载.class字节码文件?
      • 来源于本地文件系统
      • 来自于jar包中
      • 通过网络加载class文件
      • 把一个Java源文件动态编译,并执行加载。这个不懂?
  • 类的连接
    • 概念:负责把二进制数据合并到JRE中
    • 验证阶段:验证被加载的类是否具有正确的内部结构,并和其他类协调一致
    • 准备阶段:为类变量分配内存,并设置默认初始值
    • 解析阶段:将类的二进制数据中的符号引用替换为直接引用
  • 类的初始化
    • 类的初始化,主要是对类变量的初始化
    • 如果这个类还没有被加载和连接,那么先加载并连接。这个主要是针对父类
    • 如果这个类的直接父类还没有初始化,那就先初始化其父类
    • 依次执行类中的初始化语句
    • 因此最先被初始化的类总是java.lang.Object。参见:0023 Java学习笔记-面向对象-初始化代码块
  • JVM在何时初始化一个类
    • 一般说来,在JVM首次使用一个类时,对该类给予初始化,具体包含以下六种情况
      • 创建一个类的实例时:new操作符;反射;反序列化
      • 调用一个类的类方法
      • 访问一个类或接口的类变量
      • 用反射方式来强制创建一个类的Class对象:Class.forName("className");注意ClassLoader的loadClass()方法只会加载而不会初始化该类
      • 某个类的子类被初始化时,该类也会被初始化
      • 直接用java.exe运行某个主类,先初始化该主类。不懂?
    • 不会初始化的情况
      • 宏变量:static final变量,并且能在编译阶段就确定它的值。一个类使用另一个类的宏变量,另一个类不会被初始化。
      • 示例代码01:访问宏变量不会初始化它所在的类

示例代码01:访问宏变量不会初始化它所在的类

package testpack;
public class Test1{
public static void main(String[] args){
System.out.println(A.num); //输出8,没有输出“A类被初始化”,A类没有被初始化
System.out.println(B.num); //输出“B类被初始化”;21;B类被初始化
}
}
class A{
static{
System.out.println("A类被初始化");
}
public static final int num=8; //num的值在编译阶段就能确定下来
}
class B{
static{
System.out.println("B类被初始化");
}
public static final int num=8+Integer.valueOf(13); //num的值不能在编译阶段确定下来
}

类的加载器

  • 如何标识一个被载入JVM的类?
    • 类名+包名+类加载器名
  • 类加载器的层次结构
    • Bootstrap ClassLoader:根类加载器,
      • 没有父加载器;由C++写成,其他加载器都是Java写成;
      • 负责加载Java核心类库;也就是系统属性sun.boot.class.path的值表示的路径下的包;
      • (HotSpot?)可以在java.exe中用-D参数指定系统属性sun.boot.class.path的值,从而加载指定的附加类;
      • 见示例02:获取根类加载器加载的核心类库
    • Extension ClassLoader:扩展类加载器
      • 没有父加载器(实际上就是根类加载器?);由Java写成,是ClassLoader的子类;
      • 负责加载扩展目录JAR包中的类,即系统属性java.ext.dirs或者%JAVA_HOME%/jre/lib/ext
      • 因此可以把自己开发的类,打包成JAR包,放在该目录下
      • 见示例03:扩展类加载器的加载目录,父加载器
    • System ClassLoader:系统类加载器
      • 父加载器是扩展类加载器;由Java写成,是ClassLoader的子类;
      • 也是用户自定义的类加载器的默认父加载器,如果不特别指定的话。
      • 负责加载系统属性java.class.path系统CLASSPATH环境变量指定的目录中的类;
      • Java命令的-classpath参数可以临时指定CLASSPATH的路径
      • 示例04:系统类加载器的加载路径
    • 自定义类加载器:
      • 继承ClassLoader抽象类。
      • 默认的父加载器是系统类加载器;在自定义的时候,可以在一个方法中指定。见java的类加载器ClassLoader
    • 上面说的父加载器,并不是类的继承关系,而是加载器间实例间的关系,就是说在一个加载器中可以定义它的父加载器。

示例代码02:根类加载器加载的核心类库

package testpack;
import java.net.URL;
public class Test1{
public static void main(String[] args)throws ClassNotFoundException{
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs(); //获取根类加载器加载的全部URL数组
for (int i=0;i<urls.length;i++){
System.out.println(urls[i].toExternalForm());
}
System.out.println("-------------下面是系统属性(sun.boot.class.path)的值---------------");
System.out.println(System.getProperty("sun.boot.class.path"));
}
}

输出:

file:/C:/Java/jdk1.8.0_92/jre/lib/resources.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/rt.jar //核心类库java.lang.*位于该jar包中

file:/C:/Java/jdk1.8.0_92/jre/lib/sunrsasign.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jsse.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jce.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/charsets.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jfr.jar

file:/C:/Java/jdk1.8.0_92/jre/classes

-------------下面是系统属性(sun.boot.class.path)的值---------------

file:/C:/Java/jdk1.8.0_92/jre/lib/resources.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/rt.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/sunrsasign.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jsse.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jce.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/charsets.jar

file:/C:/Java/jdk1.8.0_92/jre/lib/jfr.jar

file:/C:/Java/jdk1.8.0_92/jre/classes

示例03:扩展类加载器的加载目录,父加载器

package testpack;
public class Test1 {
public static void main(String[] args){
ClassLoader systemLoader=ClassLoader.getSystemClassLoader(); //获取系统类加载器
System.out.println("这是系统类加载器: "+systemLoader); //输出系统类加载器
ClassLoader extensionLoader=systemLoader.getParent(); //获取扩展类加载器
System.out.println("这是扩展类加载器: "+extensionLoader); //输出扩展类加载器
System.out.println("扩展类的父加载器: "+extensionLoader.getParent()); //获取扩展类加载器的父加载器null
System.out.println("扩展类的加载路径: "+System.getProperty("java.ext.dirs")); //获取系统属性java.ext.dirs的值
}
}

输出:

这是系统类加载器: sun.misc.Launcher$AppClassLoader@73d16e93 //说明系统类加载器是AppClassLoader的实例

这是扩展类加载器: sun.misc.Launcher$ExtClassLoader@15db9742 //说明扩展类加载器是ExtClassLoader的实例

扩展类的父加载器: null //以上二者都是URLClassLoader的实例

扩展类的加载路径: C:\Java\jdk1.8.0_92\jre\lib\ext;C:\windows\Sun\Java\lib\ext

示例04:系统类加载器的加载路径

package testpack;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.lang.ClassLoader; public class Test1 {
public static void main(String[] args)throws IOException{
ClassLoader systemLoader=ClassLoader.getSystemClassLoader(); //获取系统类加载器
System.out.println("系统类加载器: "+systemLoader); //输出系统类加载器
Enumeration<URL> eml=systemLoader.getResources(""); //遍历其加载路径
while(eml.hasMoreElements()){
System.out.println(eml.nextElement());
}
System.out.println("系统属性java.class.path的值: "+System.getProperty("java.class.path"));
}
}

输出:

系统类加载器: sun.misc.Launcher$AppClassLoader@73d16e93

file:/D:/JavaWorkspace/Test/bin/

系统属性java.class.path的值: D:\JavaWorkspace\Test\bin

类的加载机制

  • 全盘负责制:
    • 当一个类加载器加载一个类时,该类依赖和引用的其他类,也有这个类加载器负责
  • 父类委托:
    • 先让父加载器加载这个类,如果加载不了,再从自己的类路径中加载。不懂?,一般说来,不同的加载器的加载路径都不同吗?
  • 缓存机制:
    • 要使用一个类时,先从缓存中查找,如果已经有了,就不另行加载,直接返回;如果没有,再加载

类加载器的8个步骤

  • 要加载的类是否已被加载?
      • 如果父类加载器不存在,那么请求使用根类加载器加载
        • 成功:返回Class对象
        • 失败:抛出ClassNotFoundException
      • 如果父类加载器存在,请求使用父加载器加载
        • 成功:返回Class对象
        • 失败:当前类加载器载入类
          • 当前类寻找加载
            • 成功:返回Class对象
            • 失败:抛出ClassNotFoundException
    • 是:返回其Class对象
  • 总的来说,先在缓存中找;再让父加载器加载,一直到没有父加载器,就用根加载器加载,如果父加载器失败,那么自己加载,还失败,那就抛出异常:ClassNotFoundException

自定义类加载器

  • 继承结构
  • java.lang.Object
    • java.lang.ClassLoader
      • java.security.SecureClassLoader
        • java.net.URLClassLoader
          • sun.misc.Launcher$ExtClassLoader //扩展类加载器就是这个类的实例
          • sun.misc.Launcher$AppClassLoader //系统类加载器就是这个类的实例
  • 所有的类加载器中,除了根加载器外,都是ClassLoader子类的实例。
  • ClassLoader类包含了一些protected和static方法
  • ClassLoader的主要方法:
    • protected Class<?> loadClass(String name,boolean resolve);
      • 根据指定名称加载类,返回指定类的Class对象
      • 自定义类加载器时,最好不要重写该方法
      • 执行步骤:
        • 用findLoadedClass(String)检查是否已加载
        • 在父加载器上调用loadClass()方法,若父加载器为null,则用根加载器加载
        • 用findClass(String)查找类
    • protected Class<?> findClass(String name);
      • 根据名称查找类
      • 自定义类加载器时,一般只重写这个方法
    • protected final Class<?> defineClass(String name, byte[] b, int off, int len)
      • 将指定的字节码文件读入byte数组,转换为Class对象
      • final,不可重写
    • protected final Class<?> findSystemClass(String name)
      • 从本地文件系统装入字节码文件
    • static ClassLoader getSystemClassLoader()
      • 返回系统类加载器
    • final ClassLoader getParent()
      • 返回该加载器的父加载器
      • final,不可重写
    • protected final void resolveClass(Class<?> c)
      • 链接指定的类c
      • 不懂?
    • protected final Class<?> findLoadedClass(String name)
      • 如果已经加载了名为name的类,则返回其Class实例

其他

  • 关于类加载器实例、类加载器实例的类、Class对象、普通类、普通类的对象
    • 简单的说,加载器是实例,他们的类有AppClassLoader和ExtClassLoader,都是URLClassLoader的子类
    • ExtClassLoader类的实例就是扩展类加载器,ExtClassLoader自身在加载的时候也有Class对象
    • AppClassLoader类的实例就是系统类加载器,AppClassLoader自身在加载的时候也有Class对象
    • ExtClassLoader和AppClassLoader二者没有继承关系,只是前者的实例是后者的实例的父加载器
    • 系统类加载器,加载一个普通类,创建对应的Class对象,连接,初始该普通类,再创建实例
    • 看下面的示例代码
  • 什么情况下需要自定义类加载器?见JVM——自定义类加载器 CSDN
  • 参考:深入探讨Java类加载器 IBM 成富
package testpack;
public class Test1 {
public static void main(String[] args){
A a=new A(); //创建普通类A的实例a
a.show(); //调用a的show()方法
ClassLoader cl=a.getClass().getClassLoader(); //通过实例a得到A类的Class对象,再获得该Class对象的加载器
System.out.println("实例a的类A的Class对象的加载器是: "+cl); //输出加载器,是AppClassLoader类的实例
Class appClazz=cl.getClass(); //通过系统类加载器实例获得它的类的Class对象
while(appClazz!=null){
System.out.println(appClazz); //输出该Class对象
appClazz=appClazz.getSuperclass(); //获得这个Class对象的父类的Class对象,一直到Object没有父类
}
}
}
class A{
public void show(){
System.out.println("这是A类的实例");
}
}

参考资料: