深入理解Java类加载机制(一)

2,313 阅读9分钟
原文链接: www.jianshu.com

1 前言:

在上一篇文章一文让你明白Java字节码中,
我们了解了java字节码的解析过程,那么在接下来的内容中,我们来了解一下类的加载机制。

2 题外话

Java的核心是什么?当然是JVM了,所以说了解并熟悉JVM对于我们理解Java语言非常重要,不管你是做Java还是Android,熟悉JVM是我们每个Java、Android开发者必不可少的技能。如果你现在觉得Android的开发到了天花板的地步,那不妨往下走走,一起探索JAVA层面的内容。如果我们不了解自己写的代码是如何被执行的,那么我们只是一个会写代码的程序员,我们知其然不知其所以然。看到很多人说现在工作难找,真是这样吗?如果我们足够优秀,工作还难找吗?如果我们底子足够深,需要找工作吗?找不到工作多想想自己的原因,总是抱怨环境是没有用的,因为你没办法去改变坏境。如果我们一直停留在框架层面,停留在新的功能层面,那么我们的优势在哪里呢?所以说,我们不仅要学会写代码,还要知道为什么这样写代码,这才是我们的核心竞争力之一。这样我们的差异化才能够体现出来,不信?我们走着瞧......我们第一个差异化就是对JVM的掌握,而今天的内容类加载机制是JVM比较核心的部分,如果你想和别人不一样,那就一起仔细研究研究这次的内容吧。

3 引子

为了看看自己是否掌握了类加载机制,我们看看一道题:

public class Singleton {
  private static Singleton singleton = new Singleton();
  public static int counter1;
  public static int counter2 = 0;
  private Singleton() {
      counter1++;
      counter2++;
  }
  public static Singleton getSingleton() {
      return singleton;
  }
 }

上面是一个Singleton类,有3个静态变量,下面是一个测试类,打印出静态属性的值,就是这么简单。

  public class TestSingleton {
  public static void main(String args[]){
      Singleton singleton = Singleton.getSingleton();
      System.out.println("counter1="+singleton.counter1);
      System.out.println("counter2="+singleton.counter2);
  }
}

在往下看之前,大家先看看这道题的输出是啥?如果你清楚知道为什么,那么说明你掌握了类的加载机制,往下看或许有不一样的收获;如果你不懂,那就更要往下看了。我们先不讲这道题,待我们了解了类的加载机制之后,回过头看看这道题,或许有恍然大悟的感觉,或许讲完之后你会怀疑自己是否真正了解Java,或许你写了这么多年的Java都不了解它的执行机制,是不是很丢人呢?不过没关系,马上你就不丢人了。

4 正题

下面我们具体了解类的加载机制。
1)加载
2)连接(验证-准备-解析)
3)初始化
JVM就是按照上面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。首先将字节码加载到内存中,然后对字节码进行连接,连接阶段包括了验证准备解析这3个步骤,连接完毕之后再进行初始化工作。下面我们一一了解:

5 首先我们了解一下加载

5.1 什么是类的加载?

类的加载指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法去内,然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系.这样才能通过反射找到相应的类信息.

我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。通过它可以找到类的信息,我们来看下源码:

 /*
     * Constructor. Only the Java Virtual Machine creates Class
     * objects.
     */
 private Class() {}

从上面贴出的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。

5.2 什么时候对类进行加载呢?

Java虚拟机有预加载功能。类加载器并不需要等到某个类被"首次主动使用"时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)。其实就是一句话,Java虚拟机有预加载功能。

6 类加载器

讲到类加载,我们不得不了解类加载器.

6.1 什么是类加载器?

类加载器负责对类的加载。

6.2 Java自带有3种类加载器


classloader.png
    1)根类加载器,使用c++编写(BootStrap),负责加载rt.jar
    2)扩展类加载器,java实现(ExtClassLoader)
    3)应用加载器,java实现(AppClassLoader) classpath

根类加载器,是用c++实现的,我们没有办法在java层面看到;我们接下来看看ExtClassLoader的代码,它是在Launcher类中,

static class ExtClassLoader extends URLClassLoader

同时我们看看AppClassLoader,它也是在Launcher中,

static class AppClassLoader extends URLClassLoader

