JVM类加载过程

news2024/11/18 15:46:18

文章目录

  • 1、加载
  • 2、链接
    • 2.1 验证
    • 2.2 准备
    • 2.3 解析
  • 3、初始化
    • 3.1 类初始化练习
    • 3.2 懒汉式单例练习
  • 4、类加载器
    • 4.1 启动类加载器
    • 4.2 扩展类加载器
    • 4.3 双亲委派模式
    • 4.4 线程上下文类加载器
    • 4.4 自定义类加载器
  • 5、运行时优化
    • 5.1 即时编译
      • 逃逸分析
      • 方法内联(Inlining)
      • 字段优化
      • 反射优化

类加载Class类型的文件主要三步: 加载->链接->初始化。链接过程又可以分为三步: 验证->准备->解析

1、加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用

  • _super 即父类

  • _fields 即成员变量

  • _methods 即方法

  • _constants 即常量池

  • _class_loader 即类加载器,是哪个类加载了它

  • _vtable 虚方法表,构造方法入口地址

  • _itable 接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看

image-20220603144453638

2、链接

主要分为三个阶段:验证、准备、解析

2.1 验证

验证类是否符合 JVM规范,安全性检查,四种验证:文件格式验证元数据验证字节码验证符号引用验证

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行会报错

E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
        at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

2.2 准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

这里我们要注意类对象的存储位置,可以看上面的那张图,是存储在堆中的!jdk6之前是跟着instanceKlass后面的

针对上面第二点所说的分配空间和赋值是两个步骤,这里准备了一个栗子来证明:

这里我们要回忆一下之前的知识点,我们的类在加载时会将所有的静态属性和局部变量放到一个构造方法<cinit>()V里面,并且在类的构造方法之前执行

public class LoadB {
    static int a;
}

对应字节码:

{
  static int a; //声明
    descriptor: I
    flags: ACC_STATIC

  public com.hh.classLoader.byteCode.LoadB();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hh/classLoader/byteCode/LoadB;
}

可以看到在字节码里,并没有赋值的操作()

接着我们修改一下代码:

public class LoadB {
    static int a;
    static int b = 10;
}

对应字节码:

{
  static int a;
    descriptor: I
    flags: ACC_STATIC

  static int b;
    descriptor: I
    flags: ACC_STATIC

  public com.hh.classLoader.byteCode.LoadB();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1    // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hh/classLoader/byteCode/LoadB;

  static {};  //可以这里调用了静态方法的构造方法
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: return
      LineNumberTable:
        line 10: 0
}

再看一下final修饰的静态变量

public class LoadB {
    static int a;
    static int b = 10;
    static final int c = 20;
}

对应字节码:

{
  static int a;
    descriptor: I
    flags: ACC_STATIC

  static int b;
    descriptor: I
    flags: ACC_STATIC

  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

  public com.hh.classLoader.byteCode.LoadB();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hh/classLoader/byteCode/LoadB;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10  // 10的赋值动作
         2: putstatic     #2                  // Field b:I
         5: return
      LineNumberTable:
        line 10: 0
}

可以看到有10的赋值动作,但是却找不到20的赋值动作,可以看出final修饰的静态方法并不是在调用<cinit>()V构造方法时执行的,也就是并不是在初始化阶段完成的的,而是在准备阶段就完成了

2.3 解析

将常量池中的符号引用解析为直接引用

首先我们要知道类的加载都是惰性加载的,只有在要使用时才会进行加载

我们举一个例子来看一下解析的过程

我们首先应该知道,当类加载器加载类C时,只会对类C进行加载,并不会对类C进行链接和解析,当然类D也不会被加载、链接、初始化

之后我们会new C(),因为new会主动触发类C和类D的加载、链接、初始化,所以对比着来看解析的过程

/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException,
            IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        // loadClass 方法不会导致类的解析和初始化
        Class<?> c = classloader.loadClass("com.hh.classLoader.link.C");
        // new C();
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {
}

我们将项目启动并使用HSDB工具查看此时JVM中存在的类(在Java目录下执行)

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

image-20220603162937571

我们可以看到类C已经被加载进了JVM中,但是此时类D并没有被加载

