http://www.cnblogs.com/liupeizhi/articles/1942764.html
1.类的加载:
在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的运行时数据区了,也就是他的存储结构,下面,我会用图示的方法来解释它。
我们的JVM将运行数据区分为如下几块
堆:用来存储我们创建的对象的地方
栈:JVM会为每个线程创建一个栈,用来存储局部变量和操作数,栈跟栈之间不能通信。存储单位是栈帧。我们每调用一个方法,就新建一个栈帧压入栈中。栈帧之间是屏蔽的,这就是为什么一个方法无法访问另一个方法中的变量。栈帧由局部变量区(用数组实现),操作数栈(栈结构),帧数据(主要用来支持对类常量池的解析,方法的正常返回,异常处理)
方法区:用来保存Class类型数据
JVM的内存主要结构就这么多了。
好了,我们接着说,也许你现在对这张图还有很多疑问,稍后你就会明白了。我们的类现在已经通过验证了,校验器告诉我们它符合我们的Class结构,而且在方法区域为他分配了空间,我们非常高兴。下面就是关乎初始化问题了。有人会问,不对还有解析呢。呵呵,在写程序我们也知道了,这个阶段是可选的,也就是说你可以让你的类加载后马上初始化,也可以加载完毕不进行初始化。在这里我们要让我们的类初始化,下面即使解析阶段了。
解析阶段的主要任务:将类变量的符号引用解析成直接地址引用。就拿我们的变量a来说,他的值是"hello",这个东西在Class中只是一个符号而已。然而,我们的Class需要将所有的常量都存放在常量池中,所以hello会被存储到常量池中,然后提供一个入口地址给a。a 就能直接引用它了。这里得好好地理解理解。
我们的方法b引用的是一个方法,这个方法在Class中只是一个符号而已,这个方法的实际代码存放在一张表中,这张表我们成为方法表。我们的b就指向了方法表的一个引用。
解析完毕之后,就要初始化了,初始化很简单,就是执行静态代码块中的内容了。整个加载到此已经完毕,想必大家已经很清楚了吧。
然而,我们的JVM的任务才刚刚开始。
下面我们说一下对象的创建吧,想必这个问题在很多人看来都是很不解的。那么我们马上开始吧。
对象的实例是什么呢?在内存中的样子是什么呢。
如果你知道了方法区中的东西,对于对象也就不难理解了。对象就像一种结构,其中存储了指向方法的引用,实例变量,一个指向类常量池的引用(这就是为什么实例可以访问类变量和类方法)。这些数据按照一定的结构(就像Class结构一样,只是简单很多)存储在我们的堆区,这就是我们耳熟能详的对象。当我们new的时候,JVM就会按照上面的过程,在堆区为我们构造一个这样的数据结构,然后将块数据的引用存储到栈里面。稍后我们会细细讲解栈的结构
说到堆,我们不得不说JVM的内存管理机制,或者堆空间的垃圾处理机制。假如你自己写了一个JVM,你肯定会碰到这样一个问题,我们不断的在堆里面创建对象,再大的内存也有耗尽的时候,那我们如何进行垃圾处理呢。以前,在JDK1的时候采用的是对整个堆空间进行扫描,查找不再被使用的对象将其回收,可想而知这种策略是多么的低效。后来,我们聪明的java工程师给我们提供了这样的存储结构,他们将堆分为了两大部分,新生区和永久区。