JVM基础入门

JVM 基础入门

JVM 基础

聊一聊 Java 从编码到执行到底是一个怎么样的过程?

JVM基础入门_第1张图片

假设我们有一个文件 x.Java,你执行 javac,它就会变成 x.class

这个 class 怎么执行的?

当我们调用 Java 命令的时候,class 会被 load 到内存,这块叫【Classloader】,会被 Classloader 装载到内存里。

一般的情况下,我们写自己的类文件的时候也会用到 【Java 的类库】,所以他会把 Java 类库相关的这些个类也要装载到内存里,装载完成之后会调用【字节码解释器】或者是【JIT 即时编译器】来进行解释或编译,编译完之后由【执行引擎】开始执行,这以及下面面对的,那就是操作系统和硬件了。

Java 编译好了之后变成 class, class 会被 load 到内存,与此同时像什么 string, object 这些个 class 也都会被 load 到内存。

Java 是这个解释执行的还是编译执行的?

其实解释和编译是可以混合的,特别常用的一些代码,代码用到的次数特别多,这个时候他会把代码做成一个及时的编译,做成一个本地的编译。

你可以理解为就像 c 语言在 Windows 上执行的时候,把它编译成 exe 一样,那么下次再执行这段代码的时候,就不需要通过解释器来一句一句解释来执行了,执行引擎可以直接交给操作系统去让它调用,这个效率要高很多,不是所有的代码都要都会被 GIT 进行及时编译的,如果是这样的话,那整个 Java 就完全变成了不能跨平台了。

所以有一些特定的,执行起来执行次数好多好多,用的特别多的时候,这个时候会进行一个即时编译器的编译。

什么是 JVM?

所谓的 JVM 虚拟机,其实它本身是一个规范,是虚构出来的一台计算机。拥有自己的操作系统,是一个跨语言平台。

为什么 JVM 虚拟机能够支持多种语言运行在上面呢?

最关键的原因是就是因为 class 这个东西,我们可以说任何的语言,只要你能编译成 class,符合 class 文件的规范,你就可以扔在 Java 虚拟机上去执行。

注意:JVM 只和 class 文件有关,与 java 无关。

JDK 官网:Java SE Specifications (oracle.com)

维基百科:Java虚拟机 - 维基百科,自由的百科全书 (wikipedia.org)

甲骨文中国:Java 软件 | Oracle 中国

常见的 JVM 实现

  1. Hotspot(最常用)

    • oracle 官方,我们做实验用的 JVM,Java 虚拟机
    • java -version 命令可查看使用的是什么 JVM
    • Hotspot 8 之后要收费,但 Open JDK 是开源的版本,免费
  2. Jrockit

    • BEA 曾经号称世界上最快的 JVM
    • 被 Oracle 收购,合并于 hotspot
  3. TaobaoVM(免费)

    • hotspot 深度定制版
  4. LiquidVM

    • 直接针对硬件
  5. azul zing(特别贵)

    • 最新垃圾回收的业界标杆(号称 1ms 以内)
    • www.azul.com
  6. J9-IBM

    • Microsoft VM

JDK JRE JVM 的关系

包含关系。

JVM – 运行 java 字节码的虚拟机

JRE – java 运行环境 == jvm + core(核心类库)

JDK – java 开发工具包 == jre + development kit(开发工具)

Class 文件格式

