21讲深入JVM即时编译器JIT,优化Java编译 - 图121讲深⼊JVM即时编译器JIT,优化Java编译

你好,我是刘超。

说到编译,我猜你⼀定会想到 .java⽂件被编译成 .class⽂件的过程,这个编译我们⼀般称为前端编译。Java的编译和运⾏过程⾮常复杂,除了前端编译,还有运⾏时编译。由于机器⽆法直接运⾏Java⽣成的字节码,所以在运⾏时,JIT或解释器会将字节码转换成机器码,这个过程就叫运⾏时编译。
21讲深入JVM即时编译器JIT,优化Java编译 - 图221讲深入JVM即时编译器JIT,优化Java编译 - 图3

类⽂件在运⾏时被进⼀步编译,它们可以变成⾼度优化的机器代码,由于C/C++编译器的所有优化都是在编译期间完成的,运
⾏期间的性能监控仅作为基础的优化措施则⽆法进⾏,例如,调⽤频率预测、分⽀频率预测、裁剪未被选择的分⽀等,⽽
Java在运⾏时的再次编译,就可以进⾏基础的优化措施。因此,JIT编译器可以说是JVM中运⾏时编译最重要的部分之⼀。 然⽽许多Java开发⼈员对JIT编译器的了解并不多,不深挖其⼯作原理,也不深究如何检测应⽤程序的即时编译情况,线上发
⽣问题后很难做到从容应对。今天我们就来学习运⾏时编译如何实现对Java代码的优化。

类编译加载执⾏过程

在这之前,我们先了解下Java从编译到运⾏的整个过程,为后⾯的学习打下基础。请看下图:

类编译

在编写好代码之后,我们需要将 .java⽂件编译成 .class⽂件,才能在虚拟机上正常运⾏代码。⽂件的编译通常是由JDK中⾃带的Javac⼯具完成,⼀个简单的 .java⽂件,我们可以通过javac命令来⽣成 .class⽂件。

下⾯我们通过javap( 第12讲 讲过如何使⽤javap反编译命令⾏)反编译来看看⼀个class⽂件结构中主要包含了哪些信息:
21讲深入JVM即时编译器JIT,优化Java编译 - 图4

21讲深入JVM即时编译器JIT,优化Java编译 - 图5

看似⼀个简单的命令执⾏,前期编译的过程其实是⾮常复杂的,包括词法分析、填充符号表、注解处理、语义分析以及⽣成
class⽂件,这个过程我们不⽤过多关注。只要从上图中知道,编译后的字节码⽂件主要包括常量池和⽅法表集合这两部分就可以了。

常量池主要记录的是类⽂件中出现的字⾯量以及符号引⽤。字⾯常量包括字符串常量(例如String str=“abc”,其中”abc”就是常量),声明为final的属性以及⼀些基本类型(例如,范围在-127-128之间的整型)的属性。符号引⽤包括类和接⼝的全限定名、类引⽤、⽅法引⽤以及成员变量引⽤(例如String str=“abc”,其中str就是成员变量引⽤)等。

⽅法表集合中主要包含⼀些⽅法的字节码、⽅法访问权限(public、protect、prviate等)、⽅法名索引(与常量池中的⽅法引
⽤对应)、描述符索引、JVM执⾏指令以及属性集合等。

类加载

当⼀个类被创建实例或者被其它对象引⽤时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码⽂件加载到内存中。

不同的实现类由不同的类加载器加载,JDK中的本地⽅法类⼀般由根加载器(Bootstrp loader)加载进来,JDK中内部实现的扩展类⼀般由扩展加载器(ExtClassLoader )实现加载,⽽程序中的类⽂件则由系统加载器(AppClassLoader )实现加载。

在类加载后,class类⽂件中的常量池信息以及其它数据会被保存到JVM内存的⽅法区中。

类连接

类在加载进来之后,会进⾏连接、初始化,最后才会被使⽤。在连接过程中,⼜包括验证、准备和解析三个部分。

