Bootstrap

jvm/tomcat类加载机制(知识汇总)

启动执行一个main方法大体流程:

  1. 将java文件编译成class文件
  2. 开始启动流程:window环境下由java.exe调用jdk安装目录下的jvm.dll(jdk安装目录\jre\bin\server)初始化一个java虚拟机;(C++实现的)
  3. 并创建一个引导类加载器实例(C++实现的)
  4. 实例化sun.misc.Launcher(扩展类加载器应用类加载器都是这个类的内部类)(应该也是C++实现的)
  5. 由sun.misc.Launcher.AppClassLoader加载入口方法类(main方法)
  6. 由C++发起调用main方法
  7. java程序运行结束,销毁jvm;

jdk自带类加载器关系

在这里插入图片描述

loadClass方法解析:

加载》验证》准备》解析》初始化》使用》卸载
1、加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。

2、验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

3、准备

准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
(1)类变量(static)会分配内存,但是实例变量(成员变量)不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
当然还有其他的默认值。
注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。

4、解析

解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5、初始化

这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< classinit >()方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值

JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.shengsiyuan.Test”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。

    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 {
                //双亲委派机制:由应用程序类加载器向上委派扩展类加载器,再向上委派给引导类加载器,
                //引导类加载器的parent==null
                    if (parent != null) {
                       //如果不是引导类加载器就向上委托
                        c = parent.loadClass(name, false);
                    } else {
                    	//由引导类加载器去试着加载这个类; 如果没有找到返回NULL。
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				//注意:这段代码是在loadClass递归调用里面,递归的特性是先由外而内,到底部后,再由内到外;
				//不过这个递归正常情况下只有两层:排除自定义类加载器,正常情况下是由应用类加载器发起
				//this指向的变化:应用类加载器》扩展类加载器》应用类加载器
                if (c == null) {
                    //如果父类加载器找不到要加载的class就会进入这里
                    long t1 = System.nanoTime();
                    //由子类加载器去试着加载
                    //findClass根据name去查找对应Class文件,找到后生成输入流,再将输入流转换字节数组,
                    //再有defineClass方法将字节数组转换成Class赋值给c
                    //只要c!=null,递归结束loadClass的最终结果就是c
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

1.为什么用双亲委派机制
沙箱安全机制:防止核心api库被篡改
避免类的重复加载:父类加载器已经加载过子类就不用加载了,保证加载类的唯一性;
2.全盘负责委托机制:当一个ClassLoder装载一个类时,除非显示的使用另一个ClassLoder,该类所依赖/引用的类也有这个ClassLoder去加载

  1. 项目中的Class文件不是一次性全部加载的,引导类加载器加载的文件也是如此
  2. jdk加载机制是双亲委派,通过递归实现,loadClass调用时注意this的变化,this到了扩展类加载器就到底了,因为它的parent=null, findBootstrapClassOrNull(name);这个方法是引导类加载器查找和加载类的方法;
  3. 自定义类加载器只用继承ClassLoader,主要重写findClass,如果需要打破双亲委派需要重写loadClass。注意全盘负责委托机制。最好是根据name中包名判断是否用自定义类加载器。
  4. 查看类加载器都加载了哪些类:
    bootstrapLoader(引用类加载器):Launcher.getBootstrapClassPath().getURLs()
    扩展类加载器:System.getProperty(“java.ext.dirs”)
    应用类加载器:System.getProperty(“java.class.path”)
  5. 根据源码可以看出sun.misc.Launcher是单例的;
  6. tomcat打破双亲委派机制原因:
    1.部署多个应用程序时,各个应用程序依赖第三方jar包有不同版本时,需要分开加载;版本相同类库可以共享;
    2.web容器有自己依赖的类库,不能与应用程序类库混淆,基于安全需要隔离;
    3.web容器的热加载功能;卸掉原来的类加载器,重新创建新的类加载器重新加载变更文件。后台开启线程检查是否有文件变更;
    在这里插入图片描述
;