Class 文件格式(File Format

  • 二进制字节流(由 java 虚拟机解释)
  • 数据类型: u1 u2 u4 u8 和 _info (表类型)
    • info 的来源是 hotspot 源码中的写法
  • 查看 16 进制格式的 ClassFile
    • 软件 - sublime / notepad /
    • IDEA 插件 - BinEd
  • 有很多可以更好观察 ByteCode 的方法
    • 终端命令:javap -v 参数详细查看
    • JBE 可以直接修改
    • JClassLib - IDEA 插件之一
  • 文件结构
    • General Information(通用信息): 这个部分包含了一些通用的信息,比如文件的魔数(magic number),文件版本号等。
    • Constant Pool(常量池): 常量池是一个重要的数据结构,它存储了一系列常量,包括字面值、符号引用、类和接口的名称等。常量池在字节码文件中起到了存储和管理常量数据的作用。
    • Interfaces(接口): 这部分定义了该类实现的接口。
    • Fields(字段): 这部分描述了类的字段(成员变量),包括字段的名称、类型以及修饰符等信息。
    • Methods(方法): 这部分描述了类的方法,包括方法的名称、参数列表、返回类型、字节码指令等。
    • Attributes(属性): 属性提供了关于类、字段或方法的其他信息,例如源代码行号、注解等。这个部分可以包含多个不同类型的属性,每个属性都有一个名称和相应的数据。

如下图示:

  • magic:魔数,是一个固定的字节序列,用于标识 Java 字节码文件。它的值为 0xCAFEBABE,用于确定文件是否是有效的 Java 字节码文件。
  • minor version:次版本号,指示 Java 编译器版本的次要更新。用于描述字节码文件与Java虚拟机版本的兼容性。
  • major version:主版本号,指示 Java 编译器版本的主要更新。同样用于描述字节码文件与 Java 虚拟机版本的兼容性。
  • constant_pool_count:常量池计数,表示常量池中常量的数量。十六进制 0010 转化为十进制为 16。常量池真正存储内容的时候存了 16 -1 项。后面的内容都是引用它。
  • #1::常量池项,通常以 # 开头,表示一个常量池中的单个项。
  • access flags:访问标志,描述类或接口的访问级别和特性。
  • this_class:当前类的索引,指示当前类在常量池中的位置。
  • super class:父类的索引,指示父类在常量池中的位置。
  • interface_count:接口数量,表示该类实现的接口数量。
  • interfaces:接口列表,描述该类实现的接口在常量池中的位置。
  • fields_count:字段数量,表示该类中声明的字段数量。
  • methods_count:方法数量,表示该类中声明的方法数量。
  • method_info:方法信息,描述方法的访问标志、方法名、参数列表等。
  • attribute_count:属性数量,表示该类的属性数量。

每个部分描述了 Java 字节码文件的不同方面,从类的声明到方法的定义,以及与常量池等相关的信息。这些信息在 Java 虚拟机中被解析和使用,以正确地加载和执行 Java 类。

Java 的汇编指令有 200 多条。

好文分享:class类文件结构

类加载 - 初始化

加载过程

加载、链接和初始化是 Java 程序运行时的三个主要阶段。

  1. Loading – 加载

  2. Linking – 链接

    • Verification – 验证
    • Preparation – 准备
    • Resolution – 解析
  3. Initializing – 初始化

具体来说:

  1. Loading(加载): 这是类加载过程的第一个阶段。在这个阶段,Java 虚拟机(JVM)会从类的外部源加载类的二进制数据,通常是从磁盘文件中加载,但也可以是网络、内存等。

    加载器将类的二进制数据(class文件)从外部源加载到内存中,并将其放置在运行时数据区的方法区内。

  2. Linking(链接): 这是加载过程的第二个阶段,它将类的二进制数据链接到 JVM 的运行时状态。

    链接过程分为以下三个步骤:

    • Verification(验证):确保类的二进制数据符合 JVM 规范,不违反类加载的安全性要求。这个步骤确保类的结构正确,不会引发运行时错误。
    • Preparation(准备):为类的静态变量分配内存空间,并设置默认的初始值(通常为零值)。这个步骤为类变量分配内存并初始化,但不会为实例变量分配内存。(把 class 文件的静态变量赋默认值,比如 int 类型的默认值为 0)
    • Resolution(解析):将(class文件里面的常量池里面用到的)符号引用转换为直接引用,解析动作可以在运行时再完成,也可以在编译时静态完成。这个步骤处理类、接口、方法和字段的符号引用,将其解析为实际的引用。
  3. Initializing(初始化): 这是链接过程的最后一个阶段,也是类加载的最终阶段。在这个阶段,类的静态初始化器会被执行,初始化静态字段和执行静态块。这个阶段是在类被首次使用时触发的,例如创建类的实例、访问类的静态字段等。(调用类初始化代码 ,给静态成员变量赋初始值)

总结来说,类加载过程涉及到从外部源加载类的二进制数据,然后将其链接到 JVM 的运行时状态,最终进行初始化。链接阶段包括验证、准备和解析,而初始化阶段则会执行类的静态初始化器。这个过程确保类在 Java 虚拟机中正确加载和使用。

加载之后会发生什么?

任何一个 class 被加载到内存之后,会生成了两块内容。

  • 第一块内容是这个二进制的,这块东西确实被落到内存,放到了内存的一块区域,可以原封不动的扔进去。
  • 第二个,它生成了一个 class 类的对象,通过以后其他的那些我们自己写的对象去访问这个对象,通过这个对象去访问 class 类的文件,所以生成一个 class 的对象,这个 class 对象是指向了这块内容。

在 Java 虚拟机中,类的元数据,包括类的结构信息、字段、方法、父类、接口等,都会被加载到方法区(元空间Metaspace,替代了永久代PermGen)中。Class 对象本身也是类的元数据之一,因此 Class 对象也存放在方法区中。 Class 对象在内存中的位置可以看作是类的描述符,用来操作该类的字节码以及其他相关的元数据。

可以看看下面这两篇文章:

  • 方法区与Metaspace
  • Java8取消permgen,使用mataspace有什么好处,内存结构有什么本质的变化?

类加载器

JVM 它本身有一个类加载器的层次,这个类加载器就是一个普通的 class,JVM 有一个类加载器的层次,分别来加载不同的 class。或者说,JVM 里面所有的 class 都是被类加载器给加载到内存的,那么这个类加载器简单说我们可以把它叫做 ClassLoader。

JVM基础入门_第2张图片

注意:从下往上,是委托给父加载器,不是继承关系,是语法上的一种关系。

在 Java 虚拟机中,类加载器按照层次结构进行组织,分为三个主要层次:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader,也称为系统类加载器)。这些类加载器形成了类加载器的双亲委派模型。

  1. 启动类加载器(Bootstrap ClassLoader):这是 Java 虚拟机的一部分,用于加载 Java 核心库(例如 java.lang 包中的类)。它是所有其他类加载器的父加载器,但它本身不是一个普通的 Java 类,因此在源码中并没有对应的类。它位于虚拟机内部,通常用本地代码实现。它是最顶层的类加载器,负责加载 Java 核心类库。
  2. 扩展类加载器(Extension ClassLoader): 扩展类加载器用于加载 Java 虚拟机的扩展类库,位于 jre/lib/ext 目录中的类。它的父加载器是启动类加载器。
  3. 应用程序类加载器(Application ClassLoader):这个加载器也称为系统类加载器,它负责加载应用程序的类,包括用户自定义的类和第三方库中的类。它的父加载器是扩展类加载器。