验证:验证类符合Java规范和JVM规范,在保证符合规范的前提下,避免危害虚拟机安全。

准备:为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,直接赋值为⽤户的定义值。例
如,private final static int value=123,会在准备阶段分配内存,并初始化值为123,⽽如果是 private static int value=123,这个阶段value的值仍然为0。

解析:将符号引⽤转为直接引⽤的过程。我们知道,在编译时,Java类并不知道所引⽤的类的实际地址,因此只能使⽤符号 引⽤来代替。类结构⽂件的常量池中存储了符号引⽤,包括类和接⼝的全限定名、类引⽤、⽅法引⽤以及成员变量引⽤等。如果要使⽤这些类和⽅法,就需要把它们转化为JVM可以直接获取的内存地址或指针,即直接引⽤。

类初始化

类初始化阶段是类加载过程的最后阶段,在这个阶段中,JVM⾸先将执⾏构造器⽅法,编译器会在将 .java ⽂件编译成
.class ⽂件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态⽅法,收集在⼀起成为 () ⽅法。初始化类的静态变量和静态代码块为⽤户⾃定义的值,初始化的顺序和Java源码从上到下的顺序⼀致。例如:

private static int i=1; static{
i=0;
}
public static void main(String [] args){ System.out.println(i);
}
此时运⾏结果为:

0
再来看看以下代码:

static{ i=0;
}
private static int i=1;
public static void main(String [] args){ System.out.println(i);
}

此时运⾏结果为:

1
⼦类初始化时会⾸先调⽤⽗类的 () ⽅法,再执⾏⼦类的() ⽅法,运⾏以下代码:

public class Parent{
public static String parentStr= “parent static string”; static{
System.out.println(“parent static fields”); System.out.println(parentStr);
}
public Parent(){
System.out.println(“parent instance initialization”);
}
}

public class Sub extends Parent{
public static String subStr= “sub static string”; static{
System.out.println(“sub static fields”); System.out.println(subStr);
}

public Sub(){
System.out.println(“sub instance initialization”);
}

public static void main(String[] args){ System.out.println(“sub main”);
new Sub();
}
}
运⾏结果:

parent static fields parent static string sub static fields sub static string sub main
parent instance initialization sub instance initialization

JVM 会保证 () ⽅法的线程安全,保证同⼀时间只有⼀个线程执⾏。

JVM在初始化执⾏代码时,如果实例化⼀个新对象,会调⽤⽅法对实例变量进⾏初始化,并执⾏对应的构造⽅法内的代码。

即时编译

初始化完成后,类在调⽤执⾏过程中,执⾏引擎会把字节码转为机器码,然后在操作系统中才能执⾏。在字节码转换为机器码的过程中,虚拟机中还存在着⼀道编译,那就是即时编译。

最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个⽅法或代码块的运⾏特别频繁的时候, 就会把这些代码认定为“热点代码”。

为了提⾼热点代码的执⾏效率,在运⾏时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进⾏各层次的优化,然后保存到内存中。

即时编译器类型

在HotSpot虚拟机中,内置了两个JIT,分别为C1编译器和C2编译器,这两个编译器的编译过程是不⼀样的。

C1编译器是⼀个简单快速的编译器,主要的关注点在于局部性的优化,适⽤于执⾏时间较短或对启动性能有要求的程序,例如,GUI应⽤对界⾯启动速度就有⼀定要求。

C2编译器是为⻓期运⾏的服务器端应⽤程序做性能调优的编译器,适⽤于执⾏时间较⻓或对峰值性能有要求的程序。根据各
⾃的适配性,这两种即时编译也被称为Client Compiler和Server Compiler。

在Java7之前,需要根据程序的特性来选择对应的JIT,虚拟机默认采⽤解释器和其中⼀个编译器配合⼯作。

Java7引⼊了分层编译,这种⽅式综合了C1的启动性能优势和C2的峰值性能优势,我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。分层编译将JVM的执⾏状态分为了5个层次:

