1.编译顺序:
编译器 虚拟机 虚拟机
java源文件*.java------->字节码*.class------>类装载器--->执行引擎
一个.class文件只能包含一个类或接口。因此.java文件中定义了多少类,编译时就会生成多少.class文件(内部类不算)。
2.java程序可以选择两种方式访问底层系统,由程序员选择:
(1).通过java程序调用javaapi调用本地方法,访问底层系统,与平台无关。
(2).通过java程序直接调用本地方法,访问底层系统与平台相关。
本地方法即操作系统提供的方法。
3.类装载器:
装载java编译器编译好的字节码*.class和java api的字节码到方法区。
java有两种类装载器:
(1).启动类装载器:系统唯一,属于虚拟机的一部分,用特定语言编写(与虚拟机体层语言相通)使用默认方式装载类,主要用来装载核心类库
。
(2).用户自定义类装载器:可有任意多个,用java编写,属于java应用程序的一部分,能被编译成字节码,并被虚拟机所装载。
一个装载器装载一个类及其该类所调用的一切类,使他们相互联系,并形成一个命名空间(name space),每一个类装载器对应一个命名空间。
即java中名字空间的原理。
类装载器成线形排列,自底向上,顶部为启动类装载器。除启动类装载器外,其他类装载器都由用户实例化,用来装载不同的类。当要装载一
个类时,底部的装载器试图将该类交给父装载器装载,而该父类又试图交给他的父类装载,一直向上,直到启动类装载器。若父类装载器无法
装载,则交给子类装载器装载,子类装载能装载的部分,将余下部分交给他的子类,直到底部。如:
装载器a,b,c,d,e,f,启动
a--->b--->c--->d--->e--->f--->启动
当有一个类fun需要被装载时,他会一直上溯到顶部即启动类装载器。如果启动类装载器无法装载fun则交给f装载,f装载能装载的部分,将其
余部分交给e,然后一直这样下去。
如上所述,运行过程中每个类装载器装载的类形成一个运行时包,同一运行时包里的类可以互相访问,但不能访问包外部的类。
4.虚拟机的生命周期:
每个java程序都有自己的虚拟机实例,随着程序的产生和消亡而产生与消亡。
5.java程序运行时的内存结构:
程序空间分为方法区,堆,java栈,本地方法栈。
(1)方法区存放装载的类数据信息包括:
基本信息:
每个类的全限定名。
每个类的直接超类的全限定名(可约束类型转换)。
该类是类还是接口。
该类型的访问修饰符。
直接超接口的全限定名的有序列表。
每个已装载类的详细信息:
运行时常量池:存放该类型所用的一切常量(直接常量和对其他类型,字段,方法的符号引用),它们以数组形式通过索引被访问,是外部调用
与类联系及类型对象化的桥梁。它是类文件(字节码)常量池的运行时表示。(还有一种静态常量池,在字节码文件中)。
字段信息:类中声明的每一个字段的信息(名,类型,修饰符)。
方法信息:类中声明的每一个方法的信息(名,放回类型,参数类型,修饰符,方法的字节码和异常表)。
静态变量
到类classloader的引用:即到该类的类装载器的引用。
到类class的引用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。
(2)堆存放所有生成的对象及对象的实例变量。
(3)java栈以帧的形似存放本地方法的调用状态(包括方法调用的参数,局部变量,中间结果等)。每调用一个方法就将对应该方法的方法帧压入
java栈,成为当前方法帧。当调用结束(返回)时,就弹出该帧。编译器将原代码编译成字节码(.class)时,就已经将各种类型的方法的局部变
量,操作数栈大小确定并放在字节码中,随着类一并装载入方法区。当调用方法时,通过访问方法区中的类的信息,得到局部变量以及操作数
栈的大小。
java栈帧(即方法帧)由局部变量区,操作数栈,帧数据区组成。
局部变量区为一个以字为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问
。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态
方法。
操作数栈也是一个数组,但却是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参
数压栈,如将b和c压栈,然后由操作指令将他们弹出,并执行操作,此处由iadd指令将b和c弹出并相加,然后压入操作数栈(一系列均由iadd执
行)然后由i_storex指令将结果弹出,存到索引x指向的局部变量区数组内(此处索引x指向局部变量a)。
虚拟机将操作数栈作为工作区。
帧数据区处理常量池解析,异常处理等。
(4)本地方法栈:与调用的本地方法的语言相关,如调用的是一个c语言方法则为一个c栈。本地方法可以回调java方法。
若有java方法调用本地方法,虚拟机就运行这个本地方法。在虚拟机看来运行这个本地方法就是执行这个java方法,如果本地方法抛出异常,
虚拟机就认为是这个java方法抛出异常。
(5)执行程序时,通过对象的引用在方法区中查找装载的类,若还没有装载,则查找字节码(类名.class),并将其装载入方法区。
在执行过程中,虚拟机会将对象的符号引用(即对象名)替换为直接的指针,以提高访问速度。
(6)因此,大体可以表述为:
方法区:存储类包括接口的各种信息,字节码装载到此处。
java栈:存储被调用的方法的各种信息,只有调用该方法时,才会将该方法帧压入java栈。
堆:存储对象的信息,包括对象的实例变量,但不包括对象的方法。只有调用对象的方法时,才将方法帧压入java栈中。
6.java数据类型:
数值类型:
浮点类型:float double
整数类型:byte,short,int,long,char(int和char可以互换)。
引用类型:类类型,接口类型,数组类型。
7.java的引用类型:
引用与指针。
引用代表被引用的对象,它只是引用对象的代表,并不占用内存,也不能修改。如引用变量没有引用对象,则该引用变量=null。
指针存放对象的地址,它是一个变量,可以被修改,和其他变量一样,占用内存。
8.方法区
所有线程共享方法区,但为满足线程安全,方法区中每一个类必须被设定为临界资源,即同一时刻某一个类只能被一个线程访问。
9.类标识:
由于一个程序可以多次装载同一个类且该类可以存在于不同的名字空间中(即可由不同的装载器装载),因此必须将装载该类的装载器的标识加
上,才能唯一标识一个类。
10.对象
对象实例变量存储在堆中,对象符号引用则在常量池,方法属性表等可能出现的地方。通过对象的引用可以访问对象的实例数据和创建该对象
的类的数据。对象的引用指向堆中的对象。
实例结构有两种,见书本98页。
当调用对象的方法时,需要进行动态绑定。即,不能根据对象来确定需要调用的方法,而是根据对象的类数据来确定需要调用的方法。此时,
也需要通过对象的引用来访问类数据。
动态绑定就是在运行时才绑定,而不是在编译时绑定。
11.数组
数组也是类的对象。具有相同类型和维数的数组属于同一个类(不管长度只看维数)。数组的长度属于对象实例。多维数组也是一维数组。如二
维数组,即为一个一维数组,该一维数组的每个元素是一个数组的引用。数组和普通对象一样也存储在堆中。
数组名为数组的引用,通过索引即数组标号来访问数组内容。
12.异常
在java栈帧的帧数据区内保存有针对该方法的异常表的引用。异常表记载了该方法的字节码(*.class)受catch子句保护的范围(即try子句里的
字节码)。当某个方法抛出异常时,虚拟机在对应的异常表中寻找匹配的catch子句,并将控制权交给catch子句中的代码。
13.java执行引擎
实现平台无关性,以java方法帧里的操作数栈为中心,将局部变量数组当作cpu的寄存器。每操作一个数据都要压人操作数栈,然后返回至局部
变量区。java虚拟机规定强类型转换,即低精度可以隐式转换到高精度,高精度必须强制转换到低精度。
14.线程
线程即存在于进程中的某个执行体。
每个线程必须遵守对象锁定,线程等待和通知。
对象锁定使线程互斥的访问对象资源。等待和通知则是遵守线程合理调度以达到同一个目的。java对象通过指令集达到上锁目的,同过继承
object类的wait(),notify(),notifyall()方法来等待和通知。当某个线程调用某个对象的wait()方法时,该线程被阻塞,并加入到该对象的线
程阻塞队列中,直到另一个线程调用同一对象的通知方法,才能唤醒阻塞队列中的线程。
15.常量池
常量池用来存放类型的各种信息,包括类型的各种直接常量,和对其他类型,字段,方法的符号引用。
常量池分为两种,存储在.class字节码中的常量池和存储在方法区中的运行时常量池。
常量池以入口形式(类似于中断向量表)出现,每个入口都指向一个表,表中存储常量的信息。但从常量池的入口的标志位就可以判断对应的表
中存储的常量类型。
常量池入口以一个标志位开始,该标志位指示该常量的类型。每个入口对应一个表,该表以符号_info结尾,表中存放常量的压缩形式。
常量池除了存放直接常量外还容纳如下几种符号引用:
类和接口的全限定名。
字段名称和描述符(该描述符是一个指示字段类型的字符串。字段是一个类或接口的类变量或实例变量)。
方法名称和描述符(该描述符指示方法返回类型,参数类型,数量,顺序)。
运行时,虚拟机用常量池的全限定名和方法,字段的描述符来建立类与类的关系。
常量池仅仅是一个引用和描述符的集合,并不接受任何赋值操作。
所有对象的创建,方法和类变量的调用均要从常量池中获取信息,但实例变量的调用从堆里获得。(猜想)
符号引用是由虚拟机解析后得到具体的地址来使用。
常量池解析就是将常量池中的符号引用替换成直接引用。
当要使用某个类的方法或字段时,首先从常量池中找到该方法或字段的符号引用,然后进行解析,找到其物理地址。
把代码中出现的各种符号引用,类与类的联系,进行常量池解析,叫做动态连接。
16.常量池结构
常量池由很多狠多的单元组成,每一个单元都形如(入口|内容),访问常量池单元时通过索引找到入口,然后访问其内容。但有时单元的内容也
可能是一个常量池入口(比如类或接口的常量池单元,入口包含该类的符号引用即constant_class_info,而内容则指向一个
constant_utf8_info的常量池单元,该单元里存放了该类的全限定名)。而直接常量如int,float等,内容处就是常量的值。
17.方法区的结构
方法区存储所有关于类型,接口的信息。方法区包含:
常量池:存储类型的直接常量和所有的字段,方法,其他类型的符号引用(仅仅是引用,并不存放具体信息)。
字段信息:所有声明的字段(包括字段名,类型,修饰符)。
方法信息:所有定义的方法(包括方法名,返回类型,修饰符,方法的字节码,方法栈帧的大小,方法的异常)。
类变量信息:虚拟机在方法区中为所有类变量分配空间,以后的初始化,赋值等操作也在方法区中进行,以便为所有类实例共享。
为提高访问速度,虚拟机在方法区中为每个非抽象类设置了一个方法表,该表是一个数组,每个元素是一个方法的直接引用。当类的对象调用
方法时,就在方法表中搜索(抽象类没有实例,所以不用调用方法,所以没有方法表)。
18.堆
堆存放类的实例和数组(包括实例变量,指向对应方法区中类数据的引用)。
19.一个例子
class test{
public static void main(string args[])
{
string a=new string("hello");
string b=new string("hello");
string c="hello";
string d="hello";
}
}
则a==b返回false,c==d返回ture。
因为:==比较双方是否是同一个对象。
首先:
string a=new string("hello")
string b=new string("hello")
a和b分别各自新建了hello的对象和引用变量,即在堆中有两个hello,他们各自的引用是a和b。
而:
string c="hello"
string d="hello"
先建立一个字符串类实例hello,再建立两个字符串引用变量c和d,然后让c和d都指向开始建立的hello实例。因此c和d指向的是同一个对象。
本文地址:http://com.8s8s.com/it/it10514.htm