启动类加载器、扩展类加载器和应用程序类加载器构成了类加载器的层次结构,通过双亲委派模型来保证类的加载的一致性和安全性。在加载一个类时,首先会尝试由父加载器加载,只有在父加载器无法加载时,子加载器才会尝试加载。这个模型可以防止类的重复加载,同时保证了类的隔离性。

在启动类加载器加载的类中,有一部分是虚拟机内部的类,比如 java.lang.Objectjava.lang.String 等。这些类并不是普通的 Java 类,因此没有对应的源码。而在 Java 虚拟机的源码中,通常会对这些类的加载过程进行描述,但实际上它们是由虚拟机的实现提供的。

最顶层加载器 Bootstrap 会返回一个空值。

public class T004_ParentAndChild {
    public static void main(String[] args) {
        System.out.println(T004_ParentAndChild.class.getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader()); // App
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent()); // Extension
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent()); // Bootstrap,返回 null
        // System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent()); // 解开注释就会报空指针异常
    }
}

输出结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@4554617c
null

双亲委派

  1. 父加载器
    • 父加载器不是【类加载器的加载器】!!!也不是【类加载器的父类加载器】
  2. 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程
  3. 思考:为什么要搞双亲委派?
    • java.lang.String 类由自定义类加载器加载行不行? – 不行,因为不安全

先是自底向上检查是否已经加载,然后再回过头来找 class 并加载,查看是否加载成功,一直到底都没加载成功就会抛出一个 class 找不到异常。

