疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

也谈Java的类加载机


 

       类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行。

  研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性。

  一、简单过程

  Java程序运行的场所是内存,当在命令行下执行:

  java HelloWorld

  命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。

  其中的过程就是类加载过程:

  1、寻找jre目录,寻找jvm.dll,并初始化JVM;

  2、产生一个Bootstrap Loader(启动类加载器);

  3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。

  4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。

  5、最后由AppClass Loader加载HelloWorld类。

  以上就是类加载的最一般的过程。

  二、类加载器各自搜索的目录

  为了弄清楚这个问题,首先还要看看System类的API doc文档。

  1、Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。

  2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:/projects/testproj/classes HelloWorld

  3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld

  ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。

  三、类加载器的特点

  1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。

  2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。

  3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.

  四、类加载器的获取

  很容易,看下面例子

  public class HelloWorld {

  public static void main(String[] args) {

  HelloWorld hello = new HelloWorld();

  Class c = hello.getClass();

  ClassLoader loader = c.getClassLoader();

  System.out.println(loader);

  System.out.println(loader.getParent());

  System.out.println(loader.getParent().getParent());

  }

  }

  打印结果:

  sun.misc.Launcher$AppClassLoader@19821f

  sun.misc.Launcher$ExtClassLoader@addbf1

  null

  Process finished with exit code 0

  从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(启动类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

  五、类的加载

  类加载有三种方式:

  1、命令行启动应用时候由JVM初始化加载

  2、通过Class.forName()方法动态加载

  3、通过ClassLoader.loadClass()方法动态加载

  三种方式区别比较大,看个例子就明白了:

  public class HelloWorld {

  public static void main(String[] args) throws ClassNotFoundException {

  ClassLoader loader = HelloWorld.class.getClassLoader();

  System.out.println(loader);

  //使用ClassLoader.loadClass()来加载类,不会执行初始化块

  loader.loadClass("Test2");

  //使用Class.forName()来加载类,默认会执行初始化块

  // Class.forName("Test2");

  //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块

  // Class.forName("Test2", false, loader);

  }

  }

  public class Test2 {

  static {

  System.out.println("静态初始化块执行了!");

  }

  }

  分别切换加载方式,会有不同的输出结果。

  Java 语言是一种具有动态性的解释型编程语言,当指定程序运行的时候, Java 虚拟机就将编译生成的 .class 文件按照需求和一定的规则加载进内存,并组织成为一个完整的 Java 应用程序。 Java 语言把每个单独的类 Class 和接口 Implements 编译成单独的一个 .class 文件,这些文件对于 Java 运行环境来说就是一个个可以动态加载的单元。正是因为 Java 的这种特性,我们可以在不重新编译其它代码的情况下,只编译需要修改的单元,并把修改文件编译后的 .class 文件放到 Java 的路径当中,等到下次该 Java 虚拟机器重新激活时,这个逻辑上的 Java 应用程序就会因为加载了新修改的 .class 文件,自己的功能也做了更新,这就是 Java 的动态性。

  下面用一个简单的例子让大家对 Java 的动态加载有一个基本的认识:

  class TestClassA{

  public void method(){

  System.out.println("Loading ClassA");

  }

  }

  public class ClassLoaderTest {

  public static void main(String args[]){

  TestClassA testClassA = new TestClassA();

  testClassA.method();

  }

  }

  编译后输入命令: java -verbose:class ClassLoaderTest ,执行文件。JRE ( JavaRuntime Environment )首先加载 ClassLoaderTest 文件,然后再加载TestClassA 文件,从而实现了动态加载。

  1. 预先加载与依需求加载

  Java 运行环境为了优化系统,提高程序的执行速度,在 JRE 运行的开始会将 Java 运行所需要的基本类采用预先加载( pre-loading )的方法全部加载到内存当中,因为这些单元在 Java 程序运行的过程当中经常要使用的,主要包括 JRE 的 rt.jar 文件里面所有的 .class 文件。

  当 java.exe 虚拟机开始运行以后,它会找到安装在机器上的JRE 环境,然后把控制权交给 JRE,JRE 的类加载器会将 lib目录下的 rt.jar 基础类别文件库加载进内存,这些文件是 Java 程序执行所必须的,所以系统在开始就将这些文件加载,避免以后的多次 IO 操作,从而提高程序执行效率。

  相对于预先加载,我们在程序中需要使用自己定义的类的时候就要使用依需求加载方法( load-on-demand ),就是在 Java程序需要用到的时候再加载,以减少内存的消耗,因为 Java 语言的设计初衷就是面向嵌入式领域的。

  在这里还有一点需要说明的是, JRE 的依需求加载究竟是在什么时候把类加载进入内部的呢?

  我们在定义一个类实例的时候,比如 TestClassA testClassA ,这个时候 testClassA 的值为 null ,也就是说还没有初始化,没有调用 TestClassA 的构造函数,只有当执行 testClassA = new TestClassA() 以后, JRE 才正真把 TestClassA 加载进来。

  2. 隐式加载和显示加载

  Java 的加载方式分为隐式加载( implicit )和显示加载( explicit ),上面的例子中就是用的隐式加载的方式。所谓隐式加载就是我们在程序中用 new 关键字来定义一个实例变量, JRE 在执行到 new 关键字的时候就会把对应的实例类加载进入内存。隐式加载的方法很常见,用的也很多, JRE 系统在后台自动的帮助用户加载,减少了用户的工作量,也增加了系统的安全性和程序的可读性。

  相对于隐式加载的就是我们不经常用到的显示加载。所谓显示加载就是有程序员自己写程序把需要的类加载到内存当中,下面我们看一段程序:

  class TestClass{

  public void method(){

  System.out.println("TestClass-method");

  }

  }

  public class CLTest {

  public static void main(String args[]) {

  try{

  Class c = Class.forName("TestClass");

  TestClass object = (TestClass)c.newInstance();

  object.method();

  }catch(Exception e){

  e.printStackTrace();

  }

  }

  }

  我们通过 Class 类的 forName (String s) 方法把自定义类 TestClass 加载进来,并通过 newInstance ()方法把实例初始化。事实上 Class 类还很多的功能,这里就不细讲了,有兴趣的可以参考 JDK 文档。

  Class 的 forName() 方法还有另外一种形式: Class forName(String s, boolean flag, ClassLoader classloader) , s 表示需要加载类的名称, flag 表示在调用该函数加载类的时候是否初始化静态区, classloader 表示加载该类所需的加载器。

  forName (String s) 是默认通过 ClassLoader.getCallerClassLoader() 调用类加载器的,但是该方法是私有方法,我们无法调用,如果我们想使用 Class forName(String s, boolean flag, ClassLoader classloader) 来加载类的话,就必须要指定类加载器,可以通过如下的方式来实现:

  Test test = new Test();//Test 类为自定义的一个测试类;

  ClassLoader cl = test. getClass().getClassLoader();

  // 获取 test 的类装载器;

  Class c = Class.forName("TestClass", true, cl);

  因为一个类要加载就必需要有加载器,这里我们是通过获取加载 Test 类的加载器 cl 当作加载 TestClass 的类加载器来实现加载的。

  3. 自定义类加载机制

  之前我们都是调用系统的类加载器来实现加载的,其实我们是可以自己定义类加载器的。利用 Java 提供的java.net.URLClassLoader 类就可以实现。下面我们看一段范例:

  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 来加载所需的类,最后即可使用该实例了。

  4. 类加载器的阶层体系

  讨论了这么多以后,接下来我们仔细研究一下 Java 的类加载器的工作原理:

  当执行 java ***.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。