第0层:程序解释执⾏,默认开启性能监控功能(Profiling),如果不开启,可触发第⼆层编译;
第1层:可称为C1编译,将字节码编译为本地代码,进⾏简单、可靠的优化,不开启Profiling;
第2层:也称为C1编译,开启Profiling,仅执⾏带⽅法调⽤次数和循环回边执⾏次数profiling的C1编译; 第3层:也称为C1编译,执⾏所有带Profiling的C1编译;
第4层:可称为C2编译,也是将字节码编译为本地代码,但是会启⽤⼀些编译耗时较⻓的优化,甚⾄会根据性能监控信息进
⾏⼀些不可靠的激进优化。

在Java8中,默认开启分层编译,-client和-server的设置已经是⽆效的了。如果只想开启C2,可以关闭分层编译(-XX:-
TieredCompilation),如果只想⽤C1,可以在打开分层编译的同时,使⽤参数:-XX:TieredStopAtLevel=1。

除了这种默认的混合编译模式,我们还可以使⽤“-Xint”参数强制虚拟机运⾏于只有解释器的编译模式下,这时JIT完全不介⼊
⼯作;我们还可以使⽤参数“-Xcomp”强制虚拟机运⾏于只有JIT的编译模式下。

通过 java -version 命令⾏可以直接查看到当前系统使⽤的编译模式。如下图所示:
21讲深入JVM即时编译器JIT,优化Java编译 - 图6

热点探测

在HotSpot虚拟机中的热点探测是JIT优化的条件,热点探测是基于计数器的热点探测,采⽤这种⽅法的虚拟机会为每个⽅法建
⽴计数器统计⽅法的执⾏次数,如果执⾏次数超过⼀定的阈值就认为它是“热点⽅法” 。

虚拟机为每个⽅法准备了两类计数器:⽅法调⽤计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运⾏参数的前提下,这两个计数器都有⼀个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

⽅法调⽤计数器:⽤于统计⽅法被调⽤的次数,⽅法调⽤计数器的默认阈值在C1模式下是1500次,在C2模式在是10000次, 可通过-XX: CompileThreshold来设定;⽽在分层编译的情况下,-XX: CompileThreshold指定的阈值将失效,此时将会根据当前待编译的⽅法数以及编译线程数来动态调整。当⽅法计数器和回边计数器之和超过⽅法计数器阈值时,就会触发JIT编译
器。

回边计数器:⽤于统计⼀个⽅法中循环体代码执⾏的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back
Edge),该值⽤于计算是否触发C1编译的阈值,在不开启分层编译的情况下,C1默认为13995,C2默认为10700,可通过- XX: OnStackReplacePercentage=N来设置;⽽在分层编译的情况下,-XX: OnStackReplacePercentage指定的阈值同样会失效,此时将根据当前待编译的⽅法数以及编译线程数来动态调整。

建⽴回边计数器的主要⽬的是为了触发OSR(On StackReplacement)编译,即栈上编译。在⼀些循环周期⽐较⻓的代码段中,当循环达到回边计数器阈值时,JVM会认为这段是热点代码,JIT编译器就会将这段代码编译成机器语⾔并缓存,在该循环时间段内,会直接将执⾏代码替换,执⾏缓存的机器语⾔。

编译优化技术

JIT编译运⽤了⼀些经典的编译优化技术来实现代码的优化,即通过⼀些例⾏检查优化,可以智能地编译出运⾏时的最优性能代码。今天我们主要来学习以下两种优化⼿段:

1.⽅法内联

调⽤⼀个⽅法通常要经历压栈和出栈。调⽤⽅法是将程序执⾏顺序转移到存储该⽅法的内存地址,将⽅法的内容执⾏完后,再返回到执⾏该⽅法前的位置。

这种执⾏操作要求在执⾏前保护现场并记忆执⾏的地址,执⾏后要恢复现场,并按原来保存的地址继续执⾏。 因此,⽅法调
⽤会产⽣⼀定的时间和空间⽅⾯的开销。