这个缓存是在哪缓存?

可以简单的认为是它自己内部维护的一个容器,一个 list 或者一个数组加载的东西都扔在里面。每个加载器都有自己的缓存。

为什么类加载器要使用双亲委派?(重要)

主要是为了安全。次要原因是资源浪费问题,避免重新加载问题(防止类重复加载)。

类加载器范围

来自 Launcher 源码

  1. 启动类加载器 Bootstrap ClassLoader
    • 加载路径:sun.boot.class.path
    • 范围:它负责加载 Java 核心类库,这些类库包括 Java API 中的基础类,如 java.lang 包下的类等。
    • 作用:它是 Java 类加载器中最顶层的类加载器,负责加载虚拟机自身需要的类,以及在运行期间会被系统使用的类。由于 Bootstrap ClassLoader 是用本地代码来实现的,所以在 Java 中无法直接获取到该类加载器的引用。
  2. 扩展类加载器 ExtensionClassLoader
    • 加载路径:java.ext.dirs
    • 范围:它负责加载 Java 的扩展库,这些库位于 JRE 的 lib/ext 目录或者由系统变量 java.ext.dirs 指定的路径。
    • 作用:它负责加载一些 Java 标准扩展库以及一些自定义的扩展库,这些库通常是一些可选的功能。扩展类加载器是由 sun.misc.Launcher$ExtClassLoader 实现的。
  3. 系统类加载器 AppClassLoader
    • 加载路径:java.class.path
    • 范围:也被称为系统类加载器,它负责加载用户类路径(Classpath)上指定的类。
    • 作用:它负责加载应用程序中的类,包括开发者自己编写的类以及引用的第三方类库。应用类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。

代码查看:

public class T003_ClassLoaderScope {
    public static void main(String[] args) {
        System.out.println("根目录下加载");
        String pathBoot = System.getProperty("sun.boot.class.path");
        System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        System.out.println("ext 下加载");
        String pathExt = System.getProperty("java.ext.dirs");
        System.out.println(pathExt.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        System.out.println("App 下加载");
        String pathApp = System.getProperty("java.class.path");
        System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
    }
}

输出结果:

根目录下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_321\jre\classes
--------------------
ext 下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
--------------------
App 下加载
C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar
D:\笔记\学习资料\马士兵\JVM视频源码\out\production\JVM				// 项目路径
D:\software\idea2022\IntelliJ IDEA 2022.1.3\lib\idea_rt.jar

自定义类加载器

当你想加载某个类的时候,可以调用 loadClass 方法

public class T005_LoadClassByHand {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = T005_LoadClassByHand.class.getClassLoader().loadClass("com.mashibing.jvm.c2_classloader.T002_ClassLoaderLevel");
        System.out.println(clazz.getName());

        //利用类加载器加载资源,参考坦克图片的加载
        //T005_LoadClassByHand.class.getClassLoader().getResourceAsStream("");
    }
}
loadClass 源码解析

loadClass 方法是在双亲委派模型下实现的。

findInCache -> parent.loadClass -> findClass()