image-20220603163029857

接下来我们进类C中看下

image-20220603163214506

可以看到类C左边的 JVM_CONSTANT_UnresolvedClass,表示这是一个未被解析的类

image-20220603163418741

接下来看类解析的情况

/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException,
            IOException {
//        ClassLoader classloader = Load2.class.getClassLoader();
//        // loadClass 方法不会导致类的解析和初始化
//        Class<?> c = classloader.loadClass("com.hh.classLoader.link.C");
         new C();
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {
}

重新启动项目并用HSDB连接

可以看到此时其实类C和类D都已经加载了

image-20220603163723519

我们进常量池里看一下,已经加载好了

image-20220603163858049

所以总结一下:

解析的过程就是将常量池中的符号引用解析为直接引用

3、初始化

接下来是类加载的最后一个阶段

<cinit>()V 方法

初始化即调用<cinit>()V 方法 ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

类的初始化的懒惰的,以下情况会初始化:

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化 (在类链接的准备阶段就完成)
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

这里可能有点难理解,直接来一个例子:

每次只留一个打印,把其他的都注释掉,对比着来看

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
        System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
        System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B");
        // 5. 不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.jvm.t3.B", false, c2);
        
        // 1. 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("cn.itcast.jvm.t3.B");
    }
}
class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

3.1 类初始化练习

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}
class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

前两个打印不会引起类的初始化,因为基本类型+String的赋值是在链接的准备阶段完成的

而第三个打印会引起初始化,因为Integer是类,会自动装箱,这个操作会在初始化阶段完成

字节码角度验证一下:

{
  public com.hh.classLoader.init.Load4();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hh/classLoader/init/Load4;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #5                  // String hello
        13: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: getstatic     #7                  // Field com/hh/classLoader/init/E.c:Ljava/lang/Integer;
        22: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        25: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 16
        line 13: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
}

3.2 懒汉式单例练习

public class Load9 {
    public static void main(String[] args) {
        Singleton.test();
        Singleton.getInstance();
    }
}
class Singleton{
    private Singleton(){}
    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {System.out.println("LazyHolder init...");}
    }
    public static Singleton getInstance(){
        return LazyHolder.SINGLETON;
    }
    public static void test(){
        System.out.println("test...");
    }
}

只执行test时不会引起Singleton的初始化

当执行getInstance会引起初始化

4、类加载器

以 JDK 8 为例:

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

4.1 启动类加载器

我们自己的类一般是app类加载器进行加载,但是我们也可以通过一些参数来指定使用那个类加载器进行加载

用 Bootstrap 类加载器加载类:

public class F {
    static{
        System.out.println("bootstrap F init...");
    }
}

在cmd窗口进入类目录下并使用命令执行:

java -Xbootclasspath/a:.com.hh.classLoader.init.Load5_1
bootstrap F init
null
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("com.hh.classLoader.init.F");
        System.out.println(clazz.getClassLoader());
    }
}
  • -Xbootclasspath 表示设置 bootclasspath

  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

  • 可以用这个办法替换核心类

    • java -Xbootclasspath:<new bootclasspath>
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

4.2 扩展类加载器

package cn.itcast.jvm.t3.load;
public class G {
    static {
    	System.out.println("classpath G init");
    }
}

执行

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

package cn.itcast.jvm.t3.load;
public class G {
    static {
    	System.out.println("ext G init");
    }
}

打个 jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Load5_2
输出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

4.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

来看一段loadClass的源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派
                    BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

可以通过debug的方式来执行体会一下加载的过程

4.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
        = new CopyOnWriteArrayList<>();
    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>
                    () {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers =
                        ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        // 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

于是要显示的调用Classd的forName方法使用一个能加载驱动的加载器加载驱动

类加载还有一个原则:全盘负责

默认是使用本来的加载器加载依赖类的

由于JDBC在核心类库中,它由启动类加载器加载,由于驱动是在他的类初始化方法中加载的

所以驱动是DriverManager的依赖

默认是由启动类加载器加载,但找不到,不可能加载到驱动

于是要显示的调用Classd的forName方法使用一个能加载驱动的加载器加载驱动

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image-20220603175429346

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中

4.4 自定义类加载器

问问自己,什么时候需要自定义类加载器

  • 1)想加载非 classpath 随意路径中的类文件
  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  • 继承 ClassLoader 父类
  • 要遵从双亲委派机制,重写 findClass 方法
    • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  • 读取类文件的字节码 4. 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法

