JVM-类加载器子系统

类加载器子系统

类加载器子系统作用

Class文件并非特指具体磁盘中的一个文件,而应该是一串二进制字节流。由于JVM跨语言平台的特性(Class文件不一定来源于Java,还可以来源于KolinGroovyScala等。JVM只关心字节码,不关心语言),只要符合JVM规范的二进制字节流都可以被识别,包括但不限于磁盘文件、网络、数据库、内存或者动态产生等。

而类加载器子系统负责将字节码文件从以上的来源中加载Class文件。其中Class Loader只负责Class文件的加载,而这个程序是否可以执行,由Execution Engine决定。

非正文(琐碎的笔记):

  • 加载的类信息存放于一块称之为方法区的内存空间
  • 除了类的信息外,方法区还会存放运行时常量池信息,可能包括字符串字面量和数字常量(这部分常量信息是Class文件中常量部分的内存映射)
  • 常量池在运行的时候加载到内存中,就是运行时常量池
  • 比如有个类为Car.java,编译完之后成为Car.class,类的加载器将这个Car.class加载到方法区中(成为DNS元数据模板)
  • 通过getClassLoader()方法可以得到类的加载器,然后在内存中调用Car.class中的构造器,创建Car的对象。
  • 通过实例car.getClass()也可以找到这个类本身
  • class对象(对象数据)在堆,class元数据模板(结构数据)在方法区,前者根据后者创建
  • 如果调用一个方法,首先要先用ClassLoader()去加载这个方法所在的类。

类加载机制

JVMClass文件加载到内存中,然后经过验证准备解析初始化过程,得到JVM可以直接使用的Java类型,这个过程被成为JVM的类加载机制。由于这个过程发生在程序运行期间,所以也避免不了性能上的开销,但是也为Java极高的扩展性和灵活性奠定了基础。