private final ClassLoader parent; // final 关键字修饰

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已经加载
            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
                    // 从非空父类装入器
                }
              
							 // 如果父加载器也找不到,则尝试自己加载
                if (c == null) {
                    // 如果仍未找到,则按顺序调用 findClass
                    // to find the class. 要找到这个类
                    long t1 = System.nanoTime();
                    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;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException { // findClass被protected修饰,保护起来
        throw new ClassNotFoundException(name);
    }

主要步骤如下:

  1. 首先,方法会尝试从已加载的类中查找目标类是否已经加载,如果已经加载,则直接返回该类。
  2. 如果目标类未加载,它会尝试委派给父类加载器来加载。这是双亲委派模型的核心思想之一。父类加载器会重复这个过程,直到到达启动类加载器(Bootstrap ClassLoader)为止。
  3. 如果父类加载器也无法加载目标类,则 loadClass 方法会调用 findClass 方法,尝试自己加载目标类。这是子类加载器在加载自己类的最后一步
  4. 最后,如果 resolve 参数为 true,则会调用 resolveClass 方法,用于解析加载的类,确保类的完整性和正确性。

什么时候需要自己去加载?

加载进去就生成 class 对象吗?

不是,要经过 初始化 才生成 class 对象。

自定义一个类加载器需要做什么?

只需要做一件事,就是定义自己的 findClass 就可以了。

具体来说:

  • 继承 ClassLoader

  • 重写模板方法 findClass

    • 调用 defineClass(byte[] -> Class clazz)
  • 加密:可自定义类加载器加载自加密的 class

    • 防止反编译
    • 防止篡改

代码如下:

首先继承 ClassLoader 这个类

// 自定义类加载器继承自ClassLoader
public class T006_MSBClassLoader extends ClassLoader {

    // 重写findClass方法,用于加载类字节码
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 根据类名构建文件路径
        File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
        try {
            // 读取字节码文件
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            // 读取文件内容并写入字节数组输出流
            while ((b=fis.read()) !=0) {
                baos.write(b);
            }

            // 将字节数组转换为字节数组
            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close(); // 关闭流

            // 使用defineClass方法定义并返回类
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 若加载失败,则调用父类的findClass方法
        return super.findClass(name); // throws ClassNotFoundException
    }

    // 主函数
    public static void main(String[] args) throws Exception {
        // 创建自定义类加载器实例
        ClassLoader l = new T006_MSBClassLoader();
        
        // 通过自定义类加载器加载类
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");

        // 输出两次加载的类是否相同
        System.out.println(clazz == clazz1);

        // 创建加载的类的实例并调用方法
        Hello h = (Hello) clazz.newInstance();
        h.m();

        // 输出类加载器信息
        System.out.println(l.getClass().getClassLoader()); // 输出自定义类加载器的类加载器
        System.out.println(l.getParent()); // 输出自定义类加载器的父类加载器

        System.out.println(getSystemClassLoader()); // 输出系统类加载器
    }
}

public class Hello {
    public void m() {
        System.out.println("Hello JVM!");
    }
}

这段代码实现了一个自定义的类加载器 T006_MSBClassLoader,该类加载器继承自 ClassLoader,重写了 findClass 方法来加载类的字节码。在主函数中,使用这个自定义的类加载器加载类,并输出加载的类是否相同,然后创建该类的实例并调用方法。最后,输出了自定义类加载器的类加载器和父类加载器信息,以及系统类加载器的信息。这种自定义类加载器的方式允许你从非标准的位置加载类文件,并且可以通过不同的类加载器实现类的隔离。


defineClass 方法是 ClassLoader 类中的一个重要方法,用于将字节数组转换为一个 Class 对象。该方法的作用是将一个【字节数组中的类字节码】转换为一个【Java 类的实例】。当一个类加载器调用 defineClass 方法时,它会将字节数组中的类字节码转换为一个 Class 对象,并返回该对象。(不会验证字节码的正确性)

输出结果

true
Hello JVM!
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
加密

大家都知道 java 的代码 class 文件很容易就被反编译了。

但是我要是定义自己的格式,我不想让别人反编译,这时候怎么办?

你可以通过自定义的 class loader 来进行。然后在写逻辑的时候加一个加密操作。

编译器

三大模式:

  • 解释器(bytecode intepreter) – 解释模式
  • JIT(Just In-Time compiler) – 编译模式
  • 混合模式
    • 混合使用解释器 + 热点代码编译(编译成本地代码,就不用在解释器里面解释来执行了,效率提升)
    • 起始阶段采用解释执行
    • 热点代码检测
      • 多次被调用的方法(方法计数器:监测方法执行频率)
      • 多次被调用的循环(循环计数器:检测循环执行频率)
      • 进行编译

为什么不干脆直接编译成本地代码,那执行效率不更高吗?

有两个原因。

  1. 第一,Java 的解释器,现在它的效率也已经非常高了,在一些简单代码的执行上,它并不输于你编译成本地代码。
  2. 第二,如果你一个执行的文件特别多,各种各样的类库的时候,好几十个 class,你上来二话不说先在内存里编译一遍,这个启动过程会长得吓人。所以它现在默认的模式是混合模式。

可以用指明参数的方式指定用什么模式

  1. -Xmixed 默认为混合模式

    开始解释执行,启动速度较快,对热点代码实行检测和编译

  2. -Xint 使用解释模式,启动很快,执行稍慢

  3. -Xcomp 使用纯编译模式,执行很快,启动很慢(要编译的类少的时候,启动也会很快)

懒加载

lazyloading

  • 严格讲应该叫 lazylnitializing
  • JVM 规范并没有规定何时加载
  • 但是严格规定了什么时候必须初始化,五种情况
    1. 使用 new,getstatic,putstatic,invokestatic 指令时,访问 final 变量除外
    2. java.lang.reflect 对类进行反射调用时
    3. 初始化子类的时候,父类必须首先初始化
    4. 虚拟机启动时,被执行的主类必须初始化
    5. 动态语言支持 java.lang.invoke.MethodHandle 解析的结果为 REF_getstatic, REF_putstatic, REF_invokestatic 的方法句柄时,该类必须初始化

拓展知识

双亲委派如何打破?

parent 是如何指定的,打破双亲委派

  1. super(parent) 指定
  2. 双亲委派的打破
    1. 如何打破:重写 loadClass() 方法
    2. 何时打破过?
      1. JDK1.2 之前,自定义 ClassLoader 都必须重写 loadClass()
      2. ThreadContextClassLoader 可以实现基础类调用实现类代码,通过 thread.setContextClassLoader 指定
      3. 热启动,热部署
        • osgi tomcat 都有自己的模块指定 classloader(可以加载同一类库的不同版本)
类加载器涉及到了哪些设计模式?

在 Java 虚拟机 (JVM) 中的类加载器 (ClassLoader) 实现中,通常使用了以下两种设计模式:

  1. 委托模式 (Delegation Pattern):ClassLoader 的实现通常使用了委托模式来处理类加载请求。委托模式指的是当一个对象收到请求时,它将请求委托给其他对象来处理。在类加载器的情况下,当一个类加载器接收到加载类的请求时,它首先会将请求委托给父类加载器进行处理。如果父类加载器无法加载该类,子类加载器才会尝试加载类。这种委托模式的设计方式可以实现类加载器的层次结构,从而实现类加载器的隔离和类加载的委派。
  2. 单一职责模式 (Single Responsibility Pattern):ClassLoader 的主要责任是加载类文件并定义对应的类。ClassLoader 的设计符合单一职责模式,即一个类应该只负责一项职责。ClassLoader 将类加载的职责封装在一个独立的类中,从而使得类加载的逻辑与其他功能解耦,提高了代码的可维护性和可扩展性。

除了委托模式和单一职责模式,ClassLoader 的实现可能还涉及其他设计模式,具体取决于实际的实现细节和需求。例如,一些类加载器的缓存机制可能使用了享元模式 (Flyweight Pattern) 来提高性能和资源利用效率。

在 Java 虚拟机 (JVM) 中的类加载器 (ClassLoader) 实现中,通常使用了以下两种设计模式:

  1. 委托模式 (Delegation Pattern):ClassLoader 的实现通常使用了委托模式来处理类加载请求。委托模式指的是当一个对象收到请求时,它将请求委托给其他对象来处理。在类加载器的情况下,当一个类加载器接收到加载类的请求时,它首先会将请求委托给父类加载器进行处理。如果父类加载器无法加载该类,子类加载器才会尝试加载类。这种委托模式的设计方式可以实现类加载器的层次结构,从而实现类加载器的隔离和类加载的委派。
  2. 单一职责模式 (Single Responsibility Pattern):ClassLoader 的主要责任是加载类文件并定义对应的类。ClassLoader 的设计符合单一职责模式,即一个类应该只负责一项职责。ClassLoader 将类加载的职责封装在一个独立的类中,从而使得类加载的逻辑与其他功能解耦,提高了代码的可维护性和可扩展性。

除了委托模式和单一职责模式,ClassLoader 的实现可能还涉及其他设计模式,具体取决于实际的实现细节和需求。例如,一些类加载器的缓存机制可能使用了享元模式 (Flyweight Pattern) 来提高性能和资源利用效率。

你可能感兴趣的:(Java,jvm,java)