如何确定一个类相同?

报名类名相同并且其类加载器也要相同

5、运行时优化

5.1 即时编译

逃逸分析

分层编译(TieredCompilation)

先来个例子

public class JIT1 {
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

结果:

0	46500
1	55000
2	47700
3	52300
4	52500
5	49200
6	51800
7	46400
8	54800
9	66000
10	69000
11	45000
12	54900
13	47400
14	85500
15	56400
16	49200
17	54600
18	56300
19	102100
20	78700
21	50600
22	272100
23	48800
24	94700
25	72900
26	61600
27	378900
28	102400
29	60300
30	63000
31	241300
32	59400
33	169200
34	49000
35	57900
36	66900
37	55700
38	70100
39	68900
40	60700
41	53800
42	51200
43	48200
44	61500
45	57500
46	83300
47	58700
48	53400
49	56500
50	49300
51	51200
52	52400
53	61200
54	53800
55	47300
56	84100
57	60100
58	56700
59	49500
60	56700
61	61100
62	88300
63	65000
64	66500
65	63400
66	26400
67	12800
68	12400
69	12300
70	31500
71	11300
72	39500
73	13400
74	15500
75	21200
76	14500
77	15200
78	70200
79	20900
80	11000
81	14000
82	17800
83	12100
84	24000
85	13700
86	12800
87	8800
88	12600
89	9400
90	8400
91	14200
92	12200
93	15400
94	19400
95	12300
96	14900
97	12800
98	17300
99	15800
100	12300
101	10800
102	35700
103	14500
104	10300
105	11400
106	18700
107	10900
108	10800
109	12400
110	9800
111	10800
112	14200
113	9100
114	23700
115	14400
116	11200
117	10800
118	11000
119	11400
120	11100
121	12400
122	12500
123	10100
124	10200
125	14500
126	10400
127	15200
128	11300
129	11700
130	12700
131	14400
132	13100
133	11000
134	8700
135	14700
136	10900
137	18200
138	10900
139	11700
140	12800
141	12400
142	53200
143	195900
144	59800
145	600
146	800
147	600
148	700
149	700
150	700
151	600
152	600
153	900
154	600
155	600
156	600
157	600
158	800
159	600
160	800
161	500
162	500
163	500
164	700
165	500
166	600
167	600
168	600
169	700
170	400
171	600
172	500
173	500
174	900
175	700
176	400
177	700
178	700
179	600
180	500
181	1000
182	600
183	600
184	500
185	600
186	1200
187	700
188	600
189	500
190	400
191	400
192	700
193	600
194	600
195	600
196	500
197	600
198	600
199	600

可以看到在145次运行的时候,加载类的速度一下子就变快的

原因是什么呢?

JVM会 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),优化之

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:- DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

 -XX:-DoEscapeAnalysis

参考资料:Java HotSpot Virtual Machine Performance Enhancements (oracle.com)

小结:

即时编译器就是将热点代码的机器码缓存起来,下次遇到相同的代码直接执行

c1比解释器速度快大概五倍

c2比解释器快大概10到100倍

JDK 9 引入了一种新的编译模式 AOT,它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。

逃逸分析的作用:通过逃逸分析后的对象,可将这些对象直接在栈上进行分配,而非堆上。极大的降低了GC次数,从而提升了程序整体的执行效率。s

方法内联(Inlining)

