首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >(JVM)带你一起研究JVM的语法糖功能 和 JVM的即时编译器

(JVM)带你一起研究JVM的语法糖功能 和 JVM的即时编译器

作者头像
用户11865655
发布2025-10-13 16:30:29
发布2025-10-13 16:30:29
1900
代码可运行
举报
文章被收录于专栏:CSDN专栏CSDN专栏
运行总次数:0
代码可运行

1. 语法糖

所谓的语法糖,其实就是指java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们第一个额外福利

以下代码的分析,借助了javap工具,idea的反编译功能,idea插件jclasslib等工具。 另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换出中间的java源码。

1.1 默认构造器

代码语言:javascript
代码运行次数:0
运行
复制
public class Candy1{
    
}

编译成class后的代码

代码语言:javascript
代码运行次数:0
运行
复制
public class Candy1{
    // 这个无参构造是编译器帮助我们加上的
    public Candy1(){
        super();// 即父类Object的无参构造方法,即调用 java/lang/Object."<init>":()V
    }
}

1.2 自动拆装箱

这个特性是JDK5开始加入的,代码片段1:

代码语言:javascript
代码运行次数:0
运行
复制
public class Candy2{
    public static void main(String[] args){
        Integer x = 1;
        int y = x;
    }
}

这段代码在JDK5之前是无法编译通过的,必须改写为如下:

代码语言:javascript
代码运行次数:0
运行
复制
public class Candy2{
    public static void main(String[] args){
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在JDK5以后都由编译器在编译阶段完成。

1.3 泛型集合取值

泛型也是在JDK5开始加入的特性,但java在编译泛型代码会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当作了Object类型来处理:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo4 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(10); // 实际调用的是 List.add(Object e)
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    }
}

所以在取值时,编译器真正生成字节码中,还要额外做一个类型转换的操作:

代码语言:javascript
代码运行次数:0
运行
复制
// 需要将Object转为Integer类型
Integer x = (Integer)list.get(0);

如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:

代码语言:javascript
代码运行次数:0
运行
复制
// 需要将Object转为Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

泛型擦除,在字节码中,所有的对象类型都被转为了Object了 所幸,这些麻烦事不用我们自己做

擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息

代码语言:javascript
代码运行次数:0
运行
复制
Classfile /E:/Java/学习案例/JVM/JVM/src/test/java/Class/demo4.class
  Last modified 2024年10月29日; size 478 bytes
  MD5 checksum 4d2758e590977925b85935c072dc7242
  Compiled from "demo4.java"
public class Class.demo4
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Class/demo4
  super_class: #9                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #9.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // java/util/ArrayList
   #3 = Methodref          #2.#18         // java/util/ArrayList."<init>":()V
   #4 = Methodref          #7.#20         // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #5 = Methodref          #2.#21         // java/util/ArrayList.add:(Ljava/lang/Object;)Z
   #6 = Methodref          #2.#22         // java/util/ArrayList.get:(I)Ljava/lang/Object;
   #7 = Class              #23            // java/lang/Integer
   #8 = Class              #24            // Class/demo4
   #9 = Class              #25            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               SourceFile
  #17 = Utf8               demo4.java
  #18 = NameAndType        #10:#11        // "<init>":()V
  #19 = Utf8               java/util/ArrayList
  #20 = NameAndType        #26:#27        // valueOf:(I)Ljava/lang/Integer;
  #21 = NameAndType        #28:#29        // add:(Ljava/lang/Object;)Z
  #22 = NameAndType        #30:#31        // get:(I)Ljava/lang/Object;
  #23 = Utf8               java/lang/Integer
  #24 = Utf8               Class/demo4
  #25 = Utf8               java/lang/Object
  #26 = Utf8               valueOf
  #27 = Utf8               (I)Ljava/lang/Integer;
  #28 = Utf8               add
  #29 = Utf8               (Ljava/lang/Object;)Z
  #30 = Utf8               get
  #31 = Utf8               (I)Ljava/lang/Object;
{
  public Class.demo4();
    descriptor: ()V
    flags: (0x0001) 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 5: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokevirtual #5                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
        17: pop
        18: aload_1
        19: iconst_0
        20: invokevirtual #6                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
        23: checkcast     #7                  // class java/lang/Integer
        26: astore_2
        27: return
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 9: 18
        line 10: 27
}
SourceFile: "demo4.java"