整个过程可以分为:加载(Loading) --> 链接(Linking)(验证、准备、解析)–> 初始化(Initialization

加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,JVM在此阶段主要完成以下三件事情:

  1. 通过一个类的全限定名获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

方法区:jdk7以前叫做永久代,之后叫做元空间

  1. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个大Class类所对应的一个实例,就代表了这个二进制字节流对应的类本身。

对于第一点,“通过一个类的全限定名获取此类的二进制字节流”,其中获取的源头、获取的方式有很多:

  • 从ZIP压缩包中读取,之后可以为从JAREARWAR中读取;
  • 网络中获取,典型应用就是Web Applet
  • 运行时计算生成。在java.lang.reflect.Proxy中利用ProxyGenerator.generateProxyClass()为特定的接口生成形式为"*$Proxy"的代理类的二进制字节流;代理模式可以参考之前的博客记录:设计模式-代理模式
  • 从其他文件中生成,例如JSP应用中,由JSP生成对应的Class文件;
  • 从数据库中获取;
  • 从加密文件中获取。防止Class被反编译的保护措施,通过加载时解密Class文件来确保安全;

链接

验证

确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。(因为Class文件不一定要由Java源码编译而来,所以完全可以人为在二进制编辑器用01敲出Class文件,存在风险)

主要包括四种验证方式:

  • 文件格式验证

  • 元数据验证

  • 字节码验证

  • 符号引用验证

准备

准备阶段是正式为类中定义的变量(静态变量,被static修饰的变量)分配内存并设置变量初始值(零值)的阶段。不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。这个阶段不会为实例变量分配初始化(因为此时还未创建对象),类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

public static final int a = 1;
public static int b = 2;

对于两个变量a和b。其中a在准备阶段的时候,初始值已经是1,而不是零值0,因为此时a被static final修饰,在编译成Class文件的时候,就已经对变量a进行了显示初始化。但是对于变量b,在准备阶段的时候,初始值为0,而不是2。2的赋值操作会在初始化阶段执行<clinit>()方法。基本数据类型的零值如下:

类型默认初始值
byte(byte)0
short(short)0
int0
long0L
float0.0f
double0.0
char\u0000
booleanfalse
referencenull
解析
  • 解析阶段是将常量池中的符号引用替换为直接引用的过程。

    • 符号引用就是一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用无歧义的定位到目标即可。

    • 直接引用是可以指向目标的指针、相对偏移量或者是一个可以间接定位到目标的句柄。

  • 解析动作通常伴随着JVM在执行完后再执行。

相关解释

参考:走进java_符号引用与直接引用

符号引用:编译的时候(也就是生成Class文件的时候,此时是和JVM布局没有关系的!),这个时候JVM并不能确定引用类的具体地址,所以说大多都是以符号进行标记,而解析阶段就是真正将符号引用转换成直接引用的过程。

直接引用:该引用是和虚拟机的布局密切相关的,不同的虚拟机对于不同的符号引用翻译出来的直接引用一般是不同的。如果有了直接引用,则该变量一定是在内存中的!

或者也可以参考:符号引用、直接引用

初始化

初始化阶段,就是执行类构造器<clinit>()方法的过程:

  • <clinit>()是由编辑器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中合并产生的。对于static final修饰的变量,因为它是无法在次赋值的,所致该变量是不会经过<clinit>()方法的;
  • <clinit>()方法和类的构造函数(虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式的调用父类构造器。由于父类的加载要早于子类的加载,所以说JVM第一个被执行<clinit>()方法的类一定是java.lang.Object.
public class ClinitTest1 {
    static class Father{
        // 准备阶段:0 --> 解析阶段:先是赋值操作:A=1,然后是静态代码块:A=2
        public static int A = 1; 
        static {
            A = 2;
        }
    }

    static class Son extends Father{
        public static int B = A;
    }

    public static void main(String[] args) {
        // 加载Father类,其次加载Son类
        System.out.println(Son.B); // 2
    }
}

如果父类代码修改为:

static class Father{
    static {
        A = 2;
    }
    public static int A = 1; 
}

此时 System.out.println(Son.B);的输出结果为1,静态代码块和显式赋值的操作顺序按照语句出现的前后执行。

其中要注意避免非法前向引用的发生:

public class ClassInitTest {
    static {
        number = 20; 
        System.out.println(number); // 此时不能够访问number变量,因为这个时候还没有声明变量
        // 报错:
        // 非法的前向引用:因为我们声明的是在后面,我们无法进行调用,可以赋值
    }
    private static int number = 10;
}
  • <clinit>()并不是必须执行的,如果一个类中没有赋值操作或者静态代码块,则编译器不会为这个类生成<clinit>()方法。
  • <clinit>()方法是天然的线程安全的。在多线程环境中,只有一个线程可以去执行这个类的<clinit>()方法,其他线程都出于阻塞状态,直到这个活动线程完成该方法的操作。

类的加载器

JVM支持两种类加载器:

  1. 引导类加载器(BootStrap Class Loader):由C/C++编写,Java环境中显示为null值,无访问权限。
  2. 用户自定义类加载器(User-Defined Class Loader):继承于ClassLoader抽象类,由Java编写,可以访问。

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
        // 获取了Launcher的一个内部类
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);


        // 获取其上层:sun.misc.Launcher$ExtClassLoader@1b6d3586
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);

        // sun.misc.Launcher$AppClassLoader@18b4aac2
        // sun.misc.Launcher$ExtClassLoader@1b6d3586
        // 两者是一个包含的关系

        // 获取其上层:null(引导类加载器)
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println(bootStrapClassLoader);
        
        // 对应用户自定义类的加载器:默认使用系统类加载器加载用户自定义的类
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader);


        // 间接说明了:String类的类加载器是BootStrap ClassLoader加载的
        // 因为是null的
        // java的核心类库都是Boot ..加载的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1); // null
    }
}