那么对于那些⽅法体代码不是很⼤,⼜频繁调⽤的⽅法来说,这个时间和空间的消耗会很⼤。⽅法内联的优化⾏为就是把⽬标
⽅法的代码复制到发起调⽤的⽅法之中,避免发⽣真实的⽅法调⽤。例如以下⽅法:

private int add1(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) { return x1 + x2;
}
最终会被优化为:

private int add1(int x1, int x2, int x3, int x4) { return x1 + x2+ x3 + x4;
}
JVM会⾃动识别热点⽅法,并对它们使⽤⽅法内联进⾏优化。我们可以通过-XX:CompileThreshold来设置热点⽅法的阈值。但要强调⼀点,热点⽅法不⼀定会被JVM做内联优化,如果这个⽅法体太⼤了,JVM将不执⾏内联操作。⽽⽅法体的⼤⼩阈值, 我们也可以通过参数设置来优化:

经常执⾏的⽅法,默认情况下,⽅法体⼤⼩⼩于325字节的都会进⾏内联,我们可以通过-XX:MaxFreqInlineSize=N来设置
⼤⼩值;
不是经常执⾏的⽅法,默认情况下,⽅法⼤⼩⼩于35字节才会进⾏内联,我们也可以通过-XX:MaxInlineSize=N来重置⼤⼩值。

之后我们就可以通过配置JVM参数来查看到⽅法被内联的情况:

-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进⾏诊断的选项参数。默认是关闭的,开启后⽀持⼀些特定参数对JVM进⾏诊断
-XX:+PrintInlining //将内联⽅法打印出来
当我们设置VM参数:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining之后,运⾏以下代码:



public static void main(String[] args) {
for(int i=0; i<1000000; i++) {//⽅法调⽤计数器的默认阈值在C1模式下是1500次,在C2模式在是10000次,我们循环遍历超过需要阈值add1(1,2,3,4);
}
}
21讲深入JVM即时编译器JIT,优化Java编译 - 图7

我们可以看到运⾏结果中,显示了⽅法内联的⽇志:
21讲深入JVM即时编译器JIT,优化Java编译 - 图8
热点⽅法的优化可以有效提⾼系统性能,⼀般我们可以通过以下⼏种⽅式来提⾼⽅法内联:

通过设置JVM参数来减⼩热点阈值或增加⽅法体阈值,以便更多的⽅法可以进⾏内联,但这种⽅法意味着需要占⽤更多地
内存;
在编程中,避免在⼀个⽅法中写⼤量代码,习惯使⽤⼩⽅法体;
尽量使⽤final、private、static关键字修饰⽅法,编码⽅法因为继承,会需要额外的类型检查。

2.逃逸分析

逃逸分析(Escape Analysis)是判断⼀个对象是否被外部⽅法引⽤或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进⾏优化。

栈上分配

我们知道,在Java中默认创建⼀个对象是在堆中分配内存的,⽽当堆内存中的对象不再使⽤时,则需要通过垃圾回收机制回 收,这个过程相对分配在栈中的对象的创建和销毁来说,更消耗时间和性能。这个时候,逃逸分析如果发现⼀个对象只在⽅法中使⽤,就会将对象分配在栈上。

以下是通过循环获取学⽣年龄的案例,⽅法中创建⼀个学⽣对象,我们现在通过案例来看看打开逃逸分析和关闭逃逸分析后, 堆内存对象创建的数量对⽐。

public static void main(String[] args) { for (int i = 0; i < 200000 ; i++) {
getAge();
}
}

public static int getAge(){
Student person = new Student(“⼩明”,18,30); return person.getAge();
}

static class Student { private String name; private int age;

public Student(String name, int age) { this.name = name;
this.age = age;
}

public String getName() { return name;
}

public void setName(String name) { this.name = name;
}

public int getAge() { return age;
}

public void setAge(int age) { this.age = age;
}
}

然后,我们分别设置VM参数:Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis -XX:+PrintGC以及 -Xmx1000m -Xms1000m
-XX:+DoEscapeAnalysis -XX:+PrintGC,通过之前讲过的VisualVM⼯具,查看堆中创建的对象数量。

