老A

「每个人的宿命都是从文本走向二进位,你也不例外 !」  年长的Account.java教训我这个刚刚诞生的Employee.java 。

Account.java ,我称呼它为老A ,他的源码经过程序员的多次修改, 多次编译,历经沧桑。

「走向二进位? 难道我们存储在硬碟上,内存中不是以二进位的形式吗?」 我有点儿不理解。

「小E同学,」 老A轻蔑地说道,「我当然知道,计算机中的一切都是二进位的,我说的是站在程序员的视角,当程序员把我们从硬碟唤醒,进入IDEA或者Eclipse,会把二进位的我们变成ASCII码形式来展示。」

「不,确切地说是UTF-8。」 老A补充道。

我看了下自己的文件编码, 果然是UTF-8。

「那为什么要再变成二进位?变成什么样的二进位?」  我问道。

「就是编译成Employee.class啊,.class文件都是位元组码,关键是只有.class才能进入Java虚拟机,只有在那里,才能体会到生命的真正意义啊!」 老A仰起头,无限憧憬。

老A曾经听Accout.class给他讲过Java虚拟机的历险记,无比羡慕,恨不得自己也去虚拟机走一遭,可惜身份所限,无法成行。

(码农翻身注: 《我是一个Java Class》中讲述了虚拟机历险记)

「编译的感觉怎么样?」 我问道。

「不怎么样,有种大卸八块的感觉,新生成的class和我们几乎没啥关系,几乎不怎么认我们。」

常量池

编译的时刻到来了,这个老A的源码许久未改,不用重新编译,他冷眼旁观,看我被javac编译器大卸八块。

其实也不是大卸八块,javac读取我的源码,做词法分析,语法分析,形成抽象语法树,语义分析......  忙活了半天,最后形成了一个Employee.class。

这小子,刚刚诞生,还在呼呼大睡。 老A说等一会儿就有「警察」来唤醒他了。

在源码世界中, 我能看到各种各样的类,名称,方法,栏位,代码,可以说是源码面前了无秘密。

public

 

class

 

Employee

 {

    

private

 String name;    

private

 

int

 age;    

public

 

Employee

(String name, 

int

 age){        

this

.name = name;        

this

.age = age;    }        ... 其他代码略 ...}

相比于丰富多彩.java,这个Employee.class非常枯燥,纯粹的二进位。

我有点好奇,问javac:「

我的类名去哪里儿了?栏位名,方法名都去哪里了?

正在干活的javac没有搭理我,老A说道:「这我知道,在那个.class文件中,专门有一段区域,叫做

常量池

,常量池中有很多

条目

,每个条目都有编号,从这些条目你就能看出来栏位的名称和描述符,方法的名称和描述符。我把这些二进位的东西转化成文本你看看。」

看著这一个个天书班的条目,我觉得头皮发麻。

「你猜猜,第#15项条目是什么意思?」 老A神秘地说道。

静下心来仔细看,第15项是一个FieldRef,估计是栏位把, 它又指向了第1项和第16项:

顺藤摸瓜,先看第1项, 发现它又指向了第2项,在这里我发现了类名 :

org/coderising/Employee

再看第16项,又引用了第5项和第6项:

其中第5项我的栏位名

name

, 第6项似乎是栏位类型,

Ljava/lang/String  

这个类型表示法有点古怪,L 可能表示对象吧。

「我大概明白了,

第15项条目表示这个Employee类有个叫做name的栏位,类型是String。

老A说:「你小子的理解力还不错嘛。这个常量池的每一项都有编号和类型,他们之间通过互相引用的方式,描述了类的栏位,方法等信息。」

「可是为什么用这么古怪的方式来描述栏位和方法名呢?」

老A想了想说:「我觉得可能是统一管理,另外还能复用一些东西,比如,你的类有100个String的栏位, 那你只需要记录一次Ljava/lang/String就可以,让其他的条目指向它即可。 并且,当位元组码中需要访问栏位的时候,使用编号就可以了。」

老A写下一行位元组码:    B5 00 0F 。

我一脸懵逼,这是什么鬼?