他们同时继承一个类URLClassLoader。关于这种层次关系,看起来像继承,其实不是的。我们看到上面的代码就知道ExtClassLoader和AppClassLoader同时继承同一个类。同时我们来看下ClassLoader的loadClass方法也可以知道,下面贴出源代码:

 private final ClassLoader parent;
 protected Class<?> loadClass(String name, boolean resolve)
                                           throws ClassNotFoundException
   {
      synchronized (getClassLoadingLock(name)) {
          // First, check if the class has already been loaded
             Class c = findLoadedClass(name);
              if (c == null) {
                 long t0 = System.nanoTime();
                    try {
                         if (parent != null) {
                             c = parent.loadClass(name, false);
                          } else {
                             c = findBootstrapClassOrNull(name);
                               }
                           } catch (ClassNotFoundException e) {
                                // ClassNotFoundException thrown if class not found
                                // from the non-null parent class loader
                            }
                return c;
                   }
              }

源码没有全部贴出,只是贴出关键代码。从上面代码我们知道首先会检查class是否已经加载了,如果已经加载那就直接拿出,否则再进行加载。其中有一个parent属性,就是表示父加载器。这点正好说明了加载器之间的关系并不是继承关系。

6.3 双亲委派机制

关于类加载器,我们不得不说一下双亲委派机制。听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。上面的源码也说明了这点。

  if (parent != null) {
           c = parent.loadClass(name, false);
    } else {
           c = findBootstrapClassOrNull(name);
                }

6.4 为何要双亲委派机制

对于我们技术来讲,我们不但要知其然,还要知其所以然。为何要采用双亲委派机制呢?了解为何之前,我们先来说明一个知识点:判断两个类相同的前提是这两个类都是同一个加载器进行加载的,如果使用不同的类加载器进行加载同一个类,也会有不同的结果。我们看一个例子:

public class MyClassLoader {
    public static void main(String args[]) throws ClassNotFoundException,         IllegalAccessException, InstantiationException {
    ClassLoader loader = new ClassLoader() {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream inputStream = getClass().getResourceAsStream(fileName);
            if (inputStream==null)
                return super.loadClass(name);
            try {
                byte[] bytes = new byte[inputStream.available()];
                inputStream.read(bytes);
                return defineClass(name,bytes,0,bytes.length);

            } catch (IOException e) {
                e.printStackTrace();
                throw new ClassNotFoundException(name);
            }
        }

    };
    Object object = loader.loadClass("jvm.classloader.MyClassLoader").newInstance();
    System.out.println(object instanceof jvm.classloader.MyClassLoader);

  }
}

大家可以看看输出的是什么?我们自己定义了一个类加载器,让它去加载我们自己写的一个类,然后判断由我们写的类加载器加载的类是否是MyClassLoader的一个实例。答案是否定的。为什么?因为jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加载器加载的,而我们却指定了自己的加载器,当然加载出来的类就不相同了。不信,我们将他的父类加载器都打印出来。在上面代码中加入下面代码:

ClassLoader classLoader = object.getClass().getClassLoader();
    while (classLoader!=null){
        System.out.println(classLoader);
        classLoader = classLoader.getParent();
    }
    if (classLoader==null){
        System.out.println("classLoader == null");
    }
输出内容 :
jvm.classloader.MyClassLoader$1@60172ec6
sun.misc.Launcher$AppClassLoader@338bd37a
sun.misc.Launcher$ExtClassLoader@20e90906
classLoader == null

对比一下下面的代码:

  Object object2 = new MyClassLoader();
   ClassLoader classLoader2 = object2.getClass().getClassLoader();

    while (classLoader2!=null){
        System.out.println(classLoader2);
        classLoader2 = classLoader2.getParent();
    }
    if (classLoader2==null){
        System.out.println("classLoader2 == null");
    }
输出内容:
sun.misc.Launcher$AppClassLoader@20e90906
sun.misc.Launcher$ExtClassLoader@234f79cb
classLoader == null

第一个是我们自己加载器加载的类,第二个是直接new的一个对象,是由App类加载器进行加载的,我们把它们的父类加载器打印出来了,可以看出他们的加载器是不一样的。很奇怪为何会执行classloader==null这句话。其实classloader==null表示的就是根类加载器。我们看看Class.getClassLoader()方法源码:

/**
 * Returns the class loader for the class.  Some implementations may use
 * null to represent the bootstrap class loader. This method will return
 * null in such implementations if this class was loaded by the bootstrap
 * class loader.
**/
   @CallerSensitive
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    }
    return cl;
}

从注释中我们知道了,如果返回了null,表示的是bootstrap类加载器。

7 类的连接

讲完了类的加载之后,我们需要了解一下类的连接。类的连接有三步,分别是验证,准备,解析。下面让我们一一了解

7.1 首先我们看看验证阶段。

验证阶段主要做了以下工作
-将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。
-类文件结构检查:格式符合jvm规范-语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖
-字节码验证:确保字节码可以安全的被java虚拟机执行.
二进制兼容性检查:确保互相引用的类的一致性.如A类的a方法会调用B类的b方法.那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性.因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。

7.2 准备阶段

java虚拟机为类的静态变量分配内存并赋予默认的初始值.如int分配4个字节并赋值为0,long分配8字节并赋值为0;

7.3 解析阶段

解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。

到这里为止,我们知道了类的加载,类加载器,双亲委派机制,类的连接等等操作。那么接下来需要讲的是类的初始化,初始化内容较多,另开一篇文章深入理解Java类加载机制(二)讲,这样大家就不会疲劳和畏惧了。