然⽽,运⾏结果却没有达到我们想要的优化效果,也许你怀疑是JDK版本的问题,然⽽我分别在1.6~1.8版本都测试过了,效果还是⼀样的:

(-server -Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis -XX:+PrintGC)

21讲深入JVM即时编译器JIT,优化Java编译 - 图9

(-server -Xmx1000m -Xms1000m -XX:+DoEscapeAnalysis -XX:+PrintGC)
21讲深入JVM即时编译器JIT,优化Java编译 - 图10
这其实是因为HotSpot虚拟机⽬前的实现导致栈上分配实现⽐较复杂,可以说,在HotSpot中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟,相信不久的将来HotSpot也会实现这项优化功能。

锁消除

在⾮线程安全的情况下,尽量不要使⽤线程安全容器,⽐如StringBuffer。由于StringBuffer中的append⽅法被Synchronized关键字修饰,会使⽤到锁,从⽽导致性能下降。

但实际上,在以下代码测试中,StringBuffer和StringBuilder的性能基本没什么区别。这是因为在局部⽅法中创建的对象只能被
当前线程访问,⽆法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候JIT编译会对这个对象的⽅法锁进⾏锁消除。

public static String getString(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1);
sb.append(s2); return sb.toString();
}

标量替换

逃逸分析证明⼀个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执⾏的时候可能不创建这个对象,⽽直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就⽆需分配内存空间了。这种编译优化就叫做标量替换。

我们⽤以下代码验证:

public void foo() {
TestInfo info = new TestInfo(); info.id = 1;
info.count = 99;
…//to do something
}
逃逸分析后,代码会被优化为:

public void foo() { id = 1;
count = 99;
…//to do something
}
我们可以通过设置JVM参数来开关逃逸分析,还可以单独开关同步消除和标量替换,在JDK1.8中JVM是默认开启这些操作的。

-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析

-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭就可以了

总结

今天我们主要了解了JKD1.8以及之前的类的编译和加载过程,Java源程序是通过Javac编译器编译成 .class⽂件,其中⽂件中包含的代码格式我们称之为Java字节码(bytecode)。

这种代码格式⽆法直接运⾏,但可以被不同平台JVM中的Interpreter解释执⾏。由于Interpreter的效率低下,JVM中的JIT会在运⾏时有选择性地将运⾏次数较多的⽅法编译成⼆进制代码,直接运⾏在底层硬件上。

在Java8之前,HotSpot集成了两个JIT,⽤C1和C2来完成JVM中的即时编译。虽然JIT优化了代码,但收集监控信息会消耗运
⾏时的性能,且编译过程会占⽤程序的运⾏时间。

到了Java9,AOT编译器被引⼊。和JIT不同,AOT是在程序运⾏前进⾏的静态编译,这样就可以避免运⾏时的编译消耗和内存消耗,且 .class⽂件通过AOT编译器是可以编译成 .so的⼆进制⽂件的。

到了Java10,⼀个新的JIT编译器Graal被引⼊。Graal 是⼀个以 Java 为主要编程语⾔、⾯向 Java bytecode 的编译器。与⽤
C++ 实现的 C1 和 C2 相⽐,它的模块化更加明显,也更容易维护。Graal 既可以作为动态编译器,在运⾏时编译热点⽅法; 也可以作为静态编译器,实现 AOT 编译。

思考题

我们知道Class.forName和ClassLoader.loadClass都能加载类,你知道这两者在加载类时的区别吗?

期待在留⾔区看到你的⻅解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。

21讲深入JVM即时编译器JIT,优化Java编译 - 图11

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315852662-60abf892-c473-46cb-a400-624365e09504.png#)QQ怪<br />看⽼师这篇分享花了1个⼩时,分享的⼲货很多,回答下⽼师的问题:不同点:
  1. Class.forName()除了将类的.class⽂件加载到jvm中之外,还会对类进⾏解释,执⾏类中的static块,还会执⾏给静态变量赋值的静态⽅法。
  2. classLoader只⼲⼀件事情,就是将.class⽂件加载到jvm中,不会执⾏static中的内容,只有在newInstance才会去执⾏static