1.4 可变参数

可变参数也是JDK5开始加入的新特性:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo5 {
    public static void foo(String... args){
        String[] arr = args;// 直接赋值
        System.out.println(arr);
    }
    public static void main(String[] args) {
        foo("hello","world");
    }
}

可变参数String… args 其实是一个String[] args, 从代码的赋值语句中就可以看出来

同样java编译器会在编译期间就将上述代码转为:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo5 {
    public static void foo(String... args){
        String[] arr = args;// 直接赋值
        System.out.println(arr);
    }
    public static void main(String[] args) {
        foo(new String[]{"hello","world"});
    }
}

如果调用了foo()则等价代码为foo(new String[]{}),创建了一个空的数组,而不会传递null进去

1.5 foreach循环

仍是jdk5开始引入的语法糖,数组的循环:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo6 {
    public static void main(String[] args) {
        int[] array = {1,2,4,5}; // 数组赋值的简化写法,也算是语法糖
        for (int i : array) {
            System.out.println(i);
        }
    }
}

编译器编译后,转换为:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo6 {
    public static void main(String[] args) {
        int[] array = new int[]{1,2,4,5};
        for (int i=0;i<array.length;++i) {
            int x = array[i]; // x 就是自己定义接受的变量 i 
            System.out.println(x);
        }
    }
}

集合的循环:

代码语言:javascript
代码运行次数:0
运行
复制
public void test1_List(int[] args){
    List<Integer> list = Arrays.asList(1,2,3,4,5,6);
    for (Integer i : list) {
        System.out.println(i);
    }
}

实际被编译器转换为迭代器的调用:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo6 {
    public static void main(String[] args) {
    }

    public void test1_List(int[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5,6);
        Iterator list = list.iterator(); // 获取迭代器
        while(iter.hasNext()){// 对迭代器中包含的对象进行循环
            Integer e = (Integer)iter.next();// Object类型转为Integer类型
            System.out.println(e);
        }
    }
}

foreach循环写法,能够配合数组,以及所有实现了iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器(Iterator)

1.6 switch字符串

从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

代码语言:javascript
代码运行次数:0
运行
复制
package Class;

public class demo7 {
    public static void main(String[] args) {

    }

    public static void cc(String str){
        switch (str){
            case "hello":{
                System.out.println("h");
                break;
            }
            case "world":{
                System.out.println("w");
                break;
            }
        }
    }
}

switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清除

会被编译器转换为:

代码语言:javascript
代码运行次数:0
运行
复制
public static void cc(String str){
    byte x = -1;
    switch (str.hashCode()){
        case 99162322: // hello 的哈希值
            if (str.equals("hello")){
                x = 0;
            }
            break;
        case 113318802: // world的哈希值
            if (str.equals("world")){
                x=1;
            }
            break;
    }
    switch (x){
        case 0:
            System.out.println("h");
            break;
        case 1:
            System.out.println("w");
    }
}

可以看到执行了两边switch,第一遍根据字符串的hashCode和equals将字符串的转换为相应byte类型,第二遍才是利用byte执行进行比较。

为什么第一遍时必须即比较hashCode,又利用equals比较呢?hashCode是为了提高效率,减少可能的比较;而equals是为了防止hashCode冲突,例如BMC.这两个字符串的hashCode值都是2123。

1.7 switch枚举

switch枚举的例子,原始代码:

代码语言:javascript
代码运行次数:0
运行
复制
enum Sex{
    MALE,FEMALE
}
代码语言:javascript
代码运行次数:0
运行
复制
public class demo8 {
    public static void foo(Sex sex){
        switch (sex){
            case MALE:
                System.out.println("男");
                break;
            case FEMALE:
                System.out.println("女");
                break;
        }
    }
}
代码语言:javascript
代码运行次数:0
运行
复制
/**
     * 定义一个合成类(仅JVM使用,对我们不可见)
     * 用来映射枚举的 ordinal 与数组元素的关系
     * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal() = 1
      */
