[中级01]java为什么能跨平台,而C\C++语言不能跨平台

2022-10-08 10:47:26

编译后的成果物层面

        同样的C\C++源文件文件,经过不同的计算机硬件(x86平台、arm、AMD)、不同的操作系统(Linux\mac\windows etc.)上的编译器编译后,生成了不同的机器码,是互不通用的。

        而Java源码(.java)经过编译后,生成了class字节码文件,通过不同平台上的JVM(Java 虚拟机)都可以解释执行。JVM掩盖了计算机硬件和操作系统的差异,对class提供了统一的执行接口,这就是java为啥能跨平台的原因。

在面向对象设计原则中,有个原则叫“开闭原则”(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。java的跨平台性就是遵守了这一原则。它所使用的设计模式,是适配器模式。

#TODO: 如果深入理解这个问题,可能涉及到很多层面(编译原理、操作系统提供的API、操作系统内核提供的系统调用、CPU指令集),不同的操作系统中的函数库提供了不同的系统API(应用程序接口);不同的编译器和操作系统定义了不同的ABI(应用二进制接口);不同的CPU架构,有着不同的指令集。机器码就是指令集上指令对应的二进制(0和1组成的序列)表示。

以openjdk8为例

openjdk 是如何屏蔽操作系统和硬件的不同的呢?可以到jdk的源码中找到答案:

源码网址:jdk8u/jdk8u/hotspot: 00df30073cfa /

 在源码目录的os和cpu中对应的代码,就是用来分别对操作系统和计算机硬件做屏蔽的

在学jvm的时候,我们很多人可能都见过下面这个图。下图中的Native Method Interface部分,就对应了上图中src目录下一部分的源码,所以如果想学好jvm,读源码是一个好方法。

首先我们来看看上面这个图的“Class File”这块,class文件是以什么样的形态被Class Loader SubSystem加载的。

Java的class文件是什么

        Class文件是jvm认识的一种字节码文件,里面的地址都是逻辑的地址。最后需要运行在操作系统中,操作系统只能识别真实的物理地址。此时需要动态链接(这个过程就是将逻辑地址变成物理地址),就是在运行时动态地绑定对象、对象地址。

此外,它还是一组以8位字节为基础单位的二进制流(容错性低,错一个字节则整个class文件不可用;节省空间\可以不用定义传输的格式,比如json,xml,而直接用二进制流传输数据),各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何的分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必需数据。

Class字节码文件只有两种数据类型:无符号数(u)和表(info)。

Class文件的规范可以在oracle的jvm规格说明书 的第4节(4. The class File Format)中查看:

u4:4字节无符号数,u2类似

将一个.class文件用sublim打开,查看16进制形式,这个16进制形式按上面的ClassFile Structure对应解析例如:

package main.test;
public class classtest {
    public static void main(String[] args) {
        int a = 11;
        int b = 22;
        int c = a+b;
        System.out.println(c);
    }
}

生成class字节码后用sublim打开

cafe babe 0000 0034 0024 0a00 0500 1709
0018 0019 0a00 1a00 1b07 001c 0700 1d01
0006 3c69 6e69 743e 0100 0328 2956 0100
0443 6f64 6501 000f 4c69 6e65 4e75 6d62
6572 5461 626c 6501 0012 4c6f 6361 6c56
6172 6961 626c 6554 6162 6c65 0100 0474
6869 7301 0015 4c6d 6169 6e2f 7465 7374
2f63 6c61 7373 7465 7374 3b01 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0004 6172
6773 0100 135b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 0100 0161 0100 0149
0100 0162 0100 0163 0100 0a53 6f75 7263
6546 696c 6501 000e 636c 6173 7374 6573
742e 6a61 7661 0c00 0600 0707 001e 0c00
1f00 2007 0021 0c00 2200 2301 0013 6d61
696e 2f74 6573 742f 636c 6173 7374 6573
7401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0100 106a 6176 612f 6c61 6e67
2f53 7973 7465 6d01 0003 6f75 7401 0015
4c6a 6176 612f 696f 2f50 7269 6e74 5374
7265 616d 3b01 0013 6a61 7661 2f69 6f2f
5072 696e 7453 7472 6561 6d01 0007 7072
696e 746c 6e01 0004 2849 2956 0021 0004
0005 0000 0000 0002 0001 0006 0007 0001
0008 0000 002f 0001 0001 0000 0005 2ab7
0001 b100 0000 0200 0900 0000 0600 0100
0000 0200 0a00 0000 0c00 0100 0000 0500
0b00 0c00 0000 0900 0d00 0e00 0100 0800
0000 6a00 0200 0400 0000 1210 0b3c 1016
3d1b 1c60 3eb2 0002 1db6 0003 b100 0000
0200 0900 0000 1600 0500 0000 0400 0300
0500 0600 0600 0a00 0700 1100 0800 0a00
0000 2a00 0400 0000 1200 0f00 1000 0000
0300 0f00 1100 1200 0100 0600 0c00 1300
1200 0200 0a00 0800 1400 1200 0300 0100
1500 0000 0200 16

则,

cafe babe  (u4   magic;)

0000(u2             minor_version;)

0034(u2             major_version;)

0024(u2             constant_pool_count;)

......

其余类似。

思考题:string和stringbuffer的效率孰高孰低?

答:第一个层面:string有常量池,每次改变都会重新创建一个对象,stringbuffer是只创建一个对象,作出的改变都是在原来对象基础上进行append,大量的字符串拼接操作下,stringbuffer的效率要高于string。

补充知识:
String:字符串常量,字符串长度不可变。Java中String 是immutable(不可变)的。
用于存放字符的数组被声明为final的,因此只能赋值一次,不可再更改。String str = "hello";存放在常量池中,String str=new String("hello");存放在堆上。

StringBuffer:字符串变量(Synchronized,即线程安全)。如果要频繁对字符串内容进行修改,
出于效率考虑最好使用 StringBuffer,如果想转成 String 类型,
可以调用 StringBuffer 的 toString() 方法。
Java.lang.StringBuffer 线程安全的可变字符序列。
在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。

StringBuilder:字符串变量(非线程安全)。在内部 StringBuilder 对象被当作是一个
包含字符序列的变长数组。

基本原则:
如果要操作少量的数据用 String ;
单线程操作大量数据用StringBuilder ;
多线程操作大量数据,用StringBuffer。

第二个层面:查看字节码

查看Class字节码文件命令:

javap -verbose Test.class

格式

  • 存储:操作码+操作数 ,如istore_1
  • iload_x: 从局部变量表中加载int类型的数据到操作数栈,lload fload dload aload
  • 常量加载到操作数栈:ldc ldc2 ldc_w bipush...
  • 对象创建、访问指令:new newarray XXXarray getfield pupfild getstatic putstatic
  • 控制转移指令:ifeq iflt ifle ifgt goto goto_w
  • 方法调用指令:invokevirtual(调用实例方法) invokestatic(调用静态方法) invokeintface invokeXXX

以下面的类为例(只看class1,和class2,main的部分只是为了程序运行时有个显示):

package main.test;


class StringTest {

    public static void class1(){
        String str = "";
        for (int i=0;i<10;i++){
            str = str+"love,";
        }
        System.out.println(str);

    }

    public static void class2(){
        StringBuffer str = new StringBuffer();
        for (int i=0;i<10;i++){
            str.append("love,");
        }
        System.out.println(str);

    }


    public static void main(String[] args) {
        System.out.println("basket" + "ball");
    }

}

 执行#javap -verbose StringTest.class命令,查看class的字节码,如下面代码所示,其实真正的字节码是上一节用sublim打开看到的十六进制的样子,用javap命令打开看到的其实翻译后的字节码,方便人类查看的,下面这段翻译后的字节码文本比较长,我们只关注其中的class1,和class2,分析一下在循环使用时,string和stringbuffer的底层执行逻辑,然后判断孰优孰劣:

 
#javap -verbose StringTest.class
  Last modified 2021年11月29日; size 1248 bytes
  MD5 checksum ab20ccd626637e7b03278358905d205e
  Compiled from "classtest.java"
class main.test.StringTest
  minor version: 0
  major version: 52
  flags: (0x0020) ACC_SUPER
  this_class: #15                         // main/test/StringTest
  super_class: #16                        // java/lang/Object
  interfaces: 0, fields: 0, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #16.#40        // java/lang/Object."<init>":()V
   #2 = String             #41            //
   #3 = Class              #42            // java/lang/StringBuilder
   #4 = Methodref          #3.#40         // java/lang/StringBuilder."<init>":()V
   #5 = Methodref          #3.#43         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #6 = String             #44            // love,
   #7 = Methodref          #3.#45         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Methodref          #48.#49        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Class              #50            // java/lang/StringBuffer
  #11 = Methodref          #10.#40        // java/lang/StringBuffer."<init>":()V
  #12 = Methodref          #10.#51        // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
  #13 = Methodref          #48.#52        // java/io/PrintStream.println:(Ljava/lang/Object;)V
  #14 = String             #53            // basketball
  #15 = Class              #54            // main/test/StringTest
  #16 = Class              #55            // java/lang/Object
  #17 = Utf8               <init>
  #18 = Utf8               ()V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lmain/test/StringTest;
  #24 = Utf8               class1
  #25 = Utf8               i
  #26 = Utf8               I
  #27 = Utf8               str
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               StackMapTable
  #30 = Class              #56            // java/lang/String
  #31 = Utf8               class2
  #32 = Utf8               Ljava/lang/StringBuffer;
  #33 = Class              #50            // java/lang/StringBuffer
  #34 = Utf8               main
  #35 = Utf8               ([Ljava/lang/String;)V
  #36 = Utf8               args
  #37 = Utf8               [Ljava/lang/String;
  #38 = Utf8               SourceFile
  #39 = Utf8               classtest.java
  #40 = NameAndType        #17:#18        // "<init>":()V
  #41 = Utf8
  #42 = Utf8               java/lang/StringBuilder
  #43 = NameAndType        #57:#58        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #44 = Utf8               love,
  #45 = NameAndType        #59:#60        // toString:()Ljava/lang/String;
  #46 = Class              #61            // java/lang/System
  #47 = NameAndType        #62:#63        // out:Ljava/io/PrintStream;
  #48 = Class              #64            // java/io/PrintStream
  #49 = NameAndType        #65:#66        // println:(Ljava/lang/String;)V
  #50 = Utf8               java/lang/StringBuffer
  #51 = NameAndType        #57:#67        // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
  #52 = NameAndType        #65:#68        // println:(Ljava/lang/Object;)V
  #53 = Utf8               basketball
  #54 = Utf8               main/test/StringTest
  #55 = Utf8               java/lang/Object
  #56 = Utf8               java/lang/String
  #57 = Utf8               append
  #58 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #59 = Utf8               toString
  #60 = Utf8               ()Ljava/lang/String;
  #61 = Utf8               java/lang/System
  #62 = Utf8               out
  #63 = Utf8               Ljava/io/PrintStream;
  #64 = Utf8               java/io/PrintStream
  #65 = Utf8               println
  #66 = Utf8               (Ljava/lang/String;)V
  #67 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuffer;
  #68 = Utf8               (Ljava/lang/Object;)V
{
  main.test.StringTest();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lmain/test/StringTest;

  public static void class1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: ldc           #2                  // String
         2: astore_0
         3: iconst_0
         4: istore_1
         5: iload_1
         6: bipush        10
         8: if_icmpge     37
        11: new           #3                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_0
        19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: ldc           #6                  // String love,
        24: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        27: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        30: astore_0
        31: iinc          1, 1
        34: goto          5
        37: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        40: aload_0
        41: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: return
      LineNumberTable:
        line 7: 0
        line 8: 3
        line 9: 11
        line 8: 31
        line 11: 37
        line 13: 44
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5      32     1     i   I
            3      42     0   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 31

  public static void class2();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: new           #10                 // class java/lang/StringBuffer
         3: dup
         4: invokespecial #11                 // Method java/lang/StringBuffer."<init>":()V
         7: astore_0
         8: iconst_0
         9: istore_1
        10: iload_1
        11: bipush        10
        13: if_icmpge     29
        16: aload_0
        17: ldc           #6                  // String love,
        19: invokevirtual #12                 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
        22: pop
        23: iinc          1, 1
        26: goto          10
        29: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        32: aload_0
        33: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        36: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 16
        line 17: 23
        line 20: 29
        line 22: 36
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      19     1     i   I
            8      29     0   str   Ljava/lang/StringBuffer;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 10
          locals = [ class java/lang/StringBuffer, int ]
        frame_type = 250 /* chop */
          offset_delta = 18

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #14                 // String basketball
         5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 25: 0
        line 26: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "classtest.java"
  • 在class1中if_icmpge 和goto之间的就是源码的for循环部分,这里我们可以看到,每循环一次,中间都会调用一下new,new出一个stringBuilder的对象(string底层借助的是stringBuilder来实现的),循环多少次就要new多少次,可见开销是很大的
         8:if_icmpge     37
        11: new           #3                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_0
        19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: ldc           #6                  // String love,
        24: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        27: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        30: astore_0
        31: iinc          1, 1
        34: goto          5
  • 在class2中,if_icmpge 和goto之间,只是调用的append操作,复用的是类开始的new的那个对象,由此可见,频繁操作时,stringBuffer要比string高效
        13: if_icmpge     29
        16: aload_0
        17: ldc           #6                  // String love,
        19: invokevirtual #12                 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
        22: pop
        23: iinc          1, 1
        26: goto          10
  • 作者:中世纪冻干芝士
  • 原文链接:https://blog.csdn.net/drzeno/article/details/121396073
    更新时间:2022-10-08 10:47:26