YYW'S BLOG

知识的分享就是知识的获得
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一个Java程序的生死旅程

Posted on 2013-03-23 00:48  阿武  阅读(1026)  评论(3编辑  收藏  举报

  本章通过一个简单的例子介绍一个Java程序是从无到有,从启动到结束的过程。

  通过本文可以对Java虚拟机有一个感性的认识,了解Java虚拟机的体系结构和运作流程。

 

  第一步,编写了一个简单的计算器程序,代码如下:

public class SimpleCalculator {
	public int val;
	
	public SimpleCalculator(int initVal) {		
		this.val = initVal;
	}

	public SimpleCalculator add(int i) {
		val += i;
		return this;
	}
	
	public SimpleCalculator subtract(int i) {
		val -= i;
		return this;
	}
	
	public SimpleCalculator multiply(int i) {
		val *= i;
		return this;
	}
	
	public SimpleCalculator divide(int i) {
		val /= i;
		return this;
	}
	
	public int result() {
		return val;
	}
	
	public static void main(String[] args) {
		SimpleCalculator c = new SimpleCalculator(10);
		int rst = c.add(15).subtract(5).multiply(3).result();
		System.out.println(rst);
	}	
}

在这个程序里面我们实现了加减乘除的计算操作,在main函数里面我们用它来计算(10+15-5)*3这样一个算术题。

  第二步,通过javac编译器把这段程序编译成一个SimpleCalculator.class二进制文件,SimpleCalculator.class并不是一个可执行文件,它只是一个包含了Java虚拟机能读懂的一系统指令的文件,为了能节约流量跟提高装载速度,它排版紧凑,尽量不浪费一丁点空间,因为Java最初的设计就是可以实现从网络远端传输程序到本地来执行的语言。

  第三步,执行java SimpleCalculator命令来运行它了,结果被打印出来了。接下来才是本文的重点,我们将了解一个Java程序是如何被虚拟机执行的。

  通过java命令我们在本地机器上启动了一个Java虚拟机进程, Java进程读取参数发现入口是SimpleCalculator这个类的main函数,Java进程通过类装载器到CLASS PATH下去找SimpleCalculator这个类,终于在当前路径下找到,对于想了解类装载器如何装载类文件的细节可以在网上找到很多这方面的文章,包括如何去创建一个自定义类装载器。类装载器将找到的SimpleCalculator.class文件装载进来后需要对该文件做一系列的校验,包括class文件的结构检查,类型数据的语义检查,字节码验证和符号引用的验证,保证了这个class文件是正确且无害的。

  下面这张图展示了Java虚拟机的内部体系结构:

      

  执行引擎是Java虚拟机实现的核心,用于处理各种指令。

  PC寄存器用于存储线程下一次指令的地址和返回值地址,虚拟机为每个线程创建单独的PC寄存器。如果执行的是本地方法,PC寄存器的值为"undefined"。

  本地方法栈用于存储跟本地方法的相关数据,本地方法方面的内容不属于本文的范畴,知道这一点就行。

  方法区用于存储被装载类型的信息,例如我们之前装载的SimpleCalculator类的信息,如果该类包含了常量或静态成员同样保存在方法区中,所以我们使用一个类的静态成员或者方法时不需要创建该类的实例,因为方法区所保存的信息已经足够了。

  用于存储程序在运行中所创建的对象,例如说我们例子中main函数通过new操作创建了一个对象存放在这个堆空间中,我们把它取名为c,对象c保存指向方法区中SimpleCalculator类的信息的引用跟存储全局变量val的值。这里我们要区分变量和对象的概念,变量c表示的是对象c的引用,也就是保存了对象c的内存地址,变量c保存在Java栈中,对象c则是表示堆空间中的一个对象,这就涉及到另外一个问题,当我们把一个对象做为方法的参数传递过去的时候,Java是值传递还是引用传递,答案就是如果传的是对象那么传递的是引用的一个拷贝,也就是说传的是另外一个引用,但引用指向的对象还是同一个,如果在方法内部修改了该对象则会影响到外部的引用,但如果是指向另外一个对象,则不会影响方法外部的引用;如果传的是基本数据类型(byte, short, int, long, char, boolean, double, float ),因为这些类型的值是直接存放在Java栈中,不存储在堆中,所以它们没有所谓的引用,传递的是值的拷贝,如果在方法内部改变了该变量的值,方法外部的变量不会受影响。好吧,我知道你想说String,String是一个特殊的类,它属于第一种情况,但因为String的对象是不可变的,对String所做的修改都会创建一个新的String,比如往String后面追加其它String,那么新的String会被创建,引用会指向新创建的对象,所以还是符合第一种情况的,因为方法外部引用的String对象并没有改变,所以不会影响到外面引用的值,但看起来就像第二种情况把拷贝了一个新的值传进去一样,但实际上不是。

  Java栈以栈帧为单位保存了线程的运行状态,栈帧由局部变量区操作数栈帧数据区组成。局部变量区用于存储线程运行方法的局部变量的值或引用,操作数栈为线程的工作区,存储当前指令的数据和返回结果,帧数据区则用于保存方法的返回值跟异常信息。

  虚拟机为每个线程分配了自己的Java栈,由于方法的局部变量都保存在该Java栈的局部变量区中,这就解释了为什么一个只用到局部变量的方法是线程安全的。Java虚拟机只会对Java栈以帧为单位做压栈和出栈两种操作。当调用一个方法时,虚拟机通过读取方法区对应类型的方法信息分配了一个帧内存,压入Java栈中。在上面的例子我们调用了第一个方法是c.add(5),虚拟机创建一个栈帧并压入Java栈中,在局部变量区保存了参数i的值,val因为是全局变量,跟对象相关联,保存堆对象c中。线程调用add操作时,虚拟机将i的值和val的值压入操作数据栈中,执行iadd指令将这两个值弹出栈做加法运算,并把结果压入操作数栈中,再从操作数栈中弹出结果值存入堆对象c的val中,方法结束,栈帧从Java栈中弹出。同样的方式继续执行c.subtract(5), c.multiply(3), c.result()的操作。因为堆是所有线程共享的,所以对象c并不是线程安全的,如果有多个线程同时调用了add方法,后面执行的结果就会替换掉前面的。

  最后程序把结果打印输出到屏幕,main方法结束,没有任何可运行的非守护线程,java进程功成身退,操作系统回收占用的内存,旅程结束。

  关于堆和栈我们可以打个比方,假设Java进程所占的内存空间是一个房子,房子里住着四个人分别代表了四个线程,四个人都有属于自己的房间,那么栈空间就是属于自己的那个房间,大家互不干涉房间里的小秘密,那么客厅就是堆空间,存放了各种各样公共的东西,任何人都可以拿来用,那么就难免会出现争用电视机洗衣机等情况,这就是线程不安全。