当前位置: 首页 > 科技观察

每个人的命运都是从文本走向二进制,你也不例外!

时间:2023-03-22 15:08:25 科技观察

老A《每个人的命运都是从文字变成二进制,你也不例外!》较旧的Account.java教会了我这个新生的Employee.java。Account.java,我称之为老A,它的源代码经过程序员多次修改编译,历经沧桑。“转二进制?我们不是存储在硬盘上,而是在内存中不就是二进制形式吗?”我不明白。“E同学,”老A轻蔑的说道,“我当然知道电脑里的一切都是二进制的,我说的是程序员的角度,当程序员把我们从硬盘唤醒,进入IDEA或者Eclipse的时候,将以ASCII格式显示二进制us。”“不,准确地说是UTF-8。”老A补充道。我看了一下自己的文件编码,果然是UTF-8。“那为什么又改成二进制?什么样的二进制?”我问。“编译成Employee.class,.class文件都是字节码,关键是只有.class才能进入Java虚拟机,才能体会到人生的真谛!”老A抬起头,**期待。老A曾经听Accout.class给他讲过Java虚拟机的奇遇。他很是羡慕,恨不得自己上虚拟机。遗憾的是,由于身份有限,他未能成行。“编译的感觉如何?”我问。“不好,感觉要拆了,新产生的班级几乎和我们没有关系,几乎认不出我们了。”编译常量池的时间到了。这个老A的源码好久没改了。重新编译,他冷眼旁观,看着我被javac编译器肢解。其实没什么大不了的,javac看我的源码,做词法分析,语法分析,形成抽象语法树,语义分析……折腾了半天,终于形成了一个Employee.class。这孩子才刚出生,睡的还很香。老A说稍后“警察”会来叫醒他。在源代码的世界里,我可以看到各种各样的类、名称、方法、字段和代码。可以说源码面前没有秘密。publicclassEmployee{privateStringname;privateintage;publicEmployee(Stringname,intage){this.name=name;this.age=age;}...其他代码略...}相比colorful.java,这个Employee.class很无聊,纯二进制。我有点好奇,问javac:“我的类名去哪儿了?字段名和方法名去哪儿了?”干活的javac没跟我说话,老A说:“我知道的,在那个.class文件里,有一个特殊的区域,叫做常量池。常量池里面有很多条目,每一个entry有编号,从这些entry中可以看到字段的名称和描述符,方法的名称和描述符,我把这些二进制的东西转成文本看看。看着天书班的参赛作品,我头皮发麻。“猜猜第15项是什么意思?”老A神秘的说道。静下心来仔细看看。第15项是FieldRef,它可能是一个字段。指向第1项和第16项:顺藤摸瓜,先看第1项,发现指向第2项。这里我找到了类名:org/coderising/Employee然后看第16项,引用了第5项和第6项:第5项是我的字段名,第6项好像是字段类型,Ljava/lang/String这种类型符号有点奇怪,L可能表示对象。“我大概明白了,第15条的意思是Employee类有一个字段叫name,类型是String。”老A说:“你的理解力还不错,这个常量池中的每一项都有编号和类型,它们通过相互引用来描述类的字段、方法等信息。”“可是为什么要用这么奇葩的方式来描述字段名和方法名呢?”老A想了想说:“我觉得可能是统一管理,有些东西可以复用。比如你的类有100个String字段,那么你只需要记录一次Ljava/lang/String,和让其他条目指向它。并且,当您需要访问字节码中的某个字段时,您可以使用该数字。”老A写了一行字节码:B5000F。我一头雾水,这是什么鬼?老A把它转化成一个可以理解的命令:putfield15,说:这相当于设置属性名的值(常量池的第15项是字段名)。这个类文件的设计者真是一分钱一分货,一点浪费都没有。变量去哪儿了?我问老A:“这不是常量池二进制吗?怎么让它可读?”老A笑道:“有个命令叫javap-vEmployee.class,你看都在这里了。”我也试了一下,果然,不仅是常量池,连一个方法的字节码都打印出来了javamethod:publicvoidcheck(){Accountaccount=newAccount();account.check();}编译“可读”字节码:0:new#24//创建org/coderising/Account实例3:dup4:invokespecial#26//调用Account的构造函数7:astore_18:aload_19:invokevirtual#27//调用Account的check方法12:return虽然看不懂这是干什么的,但是我确实发现了一个让我吃惊的现象:为什么我在这段字节码中找不到我的局部变量account?可以看到他只引用了#24的入口,#26,#27常量池,而我的account变量名是常量池中的#29不行!没有account变量,代码怎么执行呢?把我的疑惑告诉了老A,看了半天一时间,老A想不通了,这时候javac说:“连这个都不知道?!变量名account是给程序员用的,执行时根本不用!”“不用?如何执行它?”“使用引用,你看到新的#24指令了吗?他的意思是在Java堆上创建一个Account类(常量池第24项对应的类)的实例,并将这个实例的引用放在栈顶!”话有点深奥,所以javac只好给我们画了一幅图:我们还看不懂这幅图,javac只好耐心的解释:“Java是一个基于栈的虚拟机。所有的操作,无论是两个数相加还是创建一个对象,调用方法等,都依赖于栈上的数据。当你使用new#24创建对象时,会在堆中创建Account的实例,虚拟机会将这个实例的引用,即objectref放在栈顶。有了这个objectref,你说代码里还需要account这个变量”嗯,好像不需要了。javac接着说:“有了这个objectref,你就可以为所欲为了,比如调用他的checkmethod"invokevirtual#27//Methodorg/coderising/Account.check:()V只需要从栈顶取出这个objectref并传递给Account.check方法(注意:check方法有一个隐藏的这个参数)。(注意:函数调用需要创建一个新的栈帧,参考《我是一个Java Class》)调试的对话中,有人来唤醒Employee.class,准备让他去虚拟机执行”老A满脸羡慕:“好快啊!代码一写就可以运行了!我猜这个程序Employees喜欢用‘小步快跑’的方式开发!”我问:“难道这个Employee.class和我的源码没有关系?”Employee.class一边打包一边说:“它没关系错了,我这里有一个叫做LineNumberTable的东西,里面存放的是字节码指令和源代码行号的关系。”“这有什么用?”“这对程序员很有用,”类文件说:“他们经常需要调试程序。如果没有这样的对应关系,他们怎么知道运行的是哪一行源代码呢?就算不调试,跑起来抛出异常,也要显示出哪一行错了!”这小子虽然是我编的,但傲气十足。“还有什么办法?”“还有一个称为本地变量表。主要记录.class文件中某个方法的参数名。没有它,当其他人引用我的类时,IDE不得不用难看的名称显示它,例如arg0和arg1。算了,不跟你说了,我得赶紧走了。“Employee.class跟在警察后面,留下我和老A在这。【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号编码】点此查看该作者更多好文