学习JVM时期的所发现的一些细小知识点

70 阅读4分钟

为什么Java是一次编写处处运行?

除了将代码先是编译为字节码,再一行一行的翻译为机器码之外,Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一(《Java 编程思想》2.2 节有提到)。

对JAVA运行速度的深入理解

人们常常认为JAVA运行速度比较快,是因为JAVA是面向过程的,但这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。

我还想补充讨论下,首先面向过程和对象是一种编程思想,就像@cjjMichael 所言“直接比较语言性能本来就是不恰当的”,语言执行性能应该从他们最终执行方式上讨论。直接编译成机器码,然后执行的语言(比如C,编译器一次性编译为平台相关的机器码),从过程复杂度上,肯定比解释执行的语言(HTML,JavaScript,浏览器的解释器,把代码一行行解释为机器码)和 Java这种中间码(编译)+虚拟机(解释成机器码)的方式,要性能高的多。

JAVA 的泛型Code specialization

在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。 例如,针对一个泛型 list ,可能需要 针对 string , integer , float 产生三份目标代码

由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。

JAVA类的字节码文件

  • ldc将一个int压入操作数栈
  • bipush将一个byte的整数值压入操作数栈
  • putststic将操作数栈里的数字赋值给一个常量
  • putfield将操作数栈里的数字赋值给一个字段

对方法的操作

  • invokevirtual:调用public方法时的机器吗,无法在编译期间得知信息,因为可能有重写现象 动态绑定(vtable)
  • invokestatic,invokespecial:调用其他方法都是在编译期间就可以得知信息的 静态绑定
  • 静态方法不需要操作数,用对象调用后在aload1之后又pop了

调用多态方法的过程

  • 先通过栈帧中的对象引用找到实体对象
  • 分析对象头,找到对象的实际Class
  • Class结构中有vtable 它在类加载的链接阶段就已经生成好了
  • 查表得到方法的具体地址
  • 执行方法的字节码

常量折叠

,并不是所有的常量都会进行折叠,必须是编译期常量之间进行运算才会进行常量折叠,编译器常量就是编译时就能确定其值的常量,这个定义很严格,需要满足以下条件:

  • 字面量是编译期常量(数字字面量,字符串字面量等)。
  • 编译期常量进行简单运算的结果也是编译期常量,如1+2,”a”+”b”。
  • 被编译器常量赋值的 final 的基本类型和字符串变量也是编译期常量。

执行类的静态代码块过程

编译器会按从上到下的顺序,收集所有static静态代码块和静态成员变量赋值的信息,合并为一个特殊的方法()V

普通代码块和成员变量赋值的信息也是如此,但原始构造方法总是在最后

在执行x++时的字节码文件

x=0
x=x++;
  • 将0iload到操作数栈
  • 将局部变量表中的xiinc 1(此时槽里x=1)
  • 再将操作数栈中的0istore赋值给x

如果x为static

  • getstatic i//获取静态变量的值
  • iconst_1 //准备一个常量槽
  • isub //自减
  • putstatic //将修改后的赋值给原数字