static class $MAP{
    static int[] map = new int[2];
    static {
        map[Sex.MALE.ordinal()] = 1;
        map[Sex.FEMALE.ordinal()] = 2;
    }
}
public static void foo(Sex sex){
    int x = $MAP.map[sex.ordinal()];
    switch (x){
        case 1:
            System.out.println("男");
            break;
        case 2:
            System.out.println("女");
            break;
    }
}

1.8 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

代码语言:javascript
代码运行次数:0
运行
复制
enum Sex{
    MALE,FEMALE
}

转换后的代码:

代码语言:javascript
代码运行次数:0
运行
复制
public final class Sex extends Enum<Sex>{
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;

    static {
        MALE = new Sex("MALE",0);
        FEMALE = new Sex("FEMALE",1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }
    private Sex(String name,int ordinal){
        super(name,ordinal);
    }
    public static Sex[] values(){
        return $VALUES.clone();
    }
    public static Sex ValueOf(String name){
        return Enum.valueOf(Sex.class,name);
    }
}

本质上枚举还是创建了一个对象,只不过继承了一个枚举类型的对象 枚举中的值是对象中的成员变量

1.9 try-with-resources

JDK7开始新增了对需要关闭的资源处理的特殊语法try-with-resources:

代码语言:javascript
代码运行次数:0
运行
复制
try(资源变量=创建资源对象){
    
}catch(){
    
}

其中资源对象需要实现 AutoCloseable 接口,例如:InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了AutoCloseable,使用try-with-resources可以不用谢finally语句块,编译器会帮助生成关闭资源代码

代码语言:javascript
代码运行次数:0
运行
复制
public class demo9{
    public static void main(String[] args){
        try(InputStream is = new FileInputStream("./1.text")){
            System.out.println(is);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

转换为:

代码语言:javascript
代码运行次数:0
运行
复制
public class demo10 {
    public static void main(String[] args) {
        try{

            FileInputStream is = new FileInputStream("./1.text");
            Throwable t = null;
            try{
                System.out.println(is);
            }catch (Throwable e1){
                t = e1;
                throw e1;// t 就是代码出现的异常,
            }finally {
                if (is!=null){
                    if (t!=null){
                        try {
                            is.close();
                        }catch (Throwable e2){
                            // 如果close出现异常,那么会作为被压制异常添加
                            t.addSuppressed(e2);
                        }
                    }else {
                        // 当代码并未出现异常,close出现的异常就是最后catch块中的e
                        is.close();
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

为什么要设计一个addSuppresed(Throwable e)(添加被压制异常)的方法呢?

  • 是为了防止异常信息的丢失

1.10 方法重写时的桥接方法

都知道,方法重写时对放回值分为两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类
代码语言:javascript
代码运行次数:0
运行
复制
class A{
    public Number m(){
        return 1;
    }
}
class B extends A{
	// 子类m 方法返回值是 Integer 是父类 m 方法返回值 Number 的子类
    @Override
    public Integer m(){
        return 2;
    }
}

对于子类,编译器会做如下处理

代码语言:javascript
代码运行次数:0
运行
复制
class B extends A{
    public Integer m(){
        return 2;
    }
    // 此方法才是真正重写了父类public Number m() 方法
    public synthetic bridge Number m(){
        // 调用 public Integer m()
        return m();
    }
}

其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Integer m() 没有命名冲突,可以用下面的反射代码来验证:

代码语言:javascript
代码运行次数:0
运行
复制
for(Method m: B.class.getDeclaredMethods()){
    System.out.println(m);
}

输出:

代码语言:javascript
代码运行次数:0
运行
复制
public java.lang.Integer 方法地址;
public java.lang.Number 方法地址;

1.11 匿名内部类

代码语言:javascript
代码运行次数:0
运行
复制
public class demo12{
    public static void test(final int x){
        Runnable runnable = new Runnable(){
            @Override
            public void run(){
                System.out.println("ok: "+x);
            }
        };
    }
}

转换后

代码语言:javascript
代码运行次数:0
运行
复制
final class demo12$1 implements Runnable{
    int val$x;
    demo12$1(int x){
        this.val$x=x;
    }
    public void run(){
        System.out.println("ok: " + this.val$x);
    }
}

在编译后,会创建一个新的类,并实现Runnable接口,以此来实现run方法

在创建 demo121 对象时,将x的值赋值给 demo1 对象的valx属性,x不应该再发生改变,如果改变了那么valx属性没有机会再跟着一起变化,这解释了为什么匿名内部类引用局部变量时,局部变量必须时final的原因

2. 即时编译

2.1 分层编译

(TieredCompliation)

例:

代码语言:javascript
代码运行次数:0
运行
复制
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.println("触发数:"+i+",耗时:"+(end-start));
    }
}

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

  1. 解释执行(Interpreter)
  2. 使用c1即时编译执行(不带 profiling)
  3. 使用c1即时编译执行(带基本的 profiling)
  4. 使用c1即时编译执行(带完全的profiling)
  5. 使用c2即时编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如:

  • 方法的调用次数
  • 循环的回边次数

这种优化手段称之为 逃逸分析 ,发现新建的对象是否可以逃逸,可以使用如下选项关闭逃逸分析

-XX: -DoEscapeAnalysis

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

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

对于占据大部分的不常用代码,我们无需耗费事件将其编译成机器码,而是采用解释执行的方式运行。

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

2.2 方法内联

代码语言:javascript
代码运行次数:0
运行
复制
/**
     * 方法内联
     */
@Test
public void test1(){
    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.println("触发数:"+i+",耗时:"+(end-start)+",\t 相乘数值:"+x);
    }
}
private static int square(final int a){
    return a * a;
}

如果发现square是热点方法,并且长度不太长时,会进行内联。

内联操作:把方法内代码拷贝、粘贴到调用者的位置 ​ 这种方法可以大大降低对方法的解析速度

2.3 反射优化

代码语言:javascript
代码运行次数:0
运行
复制
public class demo2 {
    public static void foo(){
        System.out.println("foo...");
    }
    public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException {
        Method foo = demo2.class.getMethod("foo");
        for (int i =0; i<=16;i++){
            System.out.printf("%d\t",i);
            // 调用反射
            foo.invoke(null);
        }
        System.in.read();
    }
}

在进行反射时,底层代码会进行判断,当调用超过15次时,JVM会直接使用类进行调用

代码语言:javascript
代码运行次数:0
运行
复制
private static int     inflationThreshold = 15;

static int inflationThreshold() {
    return inflationThreshold;
}

public Object invoke(Object obj, Object[] args)
    throws IllegalArgumentException, InvocationTargetException
{
    // 从工厂中拿取反射次数阈值(默认15)
    if (++numInvocations > ReflectionFactory.inflationThreshold()
        && !method.getDeclaringClass().isHidden()
        && generated == 0
        && U.compareAndSetInt(this, GENERATED_OFFSET, 0, 1)) {
        try {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator(). // 方法访问器生成器 在这里就会直接生成调用方法
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
            parent.setDelegate(acc);
        } catch (Throwable t) {
            // Throwable happens in generateMethod, restore generated to 0
            generated = 0;
            throw t;
        }
    }

    return invoke0(method, obj, args);
}

故而,在超出反射阈值后,JVM会给你使用使用类调用方法

  • demo2.foo(); // 就是这行代码

通过查看 ReflectionFactory源码可知:

  • sun.reflect.noInflation 可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用以此,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阈值

3. 😊👉前篇知识点

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 语法糖
    • 1.1 默认构造器
    • 1.2 自动拆装箱
    • 1.3 泛型集合取值
    • 1.4 可变参数
    • 1.5 foreach循环
    • 1.6 switch字符串
    • 1.7 switch枚举
    • 1.8 枚举类
    • 1.9 try-with-resources
    • 1.10 方法重写时的桥接方法
    • 1.11 匿名内部类
  • 2. 即时编译
    • 2.1 分层编译
    • 2.2 方法内联
    • 2.3 反射优化
  • 3. 😊👉前篇知识点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档