Jvm类加载机制总结

目录

1,Java代码执行流程

2,类加载器

3,Java类的生命周期

1,装载

2,链接:

3,初始化:

4,类加载时机


1,Java代码执行流程

如图所示,Java代码的执行步骤:

  1. 获取Java源代码
  2. 编译器8把Java代码转成class文件,编译过程主要分为一个准备三个处理过程
  3. Java代码要执行的话,需要JVM中内置的类加载器(classLoader)将字节码从硬盘加载到JVM中。
  4. JVM中的内置字节码校验器(bytecode verifier)会检查字节码是否存在运行期错误(栈溢出等).若通过检测,字节码校验器会将字节码传递给解释器(interpreter | JIT);
  5. 解释器会将字节码逐一翻译成当前系统的机器码(machine code)
  6. 将机器码转给操作系统后,操作系统会以main方法作为入口去执行程序。然后Java程序就运行起来了。

额外应用:

        JIT和interpreter区别:interpreter和JIT都是字节码的解释器,但是JIT是为了提供解释工作效率而提出来的一个即时编译器。

问题1:JIT的原理是什么?

        其作用是把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

问题2:当前字节码如何确定使用JIT还是interpreter?

        Java程序最初是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会认为这是“热点代码”(Hot Spot Code)。热点代码则使用JIT进行解释编译。

问题3:如何判断代码是热点代码?

        热点代码分为2类:多次被调用的方法多次被执行的循环体。热点判定方式的种类
1、基于采样的方式探测(Sample Based Hot Spot Detection) 。周期性检测各个线程的栈顶,发现某个方法经常出现在栈顶,就认为是热点方法。好处就是简单,缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。
2、基于计数器的热点探测(Counter Based Hot Spot Detection)。某个方法超过阀值就认为是热点方法,触发JIT编译。(涉及计数器的热度半衰减过程)

        两个计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)触发了JIT编译,编译工作完成,这个方法的入口就会被系统自动改为新的编译入口,就会调用编译的版本。

问题3:JIT有哪些种类?

        JVM中默认内置了两款即时编译器,称为Client CompilerServer Compiler。可以用指定参数的方式,指定采用Client模式和Server模式,默认是mixed模式

java -Xint 解析 java -Xcomp 编译

        Client Compiler和Server Compiler会实现分层编译(JDK1.7默认有)。
第0层 程序解析执行,解析器不开启性能监控,可触发第一层编译;
第1层 编译成本地相关代码,进行简单优化;
第2层 除编译成本地相关代码外,还进行成编译耗时较长的优化。

  • Client Compiler获得更高的编译速度
  • Server Compiler获得更好的编译质量,无须承担性能监控任务

2,类加载器

        类加载过程可以描述为:通过一个类的全限定名来获取描述该类的二进制字节流。实现这个动作的代码被称为类加载器(Class Loader)。

系统自带的类加载器分为三种:

  • 启动类加载器(BootstrapClassLoader):它用来加载 Java 的核心类(存放<JAVA_HOME>\lib目录,或者被-Xbootclasspath 参数所指定的路径),是用原生 C++ 代码来实现的,是虚拟机自身的一部分。我们在代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它,如果获取它的对象,将会返回 null。
  • 扩展类加载器(ExtClassLoader):以 Java 代码的形式实现的。负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。
  • 应用类加载器(AppClassLoader):它负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。
  • 自定义类加载器:如果用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接从 ExClassLoader 里拿不到它的引用,同样会返回 null。

        上图展示的各种类加载器之间的层次关系被称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载 器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

        双亲委派机制:当一个自定义类加载器需要加载一个类,比如 java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直往前找,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。如果启动类加载器已经加载了某个类比如 java.lang.String,所有的子加载器都不需要自己加载了。

