JVM加载class文件的原理机制
1 JVM简介
JVM是我们Javaer的最基本功底了,刚开始学Java的时候,一般都是从“Hello World”开始的,然后会写个复杂点class,然后再找一些开源框架,比如Spring,Hibernate等等,再然后就开发企业级的应用,比如网站、企业内部应用、实时交易系统等等,直到某一天突然发现做的系统咋就这么慢呢,而且时不时还来个内存溢出什么的,今天是交易系统报了StackOverflowError,明天是网站系统报了个OutOfMemoryError,这种错误又很难重现,只有分析Javacore和dump文件,运气好点还能分析出个结果,运行遭的点,就直接去庙里烧香吧!每天接客户的电话都是战战兢兢的,生怕再出什么幺蛾子了。我想Java做的久一点的都有这样的经历,那这些问题的最终根结是在哪呢?—— JVM。
JVM全称是Java Virtual Machine,Java虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare不一样,那个虚拟的东西你是可以看到的,这个JVM你是看不到的,它存在内存中。我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备,那这个JVM也是有这成套的元素,运算器是当然是交给硬件CPU还处理了,只是为了适应“一次编译,随处运行”的情况,需要做一个翻译动作,于是就用了JVM自己的命令集,这与汇编的命令集有点类似,每一种汇编命令集针对一个系列的CPU,比如8086系列的汇编也是可以用在8088上的,但是就不能跑在8051上,而JVM的命令集则是可以到处运行的,因为JVM做了翻译,根据不同的CPU,翻译成不同的机器语言。
JVM中我们最需要深入理解的就是它的存储部分,存储?硬盘?NO,NO, JVM是一个内存中的虚拟机,那它的存储就是内存了,我们写的所有类、常量、变量、方法都在内存中,这决定着我们程序运行的是否健壮、是否高效,接下来的部分就是重点介绍之。
2 JVM的组成部分
我们先把JVM这个虚拟机画出来,如下图所示:
从这个图中可以看到,JVM是运行在操作系统之上的,它与硬件没有直接的交互。我们再来看下JVM有哪些组成部分,如下图所示:
该图参考了网上广为流传的JVM构成图,大家看这个图,整个JVM分为四部分:q Class Loader 类加载器
类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java程序,然后通过javac编译成class文件,那怎么才能加载到内存中被执行呢?Class Loader承担的就是这个责任,那不可能随便建立一个.class文件就能被加载的,Class Loader加载的class文件是有格式要求,在《JVM Specification》中式这样定义Class文件的结构:
ClassFile {
u4magic;
u2minor_version;
u2 major_version;
u2constant_pool_count;
cp_infoconstant_pool[constant_pool_count-1];
u2access_flags;
u2this_class;
u2super_class;
u2interfaces_count;
u2interfaces[interfaces_count];
u2fields_count;
field_infofields[fields_count];
u2methods_count;
method_infomethods[methods_count];
u2attributes_count;
attribute_infoattributes[attributes_count];
}
需要详细了解的话,可以仔细阅读《JVM Specification》的第四章“The class File Format”,这里不再详细说明。
友情提示:ClassLoader只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由ExecutionEngine负责的。
q Execution Engine 执行引擎
执行引擎也叫做解释器(Interpreter),负责解释命令,提交操作系统执行。
q Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
q Runtime data area运行数据区
运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治,下一章节详细介绍之。
整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!
JVM加载class文件的原理机制
1.Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中
2.java中的类大致分为三种:
1.系统类
2.扩展类
3.由程序员自定义的类
3.类装载方式,有两种
1.隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载,通过class.forname()等方法,显式加载需要的类
隐式加载与显式加载的区别:
两者本质是一样?,
4.类加载的动态性体现
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再
运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现
5.java类装载器
Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下:
BootstrapLoader-负责加载系统类
|
--ExtClassLoader-负责加载扩展类
|
--AppClassLoader-负责加载应用类
为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型
6.类加载器之间是如何协调工作的
前面说了,java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢。
在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent找不到,那么才由自己依照自己的搜索路径搜索类”,注意喔,这句话具有递归性
/** * @author Jamson Huang * */ public class TestClass { /** * @param args */ public static void main(String[] args) throws Exception{ //调用class加载器 ClassLoader cl = TestClass.class .getClassLoader(); System.out.println(cl); //调用上一层Class加载器 ClassLoader clParent = cl.getParent(); System.out.println(clParent); //调用根部Class加载器 ClassLoader clRoot = clParent.getParent(); System.out.println(clRoot); } }
/** * @author Jamson Huang * */ public class TestClass { /** * @param args */ public static void main(String[] args) throws Exception{ //调用class加载器 ClassLoader cl = TestClass.class.getClassLoader(); System.out.println(cl); //调用上一层Class加载器 ClassLoader clParent = cl.getParent(); System.out.println(clParent); //调用根部Class加载器 ClassLoader clRoot = clParent.getParent(); System.out.println(clRoot); } }
/** * @author Jamson Huang * */ public class Test1 { /** * @param args */ public static void main(String[] args) throws Exception { System.out.println(Test1.class .getClassLoader()); Test2 test2 = new Test2(); test2.print(); } } /** * @author Jamson Huang * */ public class Test2 { public void print(){ System.out.println(Test2.class ); System.out.println(this .getClass()); System.out.println(Test2.class .getClassLoader()); } }
/** * @author Jamson Huang * */ public class Test1 { /** * @param args */ public static void main(String[] args)throws Exception { System.out.println(Test1.class.getClassLoader()); Test2 test2 = new Test2(); test2.print(); } } /** * @author Jamson Huang * */ public class Test2 { public void print(){ System.out.println(Test2.class); System.out.println(this.getClass()); System.out.println(Test2.class.getClassLoader()); } }
try { URL url = new URL( "file:/d:/test/lib/" ); URLClassLoader urlCL = new URLClassLoader( new URL[]{url}); Class c = urlCL.loadClass("TestClassA" ); TestClassA object = (TestClassA)c.newInstance(); object.method(); }catch (Exception e){ e.printStackTrace(); }
try{ URL url = new URL("file:/d:/test/lib/"); URLClassLoader urlCL = new URLClassLoader(new URL[]{url}); Class c = urlCL.loadClass("TestClassA"); TestClassA object = (TestClassA)c.newInstance(); object.method(); }catch(Exception e){ e.printStackTrace(); }
我们通过自定义的类加载器实现了TestClassA类的加载并调用method()方法。分析一下这个程序:首先定义URL指定类加载器从何处加载类,URL可以指向网际网络上的任何位置,也可以指向我们计算机里的文件系统(包含JAR文件)。上述范例当中我们从file:/d:/test/lib/处寻找类;然后定义URLClassLoader来加载所需的类,最后即可使用该实例了。
9.类加载器的阶层体系
讨论了这么多以后,接下来我们仔细研究一下Java的类加载器的工作原理:
当执行java***.class的时候,java.exe会帮助我们找到JRE,接着找到位于JRE内部的jvm.dll,这才是真正的Java虚拟机器,最后加载动态库,激活Java虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器――BootstrapLoader,BootstrapLoader是由C++所撰写而成,这个BootstrapLoader所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载Launcher.java之中的ExtClassLoader,并设定其Parent为null,代表其父加载器为BootstrapLoader。然后BootstrapLoader再要求加载Launcher.java之中的AppClassLoader,并设定其Parent为之前产生的ExtClassLoader实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是,Launcher$ExtClassLoader.class与Launcher$AppClassLoader.class都是由BootstrapLoader所加载,所以Parent和由哪个类加载器加载没有关系。
下面的图形可以表示三者之间的关系:
BootstrapLoader<---(Extends)----AppClassLoader<---(Extends)----ExtClassLoader
这三个加载器就构成我们的Java类加载体系。他们分别从以下的路径寻找程序所需要的类:
BootstrapLoader:sun.boot.class.path
ExtClassLoader:java.ext.dirs
AppClassLoader:java.class.path
这三个系统参量可以通过System.getProperty()函数得到具体对应的路径。大家可以自己编程实现查看具体的路径。