微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

JVM加载机制详解

一、概述

1、类加载子系统负责从文件系统或是网络中加载.class文件,class文件文件开头有特定的文件标识。
2、ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
3、如果调用构造器实例化对象,则该对象存放在堆区。

二、类加载执行过程

1、类使用的7个阶段

在这里插入图片描述

1.1 加载
  • 预加载
    虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件
  • 运行时加载:
    虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
    那么,加载阶段做了什么,其实加载阶段做了有三件事情:
    a.获取.class文件的二进制流;
    b.将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中;
    c.在内存中生成一个代表这个.class文件java.lang.class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的。
1.2 链接

1)验证Verification
连接阶段的第一步,这一阶段的目的是为了确保.class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,因为.class文件未必要从Java源码编译而来。

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

2)准备Preparation
准备阶段是正式为静态变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。关于这点,有两个地方注意一下:

  • 比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;
  • 比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
  • 局部变量如果没有给它赋初始值,是不能使用的。

3)解析Resolution

  1. 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用: 是对于类、变量、方法的描述,符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。
直接引用: 可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了

  1. 解析阶段负责把整个类激活,串成一个可以找到彼此的网,这个阶段的工作大体可以分为:
  • 类或接口的解析
  • 方法解析
  • 接口方法解析
  • 字段解析
1.3 初始化

类的初始化阶段是类加载过程的最后一个步骤。之前介绍的几个类加载的动作里,除了在加载阶 段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码将主导权移交给应用程序。

  • 初始化阶段 就是执行类构造器()方法的过程,()并不是程序员在Java代码中直接编写 的方法, 它是Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下:
public class TestClinit { 
	static { 
		i = 0; // 给变量复制可以正常编译通过 
		System.out.print(i); // 这句编译器会提示“非法向前引用” 
	 }
	 static int i = 1;
}
  • ()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。
  • 由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
  • ()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作, 那么编译器可以不为这个类生成()方法。接口中不能使用静态语句块,但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成()方法
  • 但接口与类不同的是, 执行接口的()方法不需要先执行父接口的()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的()方法
1.3.1 cinit与init

在这里插入图片描述

cinit方法的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作) ,只执行一次
init方法的执行时期: 对象的初始化阶段都会执行

三、类加载器

1、类加载器的作用

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.class对象,用来封装类在方法区内的数据结构。

注意: JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

2、类加载器的分类

1、jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器
2、引导类加载器是由c/c++实现的,自定义加载器是由java实现的。
3、jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。

在这里插入图片描述

之前文章有介绍各个加载器作用,不再赘述。

四、双亲委派模型

1、什么是双亲委派

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。

2、为什么需要双亲委派

黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数
稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。

3、如何实现双亲委派

loadClass(String, boolean) 函数即实现了双亲委派模型。

	protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先从缓存查找该class对象,找到就不用重新加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    	//如果找不到,则委托给父类加载器去加载
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果没有父类,则委托给启动加载器去加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClasstime().addelapsedtimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            //是否需要在加载时进行解析
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

如果需要自定义类加载器,则必须重写findClass方法,findClass认实现如下:

protected Class<?> findClass(String name) throws ClassNotFoundException { 
	throw new ClassNotFoundException(name); 
}

须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象,如果是读取一个指定的名称的类为字节数组的话,需调用defineClass方法

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { 
	return defineClass(name, b, off, len, null); 
}

defineClass主要作用:一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。如,假设 class 文件是加密过的,则需要解密后作为形参传入 defineClass 函数

4、自定义类加载器

实现方式:
所有用户自定义类加载器都应该继承ClassLoader类在自定义ClassLoader的子类是,我们通常有两种做法:

  • 1)重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
  • 2)重写findClass方法 (推荐)

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