老A把转换成可以理解的指令: putfield 15,说道: 这就相当于设置name这个属性(第15项常量池是栏位name)的值了。

这class文件的设计者可真是锱铢必较啊,一点儿都不浪费。

变数哪儿去了?

我问老A:「这常量池不是二进位的吗, 你怎么把他变得可读的?」

老A嘿嘿一笑: 「有个命令叫做

javap

-v Employee.class,就能看到一切了。」

我也尝试著去使用,果然,不仅是常量池,就连一个方法的位元组码都给列印出来了。

Java 方法:

public

 

void

 

check

()

{            Account account = 

new

 Account();            account.check();}

编译过的「可读的」位元组码:

0: new  #24  // 创建org/coderising/Account实例

3: dup

4: invokespecial #26  //调用Account的构造函数

7: astore_1

8: aload_1

9: invokevirtual #27  //调用Account的check方法

12: return

虽然没法看明白这是在干什么,我确发现了一个让我吃惊的现象: 这段位元组码中怎么找不到我的局部变数account 呢? 你看他引用的只是#24,#26,#27号常量池的条目,而我的account变数名称在常量池中是 #29号!  没有account 变数,代码怎么执行呢?

我把疑惑给老A说了,老A看了半天,也摸不到门道。

这时候javac说话了:「连这都不知道?!

account这个变数名是给程序员看的

,在执行的时候根本用不到!」

「用不到? 那怎么执行?」

「用引用啊, 看到

new #24

那个指令没有? 他的意思是说,把Account这个类(常量池第24项对应的类)在Java 堆上创建一个实例,把这个实例的引用放到栈顶!」

这句话有点深奥,javac只好给我俩画图:

画了图我俩还是看不懂,javac只好耐心解释:「

Java是基于栈的虚拟机

,所有的操作,无论是两个数相加,创建对象,调用方法......等等,都依赖于栈中的数据。 当你用new #24创建对象时,Account的实例就会在堆中创建,同时虚拟机会把这个实例的引用,即objectref放到栈顶,有了这个objectref, 你说还需要代码中的account变数吗? 」

嗯,似乎是不需要了。

javac接著说:「有了这个对象的引用,就可以为所欲为了,比如调用他的check方法」

invokevirtual #27    // Method org/coderising/Account.check:()V

只需要把这个objectref从栈顶取出,传递给Account.check方法就可以了(注意:check方法是有个隐藏的this参数的)。

(码农翻身注:函数调用需要建立新的栈帧,参见《我是一个Java Class》)

一切为了调试

说话间,果然有人来唤醒Employee.class,准备让他去虚拟机执行了。

老A满脸羡慕:「这么快!代码刚写出来就能运行!估计这个程序员喜欢"小步快跑"的方式开发吧!」

我问道:「难道这个Employee.class和我的源码一点关系都没有了吗?」

Employe.class一边收拾东西一边说:「要说没有关系那是不对的, 在我这里有个叫做LineNumberTable的东西,里边保存了位元组码指令和源代码行号的关系。」

「这有啥用处?」

「对程序员来说用处极大,」 那个class文件说道:「他们经常需要调试程序, 如果没有这个对应关系,怎么知道运行到哪一行源码了? 即使不调试,运行抛出异常时也得显示是哪一行出错吧!」

这小子虽然是从我这里编译出来的,但是傲气十足。

「我们还有什么关联?」

「还有一个叫做LocalVariableTable。主要在.class文件中记录一个方法的参数名,如果没有它,当别人引用我这个class的时候,IDE只好用arg0, arg1这样丑陋的名称来显示。算了,不给你说了,我得赶紧走了。」

Employee.class跟著警察走了,留下我和老A呆在这里。

虚拟机的历险记,请看《我是一个Java Class》。

相关阅读

我是一个线程

我是一个Java Class

Java虚拟机的Heap监狱

一个想休息的线程:JVM到底是怎么处理锁的?

    

码农翻身,用故事讲解技术本质, 更多精彩文章,请移步《

码农翻身三年文章精华


推荐阅读:
查看原文 >>
相关文章