JVM是我们每个 java 程序员的功底了。也许有很多程序员不清楚 JVM 的工作原理, 今天,在这里带着大家一起深入研究一下我们可爱的 JVM 。废话少说我们切入正题。
要了解JVM 首先必须了解类加载的过程
上图表示的就是我们的类的初始化过程。
下面我们对每个阶段进行深入的探索。
类的加载:
不要被“加载”唬掉,所谓类的加载其实就是将 .class文件读入到内存中。这时候有朋友要问了,我们写程序的时候没有加载过类啊,类是怎么进入内存的呢。这就牵扯到类的加载器了。
在java 中可分为四类加载器
引导类加载器 Bootstrap Loader 用来加载 %java_home%lib 下的核心类库像 String 、 Date 等等
扩展类加载器 Extension Loader 用来加载%java_home%lib/ext 下的扩展 api
系统类加载器 AppClassLoader 加载classpath 下面的类文件,我们的所有类文件默认都是由 它来加载的,怎么样,感觉亲切吧
用户自定义的类加载器
下面我们举例说明类的加载
public class A{
public static void main(String args[]){
B b=new B();
}
}
Public class B{
Public static String a="hello";
public static String b=getValue();
Static{
System.out.println("Hello World");
}
Public static void getValue(){
}
}
假如我们自己定义了一个名字为A 的类,当我们启动虚拟机即( java A )的时候,会创建一个 JVM 实例。现在我们就看类 B 的加载过程。由于 B 是被 A 引用 的,所以 B 是由 A 的 类加载器进行加载。在这里我们不得不说一下类加载器是比较孝顺的孩子,为什么这么说呢,因为类加载器在加载类的时候采用双亲委托机制。简单说就是在加载类 的时候,加载器会调用父类加载器来加载,父类再调用父类,依次类推。这个说起来比较抽象,我们这里给出源代码来表示一下其中的加载过程
public Class loadClass(String name){
ClassLoader parent=this.getClassLoader().getParent();
Try{
Class c=findLoadedClass(name);
//如果这个类没有被加载
If(c!=null){
//如果有父类加载器
If(parent!=null)
parent.loadClass(name);
Else
BootstrapLoader.loadClass(name)
}catch(FileNotFoundException ex){
//如果父类加载器找不到,就调用自己的 findClass 查找
this.findClass(name);
}
//如果这个类已经被加载
Else
Return c;
}
这代码是我自己写的,是对源代码的简化表示,不要直接拷贝使用,如果想要知道详细内容,建议参源码。这段可以完全清晰地表示出类加载器的调用关系了。但是里面有个问题,相信各位都会发现了,就是 BootstrapLoader.loadClass(name).BootstrapLoader 为什么不创建实例呢?因为 BootstrapLoader 并不是用 java 写的,是一个本地方法( native ),也就是说是用 c/c++ 或者其他语言编写的方法。为什么要这么做呢?主要是因为我们的类加载器也是类,如果他们都是用 java 实现,那么他们如何加载?所以, sun 给了我们一个引导类加载器用来加载其他的类加载器,之后我们才能用这些类加载器加载我们的类文件。这里我们说一下他们的父子关系。
我们自定义的类加载器的父类加载器是 AppClassLoader
AppClassLoader的父类加载器是 Extension Loader
Extension Loader的父类加载器是 Bootstrap Loader
当我们加载类B 的时候,由于没有指定它的类加载器,默认由 AppClassLoader 进行加载,调用 loadClass ()方法, AppClassLoader 发现它的 parent 不是 null ,就会调用父类加载器 (Extension Loader) 加载, Extension Loader 发现它的父母是 null (因为 BootstrapLoader 不是 java 写的,所以不会被 Extension Loader 访问到)于是就调用 BootstrapLoader 来加载,由于我们的 B 类是在我们的 classpath 中,所以必然会产生 ClassNotFoundException ,接着调用自己的 findClass 进行查找, ExtensionLoader 访问的是 %java_home%/lib/ext 下面的类,必然也无法找到我们的 B 。于是会在 AppClassLoader 中捕获到异常,然后接着调用 AppClassLoader 的 findClass 进行加载,结果找到了。
终于啊,经过这么复杂的递归调用和冒泡查找后找到了我们的类B 了。至于为什么要设计的这么复杂,直接加载不就完了吗,干嘛搞得这么难受。这主要是出于安全性的考虑。你想想,这个过程中总是由 Bootstrap 来加载核心类,假如你自己写了一个名字叫 String 的类,里面含有攻击性的代码,如果能加载成功,必然会导致其他依赖此类的类导致错误,整个 JVM 就会崩溃。然而这个类是无法加载到内存中的,因为类的加载总是由 BootstrapLoader 开始,当他发现已经加载了 String ,就不会再加载了,有效地保证了系统的安全性。类的加载过程基本就这样,下面贴出一段代码,自己实现的类加载器。
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String path="F:\\JSP\\ClassLoaderDemo\\classes\\";
private String fileType=".class";
@Override
public Class findClass(String name){
byte bytes[]=this.loadClassData(name);
return this.defineClass(name, bytes, 0, bytes.length);
}
//加载类数据,返回一个 byte 数组
public byte[] loadClassData(String name){
try {
FileInputStream fin=new FileInputStream(path+name+fileType);
ByteArrayOutputStream bout=new ByteArrayOutputStream();
int ch=0;
while((ch=fin.read())!=-1){
bout.write(ch);
}
return bout.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
//类加载器的测试类
public class TestLoader {
public static void main(String argsp[]){
MyClassLoader loader= new MyClassLoader();
try {
//在指定的目录中加载 HelloWorld.class 文件
Class myclass=loader.loadClass( "HelloWorld" );
//加载完毕后进行实例化 , 这个过程包含了对类的解析
myclass.newInstance();
System. out .println(myclass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
这段代码可以直接拷贝运行.
我们大篇幅的讲述了类的加载过程,这是jvm 运行的第一步,建议各位读到这里的时候在脑海中回顾一下类的整个加载过程,以便于理解下面我们要说的类的链接。如果你觉得理解的没有问题,我们继续说一下类的连接阶段。
当我们将类加载到内存中的时候,我们就需要对他进行验证。这里我们先介绍一下JVM 是如何进行虚拟的。
想必大家都知道我们的CPU 是由控制器,运算器,存储器,输入输出设备组成的,这些是我们程序运行所必需的硬件环境。你可以这样认为,我们的 JVM 为我们的 java 程序虚拟出了完整的一套运行环境,他的控制器由我们的 JVM 直接担任,像垃圾处理了内存分配了等等,运算器当然就是我们的 cpu 了,存储器是 jvm 的运行数据区,输入输出设备就是硬件设备了。这里你可以发现,我们的 java 是不能直接与硬件进行交互的,底层功能的实现需要通过本地方法进行实现,这就是我们的 java 跨平台的原因。我们的 JVM 会根据硬件环境的不同(这里主要是指 CPU 的指令集的不同),将我们的 class 文件解释成 cpu 可以识别的指令码,这样,我们的 CPU 就能够运行我们的 java 程序了,这就是 java 的伟大之处。更确切的说使我们 JVM 的伟大之处了,呵呵。
这里,你只需要大概的了解一下JVM 的原理就 OK 了,之后我们会细细的讲解。
我们现在再说说类的连接阶段。
当我们把类加载到内存之后,我们如何保证他的正确性呢,或者说我们如何保证加载进来的二进制码是不是符合我们的Class 类型的结构呢?关于结构,要细细说来需要很大的篇幅,这里你只需要这样理解他 Class 就像是类的模板一样,它包含类的所有信息,包括访问控制符号( public , private ,友好型)、是类还是接口,直接父类是谁,实现的接口有什么,以及字段信息,方法信息 ,还有一个常量池。你看看,这不就是我们类所包含的所有信息吗。他们按照一定的结构组织在内存中,我们把这样的一块内存结构称为 Class 。就是我们常说的类类型。
我们接着说,为了保证我们加载的二进制代码是Class 结构,所以我们需要进行校验,很多地方称为验证,我感觉称之为校验更为合适。我们的校验程序校验完毕,发现它是我们需要的 Class 结构的时候,就会通知 JVM 为我们 Class 在方法区域分配空间。
说道这里,我们又要说说我们JVM 的运行时数据区了,也就是他的存储结构,下面,我会用图示的方法来解释它。
这是我用 Excel表格画的,不太好看,凑合着用吧。
我们的JVM 将运行数据区分为如下几块
堆:用来存储我们创建的对象的地方
栈:JVM 会 为每个线程创建一个栈,用来存储局部变量和操作数,栈跟栈之间不能通信。存储单位是栈帧。我们每调用一个方法,就新建一个栈帧压入栈中。栈帧之间是屏蔽 的,这就是为什么一个方法无法访问另一个方法中的变量。栈帧由局部变量区(用数组实现),操作数栈(栈结构),帧数据(主要用来支持对类常量池的解析,方 法的正常返回,异常处理)
方法区:用来保存Class 类型数据
JVM的内存主要结构就这么多了。
好了,我们接着说,也许你现在对这张图还有很多疑问,稍后你就会明白了。我们的类现在已经通过验证了,校验器告诉我们它符合我们的Class 结 构,而且在方法区域为他分配了空间,我们非常高兴。下面就是关乎初始化问题了。有人会问,不对还有解析呢。呵呵,在写程序我们也知道了,这个阶段是可选 的,也就是说你可以让你的类加载后马上初始化,也可以加载完毕不进行初始化。在这里我们要让我们的类初始化,下面即使解析阶段了。
解析阶段的主要任务:将类变量的符号引用解析成直接地址引用。就拿我们的变量a 来说,他的值是 "hello", 这个东西在 Class 中只是一个符号而已。然而,我们的 Class 需要将所有的常量都存放在常量池中,所以 hello 会被存储到常量池中,然后提供一个入口地址给 a 。 a 就能直接引用它了。这里得好好地理解理解。
我们的方法b 引用的是一个方法,这个方法在 Class 中只是一个符号而已,这个方法的实际代码存放在一张表中,这张表我们成为方法表。我们的 b 就指向了方法表的一个引用。
解析完毕之后,就要初始化了,初始化很简单,就是执行静态代码块中的内容了。整个加载到此已经完毕,想必大家已经很清楚了吧。
然而,我们的JVM 的任务才刚刚开始。
下面我们说一下对象的创建吧,想必这个问题在很多人看来都是很不解的。那么我们马上开始吧。
对象的实例是什么呢?在内存中的样子是什么呢。
如果你知道了方法区中的东西,对于对象也就不难理解了。对象就像一种结构,其中存储了指向方法的引用,实例变量,一个指向类常量池的引用(这就是为什么实例可以访问类变量和类方法)。这些数据按照一定的结构(就像Class 结构一样,只是简单很多)存储在我们的堆区,这就是我们耳熟能详的对象。当我们 new 的时候, JVM 就会按照上面的过程,在堆区为我们构造一个这样的数据结构,然后将块数据的引用存储到栈里面。稍后我们会细细讲解栈的结构
说到堆,我们不得不说JVM 的内存管理机制,或者堆空间的垃圾处理机制。假如你自己写了一个 JVM ,你肯定会碰到这样一个问题,我们不断的在堆里面创建对象,再大的内存也有耗尽的时候,那我们如何进行垃圾处理呢。以前,在 JDK1 的时候采用的是对整个堆空间进行扫描,查找不再被使用的对象将其回收,可想而知这种策略是多么的低效。后来,我们聪明的 java 工程师给我们提供了这样的存储结构,他们将堆分为了两大部分,新生区和永久区。