双亲委派模型的实现:

    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 首先,检查请求的类是否已经被加载过了 
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父类加载器抛出ClassNotFoundException 
                // 说明父类加载器无法完成加载请求 
            }
            if (c == null) {
                // 在父类加载器无法加载时 
                // 再调用本身的findClass方法来进行类加载 
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

        这个模型的好处在于 Java 类有了一种优先级的层次划分关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。

3,Java类的生命周期

         一个类在 JVM 里的生命周期有5个阶段如果细化也可以分为7个阶段,具体如下。装载(Loading),链接(Linking),初始化(Initialization),使用(Using),卸载(Unloading)。其中链接又分为:验证(Verification),准备(Preparation),解析(Resolution)。

        其中装载,链接和初始化称为类加载。

1,装载

主要是将外部的.class文件加载到Java的方法区中,该阶段主要完成以下三个动作。

  1. 通过一个类的全限定名(包名+类名)来获取定义此类的二进制文件
  2. 将字节流所代表的静态存储结构转化成方法区的运行时结构数据。
  3. 在内存中生成一个代表这个类的对象(java.long.Class对象),作为方法区该类的各种访问数据入口。

加载class文件有以下几种方式。

  1. 从 ZIP 压缩包中读取,这很常见,成为日后 jarwar格式的基础;
  2. 通过网络获取,典型场景:Web Applet
  3. 运行时计算生成,使用最多的是:动态代理技术;
  4. 由其他文件生成,典型场景是 JSP 应用;
  5. 从加密文件中获取,典型的防 Class 文件被反编译的保护措施。

2,链接:

主要分为验证(Verification),准备(Preparation),解析(Resolution)三个部分。

        1,验证:验证是连接阶段的第一步,这一阶段的目的是确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。如果输入的字节流如不符合 Class 文件格式的约束,将抛出一个 java.lang.VerifyError 异常或其子类异常。

        验证主要分为以下三个部分。

1,文件格式验证:主要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。比如:
        是否以魔数 0xCAFEBABE 开头;
        主、次版本号是否在当前 Java 虚拟机接受范围之内;
        常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)
2,元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。比如:
        这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)
        如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
        类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
3,字节码验证:这一阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的;
4,符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在 连接的第三阶段——解析阶段中发生。

        2,准备:准备阶段是为定义的类变量(即静态变量,被 static 修饰的变量)分配内存并初始化为标准默认值(比如 null 或者0 值)。注意如下:

//在准备阶段value会被分配内存赋初始值0
public static int value = 123;

//在准备阶段value会被分配内存并赋值123
public static final int value = 123;

        3,解析:解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。在字节码中所有的引用都是用符号直接应用到对应的代码,但是在实际的Java代码程序中,所有的引用都是类似指针式的引用。

3,初始化:

        类的初始化阶段是类加载过程的最后一个步骤。初始化阶段就是执行类构造器 <clinit>() 方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量赋值动作和静态语句块(static{})中的语句合并产生的,收集顺序是按在源文件中的出现顺序决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。

public class A {
     static int a = 0 ;
     static {
         a = 1;
         b = 1;
     }
     static int b = 0;
     public static void main(String[] args) {
         System.out.println(a);
         System.out.println(b);
     }
 }
/**
运行结果:
1
0
static 语句块,只能访问到定义在 static 语句块之前的变量。

**/

        Java 虚拟机会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。<clinit> 是类(Class)初始化执行的方法,<init> 是对象初始化执行的方法(构造函数)。

public class A {
     static {
         System.out.println("1");
     }
     public A(){
         System.out.println("2");
         }
}
public class B extends A {
    static{
         System.out.println("a");
    }
     public B(){
         System.out.println("b");
     }
     public static void main(String[] args){
         A ab = new B();
         ab = new B();
     }
 }
 /**
  * 
结果:
1
a
2
b
2
b
  */

4,类加载时机

        关于在什么情况下需要开始类加载过程的第一个阶段「加载」,JVM 规范中并没有进行强制约束,但是对于初始化阶段,JVM 规范规定了只有六种情况必须立即对类进行「初始化」(加载、验证、准备自然需要在此之前开始):这六种情况称为主动引用

1,遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有:
        使用 new 关键字实例化对象的时候;
        读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;
        调用一个类型的静态方法的时候;
2,使用 java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化;
3,当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
4,当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
5,当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类;
6,当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

        所有的主动引用都会触发类加载,如果是被动引用则不触发加载,下面说几个常见的被动引用的例子。

  实例1:通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

/**
 * 通过子类引用父类的静态字段,不会导致子类初始化
 **/
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

//输出:
//SuperClass init!
//123
    上述代码运行之后,只会输出SuperClass init!,而不会输出SubClass init!。
    对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

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

public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

//运行之后发现没有输出 SuperClass init!,说明并没有触发类 SuperClass 的初始化阶段。

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

public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}


//输出:
//hello world

    上述代码运行之后,也没有输出ConstClass init!,这是因为虽然在Java源码中确实引用了 ConstClass 类的常量 HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值 hello world 直接存储在 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用,实际都被转化为NotInitialization 类对自身常量池的引用了。

    也就是说,实际上 NotInitialization 的 Class文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 文件后就已不存在任何联系了。

      

参考文献:

面试题:请介绍 JVM 类加载机制_jvm类加载机制 面试_徐俊生的博客-CSDN博客

JVM的即时编译器JIT_sinat_37138973的博客-CSDN博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值