private static int square(final int i) {
    return i * i;
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

System.out.println(81);

实验:

public class JIT2 {
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining 信息
    // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
    // -XX:+PrintCompilation 打印编译信息
    public static void main(String[] args) {
            int x = 0;
            for (int i = 0; i < 500; i++) {
                long start = System.nanoTime();
                for (int j = 0; j < 1000; j++) {
                	x = square(9);
                }
                long end = System.nanoTime();
                System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
            }
    }
    private static int square(final int i) {
    return i * i;
    }
}

字段优化

反射优化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

StressAppTest的简介

StressAppTest的全称是Stressful Application Test (stressapptest) 的简称http://code.google.com/p/stressapptest/ 这里我们可以将其简化为SAT。 SAT试图让来自处理器和I/O到内存的数据尽量随机化,以创造出模拟现实的环境来测试现在的硬件设备是否稳定。 SAT的大概作用如…

【Mysql 学习笔记】

Mysql 笔记记录 MySQL学习笔记一、 DDL1. DDL 查询和创建数据库2. DDL 修改、删除、使用数据库3. DDL 查询数据表4. DDL 创建数据表5. DDL 修改数据表6. DDL 删除数据表 二、DML MySQL学习笔记 一、 DDL 1. DDL 查询和创建数据库 #查询所有数据库 SHOW DATABASES; #查询某个数…

PMP项管2023年5月的备考准备攻略!现在看还来得及!

2023年共有4次PMP考试&#xff0c;分别是3月、5月、8月、11月&#xff0c;由于3月份考试不开放新报名&#xff0c;所以第一次备考PMP的同学可以选择参加5月份考试。那么&#xff0c;现在备考5月份PMP考试还来得及吗&#xff1f; 现在开始备考5月PMP考试&#xff0c;时间是非常…

蓝牙技术|消息称三星正研发智能戒指Galaxy Ring

根据韩媒 MT 报道&#xff0c;三星内部正在开发继 Galaxy Watch、Galaxy Fit 之后的另一款健康追踪设备 Galaxy Ring。 报道称这款智能戒指配备 PPG&#xff08;光电容积脉搏波&#xff09;传感器和 ECG&#xff08;心电图&#xff09;传感器&#xff0c;可以准确追踪佩戴者的…

云智慧助力MLOps加速落地

背景 随着数字化和计算能力的发展&#xff0c;机器学习&#xff08;Machine Learning&#xff09;技术在提高企业生产力方面所涌现的潜力越来越被大家所重视&#xff0c;然而很多机器学习的模型及应用在实际的生产环境并未达到预期&#xff0c;大量的ML项目被证明是失败的。从…

云计算中的网络安全技术及其应用

云计算已经成为当今企业信息化的主要选择之一。它提供了可靠的数据存储和处理能力&#xff0c;同时降低了企业的IT成本。然而&#xff0c;云计算的安全问题也随之而来。网络安全技术的应用对于保护云计算的安全至关重要。本文将探讨云计算中的网络安全技术及其应用&#xff0c;…

Golang每日一练(leetDay0041) 股票买卖4题

目录 121. 买卖股票的最佳时机 &#x1f31f; 122. 买卖股票的最佳时机 II &#x1f31f;&#x1f31f; 123. 买卖股票的最佳时机 III &#x1f31f;&#x1f31f;&#x1f31f; 188. 买卖股票的最佳时机 IV &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每…

2023第六届世界燕窝及天然滋补品博览会

2023上海燕窝展|上海燕博会|虫草节、鱼胶、灵芝、海参、滋补品展|滋补大会 摘要&#xff1a;燕博会、上海燕窝展、上海燕博会、2023上海燕博会、2023上海燕窝展、2023中国燕窝展、2023燕窝展&#xff0c;2023原装进口燕窝展&#xff0c;2023干制燕窝展,2023即食燕窝展,2023燕窝…

前端开发中有哪些常用的数组操作方法?

javascript数组 简介 JavaScript 数组用于在单一变量中存储多个值。 JavaScript数组是无类型的&#xff0c;数组元素可以是任意类型&#xff0c;并且同一个数组中元素类型也可以不同。 实例 var cars ["Saab", "Volvo", "BMW"];什么是数组&…

探索【Stable-Diffusion WEBUI】的插件:画布扩绘(Outpaint)

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;局部重绘&#xff08;Inpaint&#xff09;&#xff08;二&#xff09;画布扩绘&#xff08;Outpaint&#xff09;&#xff08;2.1&#xff09;图片画布扩大&#xff08;插件&#xff1a;OpenOutpaint&#x…

回炉重造九---DNS服务器

1、DNS服务器的相关概念和技术 1.1 DNS服务器的类型 主DNS服务器从DNS服务器缓存DNS服务器&#xff08;forward DNS服务器{转发器}&#xff09; 1.1.1 主DNS服务器的作用 管理和维护所负责解析的域内解析库的服务器1.1.2 从DNS服务器的作用 从主服务器或从服务器“复制”解…

检测并打印C++编译器支持的feature(附Visual Studio 2022和gcc-12测试、对比结果)

C标准快速迭代&#xff0c;不同的系统平台和编译器对C各种新功能的支持不同&#xff0c;通过这个程序可以测试所用编译器对各个版本C的支持情况。另一方面&#xff0c;可以在代码中通过这些宏针对不同版本编写不同的代码分支。 源码下面附上Visual Studio 2022的测试结果&#…

32道子网划分习题详细解析

目录 1 子网划分概念&#xff1a; 2 划分方法&#xff1a; 子网划分方法&#xff1a;段&#xff0c;块&#xff0c;数的计算三步。 段就是确定ip地址段中既有网络地址&#xff0c;又有主机地址的那一段是四段中的那一段&#xff1f; 块就确定上一步中确定的那一段中的主机…

【C语言】21-结构体

本文目录 • 一、什么是结构体 • 二、结构体的定义 • 三、结构体变量的定义 • 四、结构体的注意点 • 五、结构体的初始化 • 六、结构体的使用 • 七、结构体数组 • 八、结构体作为函数参数 • 九、指向结构体的指针 说明&#xff1a;这个C语言专题&#xff0c;是学习iOS开…

算法设计与智能计算 || 专题七: 主成分分析的统计学视角

主成分分析的统计学视角 文章目录 主成分分析的统计学视角PCA 的统计学视角1. 寻找第一个主成分2. 获取第二个主成分3. 非零均值随机变量的主元4. 零均值随机变量的样本主元5. PCA 降维案例 主成分分析是将高维空间中的数据集拟合成一个低维子空间的方法&#xff0c;到目前为止…

搞定常见八大排序

文章目录 注意事项插入排序插入排序希尔排序 分组预排序选择排序堆排序直接选择排序(最拉胯的排序) 交换排序冒泡排序快速排序1. hoare版本如何解决快排缺陷&#xff1f;2.挖坑法版本3.双指针法版本&#xff08;建议&#xff09;快排算法优化实现非递归快排 归并排序归并排序 非…

Rainbond 结合 Jpom 实现云原生 本地一体化项目管理

Jpom 是一个简而轻的低侵入式在线构建、自动部署、日常运维、项目运维监控软件。提供了&#xff1a; 节点管理&#xff1a;集群节点&#xff0c;统一管理多节点的项目&#xff0c;实现快速一键分发项目文件项目管理&#xff1a;创建、启动、停止、实时查看项目控制台日志&…

CentOS系统设置中文输入法,并切换输入法

1.点击Application—>System Tools—>Settings&#xff0c;选择Region&Language 2.在Input Sources中&#xff0c;选择左下角的“”&#xff0c;找到Chinese(Intelligent Pinyin)&#xff0c;选中后点击右上角的“add”即可 3.选择好后&#xff0c;就可以切换中英…

可口可乐如何管理其全球供应链

目录 &#xff08;一&#xff09;可口可乐供应链管理的核心组成部分 &#xff08;二&#xff09;可口可乐管理全球供应链的挑战 &#xff08;三&#xff09;可口可乐利用技术简化其供应链 转载自供应链星球 可口可乐在200多个国家和地区拥有约225家装瓶合作伙伴&#xff0c;并…

【李沐—AutoGluon背后的技术】

1.资料来源 AutoGluon背后的技术_哔哩哔哩_bilibili 也是一种Automl框架【在尽量不需要人的帮助下&#xff0c;对输入进行特征提取&#xff0c;选取适合的机器学习模型对它进行训练】。大部分基于超参数搜索技术【从数十或者数百个参数中选取一个合适的参数&#xff0c;媲美人…