一、软件开发进化史
摘自《从零开始学架构》
机器语言(1940年)
最早的软件开发使用的是“机器语言”,直接使用二进制码0和1来表示机器可以识别的指令和数据。
汇编语言(20世纪40年代)
为了解决机器语言编写、阅读、修改复杂的问题,汇编语言应运而生。汇编语言又叫“符号语言”,用助记符号代替机器指令的操作码,用地址符号(Symbol)或标号(Lable)代替指令或操作数的地址。
汇编语言编写依然非常复杂,同时,不同CPU的汇编指令和结构是不同的。
高级语言(20世纪50年代)
上世纪50年代开始出现了许多高级语言,最初的高级语言有如下几个,且这些语言至今还在特定的领域继续使用。
Fortran:1955年,名称取自“FORmula TRANslator”,即公式翻译器,由约翰巴科斯等人发明。
LISP:1958年,名称取自“LISt Processor”,即枚举处理器,由约翰麦卡锡等人发明。
Cobol:1959年,名称取自“Common Business Oriented Language”,即通用商业导向语言,由葛丽丝霍普发明。
高级语言让程序员不需要关注机器底层的低级结构和逻辑,而只关注具体的问题和业务即可。
第一次软件危机与结构化程序设计(20世纪60年代-20世纪70年代)
随着软件的规模和复杂度大大增加,20世纪60年代中期开始爆发了第一次软件危机,典型的表现是软件质量低下、项目无法如期完成、项目严重超支等。因为软件而导致的重大事故时有发生,如1963年美国水手一号火箭发射失败事故,就是因为一行Fortran代码错误导致的。
IBM的System/360操作系统开发,布鲁克斯作为项目主管,率领2000多个程序员夜以继日,共计花费5000人一年的工作量,写出近100万行代码,总共投入5亿美元,但项目进度一再延迟,软件质量无法保障。布鲁克斯的基于该项目经验总结而写的《人月神话》成为了畅销书。
为了解决问题,在1968年、1969年连续召开两次著名的NATO会议,会议正式提出“软件危机”一词,并提出了针对性的解决方案“软件工程”,但事实证明“软件工程”无法根除软件危机。
“结构化程序设计”作为另一种解决软件危机的方案被提出,Edsger Dijkstra于1968年发表了著名的《GOTO有害论》论文,引起了长达数年的论战,并由此产生了结构化程序设计方法。同时,第一个结构化的程序设计语言Pascal也在此时诞生,并迅速流行起来。
结构化程序设计的主要特点是抛弃goto语句,采取“自顶向下、逐步细化、模块化”的指导思想。结构化程序设计本质上还是一种面向过程的设计思想,但通过“自顶向下、逐步细化、模块化”的方法,将软件的复杂度控制在一定范围内,从而整体上降低了软件开发的复杂度。
第二次软件危机与面向对象(20世纪80年代)
结构化编程一定程度上缓解了软件危机,然而随着硬件的快速发展,业务需求越来越复杂,以及编程应用领域越来越广泛,第二次软件危机很快到来。第二次软件危机的根本原因是软件生产力远远跟不上硬件和业务的发展,主要体现在软件的”扩展“变得非常复杂。在这种背景下,面向对象思想开始流行起来。
面向对象思想早在1967年的Simula语言中就开始提出了,但第二次软件危机促进了该思想的发展。面向对象真正流行起来是20世纪80年代,主要得益于C++的功劳,后来的Java、C#把面向对象推向了新的高峰。
软件架构
20世纪90年代开始,软件架构的概念越来越流行。
二、类和对象
2.1 概述
类是构造对象的模板或蓝图,由类构造(construct)对象的过程称为创建类的实例(instance)。
Java编写的所有代码都位于某个类的内部。
语法
[public][abstract|final] class 类名称 [extends 父类名称] [implements 接口名称列表]{ 数据成员声明及初始化; 方法声明及方法体; }
类之间的关系:
依赖 uses-a
聚合 has-a
继承 is-a
对象的三个主要特征:
对象的行为(behave):可以对对象施加哪些操作,或可以对对象施加哪些方法
对象的状态(state):当施加那些方法时,对象如何响应
对象的标识(identity):如何辨别具有相同行为与状态的不同对象
对象变量
希望构造的对象可以多次使用,可以将对象存放在对象变量中,对象变量也叫引用变量。对象变量不是对象。
声明引用变量:
类名 引用变量名;
如
Clock clock;
此时,没有生成Clock对象,也没有建立引用。
初始化
引用变量名 = new <类名>();
如
clock = new Clock();
此时,创建了对象,对象变量引用了该对象。
也可以让这个变量引用一个已经存在的对象,即将一个已经存在的对象变量赋值给新的对象变量,两个对象变量将引用同一个对象。
对象变量可以显示设置为null。
2.2 类的构成
类中包含数据成员和方法成员
数据成员
语法
表示对象的状态,可是任意的数据类型。
实例变量
没有static修饰的变量称为实例变量,是属于对象的属性。
<实例名>.<实例变量名>
类变量
用static修饰,属于类,在整个类中(所有对象中)只有一个只,类初始化的同时被赋值。
<类名|实例名>.<类变量名>
方法成员
实例方法
类外,<对象名>.<方法名>([参数列表])
类内,方法之间直接用方法名进行调用
类方法
静态方法,不能声明为抽象的,可以类名直接调用,也可用类实例调用。
2.3 方法参数
Java程序设计语言总是采用按值调用,即方法得到是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。
方法参数有两种类型:
- 基本数据类型 变量
- 对象引用 变量
对象引用作为参数
C++中有两种参数传递的方式:值调用和引用调用
Java只有值调用(值传递),方法参数即使是对象引用变量,也是值传递,传到函数体内部的变量实际上是外部对象引用变量的拷贝,该拷贝和外部的对象引用同时指向同一个对象。
public class TestSwap { public static void swap(com.aidata.clazz.Person x, com.aidata.clazz.Person y) { com.aidata.clazz.Person temp = x; x = y; y = temp; } public static void main(String[] args) { Person a = new Person("Tom", 18); Person b = new Person("Jack", 20); swap(a, b); System.out.println(a); System.out.println(b); } }
并没有起到交换a、b的作用,结果为
Person [name=Tom, age=18]
Person [name=Jack, age=20]
因为函数体内的x、y只是a、b的拷贝,在里面交换,x从引用Tom变为引用Jack,y从引用Jack变为引用Tom,但是a依然引用Tom,b依然引用Jack
函数调用结束后,函数体内的x、y被销毁,a、b保持原状,因此起不到交换a、b的作用
可变长参数实际是数组
2.4 权限控制
类成员的控制权限
private | 无修饰 | protected | public | |
同一类 | 是 | 是 | 是 | 是 |
同一包中的子类 | 否 | 是 | 是 | 是 |
同一包中的非子类 | 否 | 是 | 是 | 是 |
不同包中的子类 | 否 | 否 | 是 | 是 |
不同包中的非子类 | 否 | 否 | 否 | 是 |
注:是代表可访问,否代表不可访问
protected的出现可以让不同包的子类使用父类的属性,主要用于继承中
无修饰的时候,是指包访问控制权限(friendly),也是默认的控制权限。当前包中的所有其他类对那个成员都有访问权限,但对于这个包外的类,这个成员是private。
类的访问权限
类即不可用是private的也不可以是protected的,只能是public或包访问权限的。
无修饰(默认) | public | |
同一包中的类 | 是 | 是 |
不同包中的类 | 否 | 是 |
2.5 构造器
有了类并不可直接使用,我们必须构造对象。
对象是有状态的,初始化后才能使用,我们使用构造器来构造对象,并为对象初始化。
构造器与类同名,这确保了不会与类成员名称冲突。
new 对象名() 时,将会为对象分配存储空间,并调用构造器,这确保了在操作对象之前已被恰当地初始化了。
无参构造器
不接受任何参数的构造器叫无参构造器,这是默认构造器,当你的类中没有提供任何构造器的时候,系统会提供一个默认的构造器。
当只有无参构造器的类构造对象的时候,构造器不会为实例域赋初值,你又没有显示赋值的话,实例域将会初始化为默认值,即数值型数据设置为0、布尔型数据设置为false、所有对象变量设置为null。
如果已经定义了一个构造器,无论是否有参数,编译器不会帮你自动创建默认构造器,此时,你可以在有参的构造器中为实例域设置初值。
方法重载Overload
让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。
每个重载的方法都必须有一个独一无二的参数类型列表,即方法名称相同但参数列表不同。
方法重写Override
子类对父类方法的实现过程进行重写,方法名、参数列表、返回值都不变。
2.6 this
this表示当前对象自身
如果有一个类型构造了两个对象,如何让两个对象都调用该类的同一方法?
把对象的引用作为参数传递给该方法,即可分辨是哪个对象调用的了。
如两个同类Some的对象a、b,调用了方法do(),a.do()、b.do()
实际上是这样运作的:Some.do(a)、Some.do(b)
为此,Java专门提供了this关键字。this关键字只能在方法内部使用,表示”对调用方法的那个对象“的引用。
在构造器中调用构造器
在构造器中,如果为this添加了列表参数,将产生对符合此参数列表的某个构造器的明确调用。通常用参数个数较少的构造方法调用参数个数较多的构造方法。
2.7 static
static方法就是没有thsi的方法,在static方法的内部不能调用非静态方法。静态方法是不能向对象实施操作的方法。用static的方法是类方法,用static的属性是类变量。
如果将属性(域)定义为static,每个类中只有一个这样的域,即所有对象共享该属性。
静态方法(static mehod)不能直接调用非静态方法(non-static method),可以通过一个对象的引用传入静态方法中,再去调用该对象的非静态方法。在主函数(static方法)中,经常创建某个类的实例,再利用其引用变量调用他的非静态方法。
public class staticMethodTest { void nonStaticMethod() { System.out.println("This is a non static method"); } static void staticMethod(staticMethodTest s) { System.out.println("This is a static method"); s.nonStaticMethod(); } public static void main(String[] args) { staticMethodTest obj=new staticMethodTest(); staticMethod(obj); } }
其实main函数就是静态方法,我们都要先创建对象,再通过对象调用方法,也不能直接调用非静态方法。
2.8 成员初始化
这里讨论的是没有继承的初始化
初始化数据域的三种机制:
- 在构造器中设置值
- 在声明中赋值
- 初始化块
即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化(当然,构造器中的赋值也被称为初始化,两者不必纠结)。想一想,这个顺序很合理的,变量没有这种初始化当然不可用,方法也就无法去操作它们,因此初始化之前方法也是无法正常运作的。
如果有一个类Dog
new Dog()的二个阶段
该句会调用Dog()的构造器,构造器实际上是静态方法,静态方法的调用会引发类的初始化,初始化是类的加载的一个阶段
类的加载 类加载的过程分为加载、验证、准备、解析和初始化等步骤。加载的准备阶段,为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。加载的最后阶段是类的初始化,类的初始化会使得static变量完成初始化。
对象创建 类加载完成,才实质进行new Dog()创建对象,此时在堆上为Dog对象分配足够的空间,这块存储空间会被清零,所以非静态的实例变量此时被设置成立默认值,然后执行所有出现在字段定义处的初始化动作。执行构造器。
发现,在分配空间的时候(静态变量的准备阶段,非静态变量对象创建阶段),空间中的变量(静态变量和非静态变量)都会设置为默认初始值
静态变量和非静态变量都可以看成经历了两次的初始化(如果有第二次的话),一是赋默认初值,二是执行Java代码中的赋值操作,静态变量初始化是在类加载中完成的,非静态变量是在创建对象的时候完成的。
从一道面试题来认识java类加载时机与过程
二、类的复用
我们千辛万苦写出了类,下次开发的时候绝对不想再从头到尾重新写一个除了个别地方不同大部分都很类似的类了。
是的,我们想重用类,这是非常基本的需求。
此方法的窍门是使用类而不破坏现有程序代码,大体有两种达到这一目的的方法:
- 一是,只需在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成,所以这种方法称为组合。这种方法只是复用了现有程序代码的功能,而非它的形式。
- 二是,它按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新的代码,这种方式被称为继承。
2.1 组合
设计新类时可选用已有类的对象作为它的成员,这就构成了一个组合机制,类的组合只需将类的引用置于新类中即可。
2.2 继承
隐藏与覆盖
对于从超类继承过来的属性和行为,如果子类定义了重名的属性则从超类继承的属性被隐藏,如果子类定义了一个和超类继承过来的方法原型完全一样的方法,那么从超类继承过来的方法就被覆盖了。
隐藏不是删除,父类中的属性和方法仍然是存在的。
属性的隐藏
子类声明了与超类中相同的成员变量名,则:
- 从超类继承的变量将被隐藏
- 子类拥有两个相同名字的变量,一个继承自超类,一个又自己声明
- 当子类执行继承自超类的操作时,处理的是继承自超类的变量,而当子类执行它自己声明的方法时,所操作的就是它自己声明的变量
- 那如何在子类声明的方法中访问父类被隐藏的属性呢?在子类的方法使用super.属性访问父类被隐藏的属性
方法的覆盖
如果子类不需要使用从超类继承来的方法,则可声明自己同名的方法,称为方法覆盖。
覆盖方法的返回类型,方法名称,参数个数及类型必须和被覆盖的方法一模一样,即只有方法的实现和访问权限是不同的
覆盖方法的访问权限可比被覆盖的方法更宽松,但不能更严格
使用super可调用父类被覆盖的方法
父类中被static和final修饰的方法不能被覆盖
方法的调用
调用x.f(args),隐式参数声明为类C的一个对象
- 编译器查看对象的声明类型和方法名。由于有可能存在多个名字为f,但参数类型不一样的方法,编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。编译器获得了所有可能被调用的候选方法。
- 编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配的方法,就选择这个方法,这个过程被称为重载解析。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。
- 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行,并采用动态绑定调用方法时,虚拟机一定调用与x所引用对象实际类型最合适的那个类方法。假设x的实际类型是D,但是被声明为C类,D是C的子类。如果D类定义了方法f,即使被声明为了C类,也直接调用实际类D中的方法f;否则,将在D类的超类C中寻找f,以此类推。
下面的例子就是,b实际类型是Beetle,但被声明为Insect,由于Beetle中有play方法,实际调用的还是Beetle中的play方法。
class Insect { static int printInit(String s) { System.out.println(s); return 47; } public void play() { System.out.println("Insect play."); } } public class Beetle extends Insect { public void play() { System.out.println("Beetle play."); } public static void main(String[] args) { Insect b = new Beetle(); b.play(); } }
结果
Beetle play.
每次调用方法都进行搜索,时间开销大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用方法。在正真调用方法的时候,虚拟机仅查找这个表就可以了。
子类构造器
由于子类的构造器不能访问父类的私有域,必须利用父类的构造器对这部分私有域进行初始化,可以通过super实现对超类构造器的调用,使用super调用构造器的语句必须是子类构造器的第一条语句。
基类的初始化
继承不只是复制父类的接口,当创建一个子类的对象时,该对象包含了一个父类的对象,用super表示指向该父类对象的引用,可以在子类中使用super来调用父类的方法。那父类对象是如何初始化的呢?子类构造器中调用了父类构造器,从而保证了父类的初始化。
构造器的执行是按照从父类到子类的顺序进行的,子类依赖父类,父类没有初始化,子类依赖的功能就缺失了。父类在子类构造器可以访问之前就已经完成了初始化。
如果子类构造器没有显示地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没显式地调用超类的其他构造器,则Java编译器将报告错误。
Object
所有类的超类
在Java中只有基本类型不是对象。
getClass方法
获取当前对象所属的类信息,返回Class对象
equals方法
在Object类中,这个方法将判断两个对象是否有相同的引用。
这其实是同一
想要判断两个对象是否相等(两个对象有相同的类型和相同的属性),就不能直接使用从Object类里继承的equals方法,要重写该方法
String类中就覆盖了equals方法,因此可以比较两个字符串是否相等
hashCode方法
该方法得到散列码,每个对象都有一个默认的散列码,其值为对象的存储地址。
如果依照equals方法判定两个对象是相等的,则在这两个对象上调用hasCode方法应该返回同样的整数结果。
final
可能使用final的三种情况:数据、方法和类
final类
阻止继承
不允许扩展的类称为final类,定义类的时候使用了final修饰符就表明这个类是final类。
final数据
Java中,常量必须是基本数据类型,并且以关键字final表示。在这个常量进行定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本类型运用final时,final使引用恒定不变,一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径。
空白final
被声明为final但又未给定初值的域。空白final在使用前必须初始化,只是没有在声明处初始化。
final参数
基本类型的参数被指定为final时,可以读参数但是无法修改参数。
参数引用,无法在方法中更改参数引用所指向的对象。
final方法
使用final方法,可以把方法锁定,防止任何继承类修改它的含义。
强制类型转换
将一个值存入变量时,编译器将检查是否允许给操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能通过运行时的检查。
举例
class Insect { static int printInit(String s) { System.out.println(s); return 47; } public void play() { System.out.println("Insect play."); } } public class Beetle extends Insect { public void play() { System.out.println("Beetle play."); } public void run() { System.out.println("Beetle run."); } public static void main(String[] args) { Beetle a = new Beetle(); Insect b = a; b.play(); // b.run(); Beetle c = (Beetle) b; c.run(); } }
结果
Beetle play.
Beetle run.
Beetle对象的引用赋给父类Insect类的变量是被允许的
但是只能使用共有的功能play,这个play是真实Beetle的play
但是没法使用Beetle专有的run方法
为了恢复变量b本该具有的Beetle的功能,要进行强转
将超类Insect的引用b赋给子类Beelte变量c的时候,必须进行强制转换
为什么可以调用play不能调用run呢?
暂时猜想是:
b被声明为Insect,因此通过b调用方法的时候,还是先向Insect及其父类中找的,找到了后,后期动态绑定机制才会运行,才可以调用Bettle的play
但是Insect及其父类中没有run方法,因此去查找方法的时候就报错了,后期动态绑定也就谈不上了
在将超类转换成子类之前,使用instanceof进行检查
2.3 继承与初始化
和没有继承的过程类似,也可以分两个阶段
- 加载 加载的过程从子类开始,若发现基类,会加载(这里应是指类加载过程的第一个阶段的那个加载)基类,一直到所有基类的加载完。这个过程中,各类加载应该紧跟着检验和准备阶段,此时各方法区中应该有了各静态变量,且值为零值。然后,从根基类开始,进行static初始化,一直到最下面的子类。因为子类的static初始化可能依赖基类成员的初始化,因此采用这种顺序。
类加载过程的第一阶段——加载
该阶段完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
也就是在取得二进制.class文件,方法区为类的信息中开辟空间
方法区存储加载类的信息、常量、静态变量、编译后的代码等数据
类加载的第三个阶段——准备
该阶段在方法区内开辟新的空间,存放静态变量,并设置默认初始值
- 对象创建 所有的类加载完毕,可以进行对象创建了。首先,堆上开辟空间,所有非静态变量都被设为默认值,这是通过将对象内存设为二进制零值而一举生成的。然后,基类的构造器会被调用。基类构造器完成后,实例变量按次序进行初始化。最后,构造器的其余部分被执行。
非静态变量是推荐在构造器中进行赋值的,非静态变量的初始化可能讨论的比较少吧。
举例
class Insect { private int i = 9; protected int j; private int m = printInit("Insect.m initialize"); public Insect() { System.out.println("i= " + i + ", j= " + j); j = 39; } private static int x1 = printInit("static Insect.x1 initialized"); static int printInit(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect { private int k = printInit("Beetle.k initialize"); public Beetle() { System.out.println("k = " + k); System.out.println("j = " + j); } private static int x2 = printInit("static Beetle.x2 initialized"); public static void main(String[] args) { System.out.println("Beetle.main start"); Beetle b = new Beetle(); } }
结果
static Insect.x1 initialized static Beetle.x2 initialized Beetle.main start Insect.m initialize i= 9, j= 0 Beetle.k initialize k = 47 j = 39
图解:
总而言之,static早就进行了初始化,非static变量都要在构造器方法之前进行初始化,构造器方法是按从父类到子类的顺序执行,各层次的构造器执行之前先完成本层次非static变量的初始化
没变量,方法是无法运作的,先初始化是自然的
2.4 抽象类
abstract修饰的类
一些方法在抽象类中是无法具体化的,在子类中才能根据子类的实际情况提供足够的信息来产生具体的方法。
抽象类也是类,因此除了抽象方法之外,和正常类的构成没有区别,可以包含类常规类能包含的任何成员。
可以包含具体的属性和具体的方法, 即使不含抽象方法,也可以将类声明为抽象类。抽象方法充当着占位的角色,它们的具体实现在子类中。
可以定义抽象类的对象变量,但是只能引用非抽象子类的对象(抽象类不能实例化,想引用也引用不了),通过该对象变量调用的方法是被引用的非抽象子类的方法,如果一个方法没有在抽象类中定义,而在非抽象子类中定义,不能通过该对象变量调用该非抽象子类中的方法,编译器只允许调用在类中声明的方法。
三、类的加载和执行
JVM体系结构与工作方式
指令集是在CPU中用来计算和控制计算机系统的一套指令的集合,它是体现CPU性能的一个重要标志。那指令集与汇编语言又有什么关系?指令集是可以直接被机器识别的机器码,它必须以二进制格式存储在计算机当中,而汇编语言是能够被人所识别的指令,它在逻辑和顺序上是与机器指令一一对应的。
每个运行的Java程序都是一个JVM实例,JVM和实体机一样也有一套合适的指令集,这个指令集能够被JVM解析执行,这个指令集我们称为JVM字节码指令集。符合JVM规范的class文件字节码都可以被JVM执行。
一个程序要运行,需要经过一个编译执行的过程:
Java的编译程序就是将Java源程序 .java 文件 编译为JVM可执行代码的字节码文件 .calss 。Java编译器不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由解释器在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址。这样就有效的保证了Java的可移植性和安全性。
类的加载就是把.class文件搞进内存里,各种东西放到方法区、堆内存等等地方
编译完之后就需要加载进内存,然后执行
类的加载就是将类的.class文件中的二进制数据读进内存之中,将其放进JVM运行时内存的方法区中, 然后在堆中创建一个java.lang.Class对象,用于封装在方法区中类的数据结构,然后 根据这个Class对象,我们可以创建这个类的对象,对象可以有很多个,但是对应的Class对象只有这一个。
这个Class对象就像是一面镜子一样,可以反射一个类的内存结构,因此Class对象是整个反射的入口, 可以通过 (对象.getClass(), 类.Class, 或者Class.forName( 类名的全路径:比如java.lang.String) 三种方式获取到, 通过class对象,我们可以反射的获取某个类的数据结构,访问对应数据结构中的数据,这也是反射机制的基本实现原理。
加载 .class 文件有几种途径
1. 可以 从本地直接加载(加载本地硬盘中的.class文件)
2. 通过网络下载 .class 文件,(java.net.URLClassLoader 加载网络上的某个 .class文件)
3. 从zip,jar 等文件中加载 .class 文件
4. 将java源文件动态的编译为 .class 文件(动态代理)
字节码加载进来就要执行字节码指令了,jvm执行引擎在执行Java代码的时候有解释执行(通过解释器)和编译执行(通过即时编译器产生本地代码执行)两种选择。
执行引擎是JVM的核心部分,执行引擎的作用就是解析JVM字节码指令,得到执行结果。执行引擎也就是执行一条条代码的一个流程,每个Java线程都是一个执行引擎的实例。执行引擎一般有两种执行方式:
直接解释执行:解释器会将代码一句一句的进行解释,每解释一句就运行一句,在这个过程中不会产生机器码文件。 JIT(即时编译器)执行:先编译再执行,需要编译器先将字节码编译成机器码,然后再进行执行,该过程会产生机器码文件.
在实际JVM当中会对代码进行分类判断,对于热点代码(多次调用的代码和多次执行的循环体)做编译,非热点代码做解释执行。
VM执行字节码指令是基于栈的架构,也就是所有的操作数都必须先入栈,然后再根据指令中的操作码选择从栈顶弹出若干个元素进行计算之后再压入栈中。这就和一般的基于寄存器的操作有所不同,一个操作需要频繁的入栈和出栈,如果是基于寄存器(CPU的存储器,读取数据速度较快)的话不需要这么多的移动数据的操作,那么为什么JVM仍然选择基于栈的架构呢?
主要的原因就是JVM要设计成平台无关的,而平台无关性就是要保证再没有或者很少的寄存器的机器上也要同样能正确执行Java代码,而基于寄存器的架构过于依赖于寄存器本身的硬件特性,虽然基于寄存器可能会提高性能,但很大程度上牺牲了跨平台的移植性,很难设计出一款通用的基于寄存器的指令,综上所述,JVM最终选择了基于栈的架构!
每当创建了一个新的线程时,JVM会为这个线程创建一个Java栈,同时会为这个线程分配一个PC寄存器,并且此时这个PC寄存器会指向这个线程的第一行可执行代码(随着线程的执行,PC寄存器保持指向当前正在执行的代码)。每当线程调用一个新方法时,会在这个栈上创建一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,如方法中定义的局部变量,正常方法返回以及异常处理机制等。JVM在调用某些指令时可能需要调用到常量池中的一些常量,Java对象和构造函数等都存储在所有线程共享的方法区和Java堆中。
下面介绍基于栈的字节码解释执行引擎:
操作数栈栈顶的500弹出来,保存到本地变量表1中
先入栈,再取出来,赋值到本地变量表中