Java字节码剖析String(done)

Java命令

javac:.java文件编译成.class文件,.class文件是字节码(概念类似机器码,字节码是JVM层面)
java:执行.class文件
javap:反汇编,.class文件反汇编成JVM的指令,The Java Virtual Machine Instruction Set

IDEA执行javap

IDEA安装jclasslib插件
在这里插入图片描述
使用插件
在这里插入图片描述
第一个是插件,第二个是IDEA自带的,本质上都是执行javap命令

方式一

        String s = new String("Hello World!");

JVM指令:

 0 new #2 <java/lang/String>  ## Create new object, #2 对应常量池中的String.class
 3 dup ## Duplicate the top operand stack value
 4 ldc #3 <Hello World!>  ## Push item from run-time constant pool
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1  ## Store reference into local variable 1
10 return

在这里插入图片描述
下面逐条指令分析

new #2

在这里插入图片描述
new指令创建了一个String对象(#2 表示常量池中String类),并且把对象引用objectref压入Operand Stack;需要注意,此时创建的对象还不是完整的对象;
在这里插入图片描述

在这里插入图片描述

dup

在这里插入图片描述
dup指令拷贝栈顶的值,即是dup指令执行后Operand Stack有两个objectref,该操作的意义后续再说;

ldc #3

在这里插入图片描述
ldc指令从常量池取出对应的值并压入Operand Stack;#3表示常量池中字符串常量"Hello World!";到该步骤说明了new String()方法会在常量池创建对应的字符串常量;
在这里插入图片描述
我们可以做进一步的验证,在这里我们new一个普通的对象:

	Object object = new Object();

JVM指令:

0 new #7 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
8 return

可以看到,以上指令没有包含ldc

invokespecial #4

在这里插入图片描述
invokespecial指令实例化对象,该步骤会将栈顶的字符串常量"Hello World!"弹出,调用String类的父类和它本身的构造方法;#4 对应String类的init方法;此时得到一个完整的String对象;
在这里插入图片描述

astore_1

在这里插入图片描述
astore命令将Operand Stack的栈顶值弹出,写入局部变量表,_1表示局部变量表中的Nr.1;我们回顾前面的dup指令拷贝了一份引用,此时弹出一份引用,栈顶还有一份引用,因为从历史经验来看,我们通常会在定义了一个变量后紧接着就会使用它;所以JVM拷贝引用便于后续继续使用该变量;
在这里插入图片描述

总结

从以上指令可以得出:

  • new Object()方法不是一个原子性操作(双重检查锁为什么要volatile关键字)
  • new String()方法会创建两个对象,一个在常量池(如果没有则创建),一个在堆,还有一个栈中的引用;

方式二

String s = "Hello World!";

JVM指令:

0 ldc #3 <Hello World!>
2 astore_1
3 return

该方式只会在常量池创建(如果没有则创建),不会在堆上创建;

总结

  • 方式二比方式一更加简洁,所需要的空间更少,指令更少,所以我们在没有特殊情况下,尽可能使用方式二创建字符串;IDEA编译器也会给出相应的提示:
    在这里插入图片描述

方式三

    private static void internString() {
        String s1 = "Hello World!";
        String s2 = s1;
        String s3 = s1.intern();
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s2));
        System.out.println(System.identityHashCode(s3));
    }

    private static void newString() {
        String s1 = new String("Hello World!");
        String s2 = s1;
        String s3 = s1.intern();
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s2));
        System.out.println(System.identityHashCode(s3));
    }

打印结果:
在这里插入图片描述
因为String类的hashCode()方法被重写了,此处使用System.identityHashCode()方法,该方法说明不管hashCode()是否被重写,都会返回对象的默认的hashCode()方法;
在这里插入图片描述

总结

  • 从打印结果可以看出,intern()方法是指向常量池(如果没有则创建);所以当两个字符串的字面量相等,那它们的intern()结果一定是一致的;
    在这里插入图片描述
    那么问题来了,从上面的指令来看,不管哪种方法创建字符串都会在常量池有一份,intern()方法中的常量池中不存在的情况是怎么出现的呢?

方式四

	String s = new String("Hello ") + new String("World!");

JVM指令:
在这里插入图片描述
字节码分析可以得到,该方式创建的字符串,常量池只会存每个分量,整体的值不会存入常量池,并且JVM进行了优化,通过StringBuilder拼接字符串再toString();在此之后调用s.intern()方法才会将整体的值写入常量池,此处不再赘述;

方式五

	String s = "Hello " + "World!";

JVM指令:
在这里插入图片描述
该方式的指令跟方式二是一样的,并且该步骤是在编译阶段完成的,可以去看一下反编译的.class文件:
在这里插入图片描述
.java文件:
在这里插入图片描述
此处也说一下反编译和反汇编的区别:
Java代码执行过程
如上图所示,我们编写的.Java源代码,经过Java编译器(即Javac)编译后得到.class字节码文件(要注意这里的字节码文件是我们看不懂的二进制表示,而我们平时看到的.class文件是Decompiled .class文件,跟我们编写的.Java代码很像,这就是反编译)
Decompiled .class文件
真正的.class文件是纯纯的字节码,想看纯纯的字节码可以IDEA安装插件:
在这里插入图片描述
然后Build Project
在这里插入图片描述
.class文件Open As Binary
在这里插入图片描述
字节码文件(即.class文件)都会以CAFE BABE开头
在这里插入图片描述
而我们通常说的汇编,是从CPU汇编指令到机器码的过程。所以反汇编就是从机器码得到汇编指令的过程。而从JVM层面来看的话,JVM指令跟字节码也有着对应关系,从字节码到JVM的过程,也许可以理解为JVM层面的反汇编,但是感觉怪怪的,因为我们通常讲的反汇编都是得到汇编指令。

不同于C语言,它们是源代码经过编译之后得到汇编指令,汇编指令汇编后得到机器码。而Java语言在中间多了一层JVM的字节码,这也是Java跨平台的特性。

这里也要强调一下,JVM指令跟汇编指令不是一一对应的关系,不是说每条JVM指令都会编译成一条汇编指令,通常情况下是一条JVM指令被编译成多条汇编指令(所以JVM指令比汇编指令更容易看懂)

方式六

	String s = new String("Hello ") + "World!";

JVM指令:
在这里插入图片描述

最后补充一点
当String是final变量,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用(也就是后续代码用到该变量的地方都会被直接替换成常量)。

        String a = "Hello World!";
        final String b = "Hello ";
        String d = "Hello ";
        String c = b + "World!";
        String e = d + "World!";
        System.out.println((a == c)); // true
        System.out.println((a == e)); // false

反编译结果
我们可以从反编译结果看到,c字符串中的b已经被替换了。

总结

该讲的都讲完啦,因为intern()是native方法,想要了解底层实现,可以参考:
美团技术团队深入解析String#intern

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值