JVM深入学习(二十五)-字节码文件概述

简介: 通过字节码文件了解java编译的过程,进而深入学习jvm

1. 概述

字节码文件是Java虚拟机跨平台/跨语言的基础.

Java虚拟机只与字节码文件绑定,至于字节码文件的源代码是否是Java代码编写就不是JVM考虑的问题了,这也是JVM的强大之处.

字节码的生成由前端编译器生成,前端编译器可以是多种语言的编译器,将源代码编译为符合JVM规范的class文件,交由JVM解释执行.

以Java为例,jdk中就包含了可以编译java源码的编译器,我们可以使用javac来将一个java源代码文件编译为class文件.

而通过对class文件的详细了解,我们可以看到源代码中无法表现的一些细节,编译器是如何对源代码进行优化编译的.

字节码文件是二进制文件.

字节码文件由 操作码操作数组成. 例如 bipush 20 前面的就是操作码,20就是操作数,操作数可以没有

2. 前端编译器

2.1 javac

javac就是一种前端编译器,也是我们使用最多的编译器,目前在idea里默认的编译器就是javac

javac是一种全量编译器,就是不管java源代码改了多少,都是全量编译.


2.2 ECJ

Eclipse提供了一种区别于javac的编译器给,叫ECJ(Eclipse Compiler For Java),属于Eclipse的插件,他是一个增量的编译器,所以速度要比javac快,而且质量不差多少

2.3 AJC

AspectJ Compiler 可以作为idea的一个插件与javac配合使用,提高编译效率

3. 学习查看字节码的优点

通过几个例子的字节码查看,理解字节码的好处.

3.1 例一

先来一个例子:

publicclassClassTest {
publicstaticvoidmain(String[] args) {
Integeri1=10;
inti2=10;
System.out.println(i1==i2);
    }
}

结果为true,为什么呢?

我们来看一下字节码,就很清楚了:


通过第二行我们可以看到 i1的定义本质上是一个Integer.valueOf方法,我们进入该方法看一下:

publicstaticIntegervalueOf(inti) {
if (i>=IntegerCache.low&&i<=IntegerCache.high)
returnIntegerCache.cache[i+ (-IntegerCache.low)];
returnnewInteger(i);
    }
// 进入cache方法 静态内部类privatestaticclassIntegerCache {
staticfinalintlow=-128;
staticfinalinthigh;
staticfinalIntegercache[];
static {
// high value may be configured by propertyinth=127;
StringintegerCacheHighPropValue=sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue!=null) {
try {
inti=parseInt(integerCacheHighPropValue);
i=Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUEh=Math.min(i, Integer.MAX_VALUE- (-low) -1);
                } catch( NumberFormatExceptionnfe) {
// If the property cannot be parsed into an int, ignore it.                }
            }
high=h;
cache=newInteger[(high-low) +1];
intj=low;
for(intk=0; k<cache.length; k++)
cache[k] =newInteger(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)assertIntegerCache.high>=127;
        }
privateIntegerCache() {}
    }

可以看到是一个IntegerCache在获取了值 (int为10 不超过low的值-128 high的值127); IntegerCache对-128到127这256个数字做了缓存,所以取到的数字10本质上就是int 10

所以输出为true

3.2 例二

字符串拼接里在从jvm查看字符串

packagecom.zy.study15;
/*** @Author: Zy* @Date: 2022/2/7 15:45* 字节码查看字符串拼接*/publicclassClassTest {
publicstaticvoidmain(String[] args) {
// 其实是StringBuilder的拼接 生成的是一个新的String对象StringhelloWorld=newString("hello") +newString("world");
// 字符串常量池StringhelloWorld1="helloworld";
// 不相等 falseSystem.out.println(helloWorld==helloWorld1);
// 也是新创建的对象Stringhelloworld2=newString("helloworld");
// 不相等 falseSystem.out.println(helloWorld1==helloworld2);
    }
}

字节码如下:

0new#2<java/lang/StringBuilder>3dup4invokespecial#3<java/lang/StringBuilder.<init> : ()V>7new#4<java/lang/String>10dup11ldc#5<hello>13invokespecial#6<java/lang/String.<init> : (Ljava/lang/String;)V>16invokevirtual#7<java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>19new#4<java/lang/String>22dup23ldc#8<world>25invokespecial#6<java/lang/String.<init> : (Ljava/lang/String;)V>28invokevirtual#7<java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>31invokevirtual#9<java/lang/StringBuilder.toString : ()Ljava/lang/String;>34astore_135ldc#10<helloworld>37astore_238getstatic#11<java/lang/System.out : Ljava/io/PrintStream;>41aload_142aload_243if_acmpne50 (+7)
46iconst_147goto51 (+4)
50iconst_051invokevirtual#12<java/io/PrintStream.println : (Z)V>54new#4<java/lang/String>57dup58ldc#10<helloworld>60invokespecial#6<java/lang/String.<init> : (Ljava/lang/String;)V>63astore_364getstatic#11<java/lang/System.out : Ljava/io/PrintStream;>67aload_268aload_369if_acmpne76 (+7)
72iconst_173goto77 (+4)
76iconst_077invokevirtual#12<java/io/PrintStream.println : (Z)V>80return

 从字节码里你就可以看到多个String对象相加是怎么操作了,创建的StringBuilder对象进行append操作,最后调用toString方法

3.3 例三

packagecom.zy.study15;
/*** @Author: Zy* @Date: 2022/2/9 18:44* 父子类从字节码看*/publicclassClassTest2 {
staticclassFather{
intnum=0;
voidprint(){
System.out.println("Father打印,num的值为"+num);
        }
Father(){
print();
num=20;
        }
    }
staticclassSonextendsFather{
intnum=100;
@Overridevoidprint(){
System.out.println("Son打印,num的值为"+num);
        }
Son(){
print();
num=200;
        }
    }
publicstaticvoidmain(String[] args) {
// 多态 父类子类的构造器里都有打印,那么打印num的结果是什么,为什么Fatherfather=newSon();
System.out.println(father.num);
    }
}

打印结果

解释这个结果只需要看下字节码就可以了

new Son()本质上就是调用了构造器,所以我们看下构造器的方法


3.3.1 Father类init字节码

0aload_0this对象1invokespecial#10<java/lang/Object.<init> : ()V>父类Object的构造方法4aload_05iconst_0常量0也就是num的默认值6putfield#6<com/zy/study15/ClassTest2$Father.num : I>给num赋默认初始值09aload_010invokevirtual#11<com/zy/study15/ClassTest2$Father.print : ()V>调用Print方法,打印num13aload_014bipush20构造方法中的给num赋值20,这里是取出常量2016putfield#6<com/zy/study15/ClassTest2$Father.num : I>给num赋值2019return


3.3.2 Son类init字节码

0aload_01invokespecial#10<com/zy/study15/Father.<init> : ()V>同理,父类Father的构造方法4aload_05bipush100取出1007putfield#6<com/zy/study15/Son.num : I>给num赋10010aload_011invokevirtual#11<com/zy/study15/Son.print : ()V>Print打印方法,打印num14aload_015sipush200取出常量20018putfield#6<com/zy/study15/Son.num : I>给num赋值20021return

解释下:

  1. new Son()会先调用Son类的构造方法,具体执行过程可以看上面的Son类的init字节码
  2. 从Son的init构造方法的执行过程中可以看到,在第二行调用父类Father的构造方法.
  3. 这个时候再去调用Father的构造方法
  4. 可以看到在打印num的时候,只给num赋了初始值0,所以第一次打印的值为0
  5. 并且由于多态,Print方法被子类Son重写,所以打印的内容里是Son打印,而不是Father打印
  6. 执行完父类的构造方法后,继续回到Son类的init方法
  7. 可以看到打印num之前给num赋了100,所以打印num的值是100
  8. 再之后给num赋了200,所以再获取Son的num的值就是200了.
  9. 最后一行的输出为20,这是因为我们获取的是Father的num值,而不是Son的num的值.

3.3 总结

通过字节码可以看到一些不容易理解的代码的执行过程.

4. 查看字节码文件的方法

4.1 第三方软件

安装Binary Viewer工具,可以打开字节码文件

Binary Viewer安装包 https://www.aliyundrive.com/s/5HCJfAFQoxf 提取码0eo6

或者Notepad++插件 Hex-Editor

直接在插件市场安装即可(如果插件市场有问题,尝试安装最新版本notepad++)


4.2 jdk自带javap

# 将class文件反编译后的内容输出到当前目录

javap -v ClassTest.class > ClassTestOut.java


4.3 idea插件 jclasslib

idea插件市场安装 jclasslib即可

效果如下:

目录
相关文章
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
77 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
39 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
2月前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
36 4
|
2月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
68 3
|
2月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
35 3
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
59 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
25天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
208 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
40 4
|
15天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
24天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80