引导类加载器(BootStrap Class Loader

  • BootStrap Class Loaderc/c++编写的,嵌套在JVM内部;
  • 用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承于java.lang.ClassLoader,没有父类加载器
  • 出于安全考虑,BootStrap启动类加载器只加载包名为javajavaxsun等开头的类
  • 加载扩展类和应用程序类加载器,并指定为它们的父类加载器

扩展类加载器(Extension ClasssLoader

  • sun.misc.Launcher$ExtClassLoader形式实现
  • 负责加载路径:jre/lib/ext

应用程序类加载器(系统加载器,AppClassLoader

  • sun.misc.Launcher$AppClassLoader形式实现

  • 父类加载器为扩展类加载器(父类加载器不是父类!)

  • 该类加载的是程序中默认的类加载器(用户自定义的类一般来说都是它加载的)

  • 它负责加载环境变量classpath或系统属性,java.class.path 指定路径下的类库

  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载

自定义类加载器

日常开发中,类的加载几乎是由上面3种类加载器相互配合执行的,在必要时,还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器?

  • 隔离加载类(中间件拥有自己依赖的jar包,避免冲突)
  • 修改类加载的方式(BootStrap是必须的,其余的不必要)
  • 扩展加载源(加载的来源可以扩展,例如从数据库中)
  • 防止源码泄露(对字节码文件的加密,采用自定义类加载器进行解密)

ClassLoader抽象类常见方法

方法名称描述
getParent()返回该类加载器的超类加载器
loadClass(String name)加载名为name 的类,返回结果为 java.lang.Class 类的实例
findClass(String name)查找名为name 的类,返回结果为 java.lang.Class 类的实例
findLoadedClass(String name)查找名为name 的已经被加载过类,返回结果为 java.lang.Class 类的实例
defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
resolveClass(Class<?> c)连接指定的一个 Java类

双亲委派机制

一个类加载器收到了类加载的请求,它并不会自己先去加载,而是将这个请求委托给父类加载器去执行。

如果这个父类加载器还有父类加载器,则进一步向上委托,依次委托,请求最终到达启动类加载器。

如果父类加载器可以完成类的加载操作,就成功返回。如果无法加载此类,子加载器才会自己去尝试加载,这就是双亲委派机制

优势

  • 避免了类的重复加载(类只有一个)
  • 保护程序安全,防止核心API被随意的篡改
public class QybStart {
    public static void main(String[] args) {
        // java.lang包访问时需要权限的
        // 阻止用户在核心包下创建自定义类
        // 此时报错:Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
        // 出于安全的考虑
        System.out.println("Hello~");
    }
}

沙箱安全机制

自定义String类,但是加载自定义的String类会率先使用引导类加载器加载,但是引导类加载器在加载的过程中会优先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar下的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制

其他

JVM中表示两个class对象是否为同一个类存在的两个必要条件:

  1. 类的完整类名必须一致,包括包名;(全限定名一致)
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

对类加载器的引用

JVM必须知道这个类是用启动类加载器加载的还是用户类加载器加载的。

引导类加载器的实例对象是null,所以就不会记录引导类加载器。但是如果一个类是有用户类加载器加载的,那么JVM就会将这个类加载器的一个引用作为信息的一部分保存在方法区。当解析一个类型到另一个类型的引用时,JVM需要保证这两个类型的类加载器是相同的。

类的主动引用和被动引用

Java程序对类的使用方式分为两种

  • 主动使用情况

    1. 创建类的实例

    2. 访问某个类或接口的静态变量,或者对该静态变量赋值

    3. 调用类的静态方法

    4. 反射(如:Class.forName("cn.duniqb.Test")

    5. 初始化一个类的子类

    6. Java虚拟机启动时被标明为启动类的类

    7. JDK7开始提供的动态语言支持

      java.lang.invoke.MethodHandle 实例的解析结果,REF_getStatic, REF_putStatic, REF_invokeStatic 句柄对应的类没有初始化,则初始化

  • 除了以上7种,其他方式都被看做是被动使用,都不会导致类的初始化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YuanbaoQiang

你的鼓励将是我创作的最大动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值