块。
2019-07-12 00:09
作者回复
这次内容确实有点多,慢慢消化。
2019-07-12 10:46

21讲深入JVM即时编译器JIT,优化Java编译 - 图12nightmare
Class•forName有重载⽅法可以指定是否需要初始化,⽽默认的⽅法初始化设置为true这会初始化类执⾏链接和初始化操作,⽽
ClasaLoader是有类加载器的loadClass⽅法加载,传⼊的是false只会执⾏链接操作,⽽不会执⾏初始化操作
2019-07-11 08:56
作者回复
回答⾮常详细
2019-07-12 10:44

21讲深入JVM即时编译器JIT,优化Java编译 - 图13-W.LI-
这其实是因为由于 HotSpot 虚拟机⽬前的实现导致栈上分配实现⽐较复杂。栈上分配影响:
1.java只有值传递,跨⽅法的局部变量在栈上分配的话,在现有栈实现上会影响栈的回收。
2.栈属于线程所有,实现栈上分配,会消耗更多的内存。让java的多线程更吃内存。
3.如果实现栈上分配,还需要GC作⽤会弱化很多吧。
4.基类型栈上分配+引⽤类型堆分配->全栈上分配。这么实现的话hotspot感觉全推翻了。
⽼师好有理解的不对的请指出谢谢
2019-07-13 17:42
作者回复
W.LI同学解释的很透彻,赞⼀个。
2019-07-14 16:20

21讲深入JVM即时编译器JIT,优化Java编译 - 图14梁中华
据⼀些书上说,因为默写原因Hotspot JVM并没有实现逃逸分析的优化,不知道最新的JVM有没有实现这条优化。
2019-07-15 19:07
作者回复
⽬前的资料来看,最新的JVM暂时没有实现。
2019-07-16 10:42

21讲深入JVM即时编译器JIT,优化Java编译 - 图15王世林
class.forNam实现也调⽤ClassLoader. 不过class.forName有static静态代码块,在类初始化的过程中执⾏了静态代码块中的代码,例如jdbc的实现需要加载数据库驱动,在静态代码块中向DriveManager注册⾃⼰.Spring直接⽤的Classloader
2019-07-14 10:34

21讲深入JVM即时编译器JIT,优化Java编译 - 图16bro.
逃逸分析跟标量替换在JDK 6u23以上版本都是默认开启状态的
2019-07-12 17:02
作者回复
是的
2019-07-17 10:20

21讲深入JVM即时编译器JIT,优化Java编译 - 图17Wendy
你好,刘兄!⽅便加下你微信吗?⽅便沟通
2019-07-12 14:11
作者回复
你好 Wendy,平台暂时不⽀持我们加个⼈微信,有问题可以在这⾥沟通,或通过github中的邮箱沟通。感谢你的⽀持。
2019-07-17 11:23

21讲深入JVM即时编译器JIT,优化Java编译 - 图18undifined
⽼师 这段代码没有懂

static{ i=0;
}
private static int i=1;
public static void main(String [] args){ System.out.println(i);
}

static代码块在类加载的时候执⾏,按照书写顺序,此时 i 还没有被声明,是怎么赋值的
2019-07-11 09:00

21讲深入JVM即时编译器JIT,优化Java编译 - 图19Liam
栈上分配这⾥,对局部变量对象的⼤⼩是否有要求,毕竟栈的内存⽐较⼩
2019-07-11 08:51
作者回复
⽬前hotspot虚拟机暂时不⽀持栈上分配对象。
2019-07-12 10:43

21讲深入JVM即时编译器JIT,优化Java编译 - 图20我⼜不乱来
Class.forName加载类的时候会对类进⾏初始化,如静态代码块,ClassLoader 不会做初始化。spring做类加载的时候应该⽤的是ClassLoader把。超哥。
2019-07-11 07:36
作者回复
正确
2019-07-12 10:41