Java类加载机制

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

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都不了解它的执行机制,是不是很丢人呢?不过没关系,马上你就不丢人了。
正题
下面我们具体了解类的加载机制。
1)加载
2)连接(验证-准备-解析)
3)初始化
JVM就是按照上面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。首先将字节码加载到内存中,然后对字节码进行连接,连接阶段包括了验证准备解析这3个步骤,连接完毕之后再进行初始化工作。下面我们一一了解:
首先我们了解一下加载

  1. 什么是类的加载?
    类的加载指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法去内,然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系.这样才能通过反射找到相应的类信息.
    我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。通过它可以找到类的信息,我们来看下源码:
/* 
    * Constructor. Only the Java Virtual Machine creates Class
    * objects. 
    */ 
private Class() {}

从上面贴出的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。
2. 什么时候对类进行加载呢?
Java虚拟机有预加载功能。类加载器并不需要等到某个类被"首次主动使用"时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)。其实就是一句话,Java虚拟机有预加载功能。
类加载器
讲到类加载,我们不得不了解类加载器.

  1. 什么是类加载器?
    类加载器负责对类的加载。
  2. Java自带有3种类加载器
    在这里插入图片描述
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属性,就是表示父加载器。这点正好说明了加载器之间的关系并不是继承关系。
3. 双亲委派机制
关于类加载器,我们不得不说一下双亲委派机制。听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。
上面的源码也说明了这点。

 if (parent != null) { 
     c = parent.loadClass(name, false); 
   } else {
          c = findBootstrapClassOrNull(name);
   }
  1. 为何要双亲委派机制
    对于我们技术来讲,我们不但要知其然,还要知其所以然。为何要采用双亲委派机制呢?了解为何之前,我们先来说明一个知识点:
    判断两个类相同的前提是这两个类都是同一个加载器进行加载的,如果使用不同的类加载器进行加载同一个类,也会有不同的结果。
    如果没有双亲委派机制,会出现什么样的结果呢?比如我们在rt.jar中随便找一个类,如java.util.HashMap,那么我们同样也可以写一个一样的类,也叫java.util.HashMap存放在我们自己的路径下(ClassPath).那样这两个相同的类采用的是不同的类加载器,系统中就会出现两个不同的HashMap类,这样引用程序就会出现一片混乱。
    我们看一个例子:
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类加载器进行加载的,我们把它们的父类加载器打印出来了,可以看出他们的加载器是不一样的。很奇怪为何会执行classloadernull这句话。其实classloadernull表示的就是根类加载器。我们看看

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类加载器。
类的连接
讲完了类的加载之后,我们需要了解一下类的连接。类的连接有三步,分别是验证,准备,解析。下面让我们一一了解

  1. 首先我们看看验证阶段。
    验证阶段主要做了以下工作
    将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。
    类文件结构检查:格式符合jvm规范-语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖
    字节码验证:确保字节码可以安全的被java虚拟机执行.
    二进制兼容性检查:确保互相引用的类的一致性.如A类的a方法会调用B类的b方法.那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性.因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。
  2. 准备阶段
    java虚拟机为类的静态变量分配内存并赋予默认的初始值.如int分配4个字节并赋值为0,long分配8字节并赋值为0;
  3. 解析阶段
    解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。
    初始化时机
    类的加载时机中我们提到了“首次主动使用”这个词语,那什么是“主动使用”呢?
    主动初始化的6种方式
    (1)创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
    (2)调用类的静态属性或者为静态属性赋值
    (3)调用类的静态方法
    (4)通过class文件反射创建对象
    (5)初始化一个类的子类:使用子类的时候先初始化父类
    (6)java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类
    只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。
    同时我们需要注意下面几个Tips:
    1)在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.
    这里多说一点,为什么只初始化一次呢?因为我们上面讲到过类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到
    类的相关信息。唯一一个Class对象说明了类只需要初始化一次即可,如果再次初始化就会出现多个Class对象,这样和唯一相违背了。
    2)在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化;
    3)在编译时无法确定下来的静态变量(运行时常量),会对类进行初始化;
    4)如果这个类没有被加载和连接的话,那就需要进行加载和连接
    5)如果这个类有父类并且这个父类没有被初始化,则先初始化父类.
    6)如果类中存在初始化语句,依次执行初始化语句.
public class Test1 { 
  public static void main(String args[]){ 
    System.out.println(FinalTest.x);
  } 
} 

class FinalTest{ 
  public static final int x =6/3; 
  static { 
      System.out.println("FinalTest static block"); 
  } 
}

上面和下面的例子大家对比下,然后自己看看输出的是什么?

public class Test2 { 
    public static void main(
        String args[]){ System.out.println(FinalTest2.x); 
    } 
} 
class FinalTest2{ 

public static final int x =new Random().nextInt(100); 
static { 
    System.out.println("FinalTest2 static block"); 
} 
}

第一个输出的是
2
第二个输出的是
FinalTest2 static block
61(随机数)
为何会出现这样的结果呢?
参考上面的Tips2和Tips3,第一个能够在编译时期确定的,叫做编译常量;第二个是运行时才能确定下来的,叫做运行时常量。编译常量不会引起类的初始化,而运行常量就会。
那么将第一个例子的final去掉之后呢?输出又是什么呢?
这就是对类的首次主动使用,引用类的静态变量,输出的当然是:
FinalTest static block
2
那么在第一个例子的输出语句下面添加
FinalTest.x =3;
又会输出什么呢?
大家不妨试试!提示(Tips1)
类的初始化步骤
讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)
没有父类的情况:

1)类的静态属性 
2)类的静态代码块 
3)类的非静态属性 
4)类的非静态代码块 
5)构造方法

有父类的情况:

1)父类的静态属性 
2)父类的静态代码块 
3)子类的静态属性 
4)子类的静态代码块 
5)父类的非静态属性 
6)父类的非静态代码块 
7)父类构造方法 
8)子类非静态属性 
9)子类非静态代码块 
10)子类构造方法

在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。
类的初始化内容这样看起来还是挺多的,包括“主动使用”大家可以自己去写一些demo去验证一下。
结束JVM进程的几种方式
了解完类加载机制之后,接下来我们了解一下结束JVM进程的几种方式吧。

(1) 执行System.exit() 
(2) 程序正常结束 
(3) 程序抛出异常,一直向上抛出没处理 
(4) 操作系统异常,导致JVM退出

JVM有上面4种结束的方式,我们一一了解下:
(1)我们先来看看第一种方式,找到源代码我们发现:

/** 
    * Terminates the currently running Java Virtual Machine. The 
    * argument serves as a status code; by convention, a nonzero status 
    * code indicates abnormal termination. 
    */ 
  public static void exit(int status) { 
      Runtime.getRuntime().exit(status); 
  }

上面的代码解释了System.exit()方法的作用就是:是中断当前运行的java虚拟机。这是自杀方式。
(2)第二种程序正常结束的方式,我们在运行main方法的时候,运行状态按钮由绿色变红色再变绿色的过程就是程序启动-运行-结束的过程。 那么,我们来看看Android的程序,同样,安卓也有自己的启动方式,也是一个main方法。那么我们的android程序能够一直运行的前提就是我们的main方法一直被执行着,一旦main方法执行完毕,程序就是kill。我们找找源代码才能有更好的说服力;我们找到ActivityThread的main方法

 public static void main(String[] args) { 
     SamplingProfilerIntegration.start(); 
     // CloseGuard defaults to true and can be quite spammy. We 
     // disable it here, but selectively enable it later (via 
     // StrictMode) on debug builds, but using DropBox, not logs. 
     CloseGuard.setEnabled(false); 
     
     Environment.initForCurrentUser(); 
     // Set the reporter for event logging in libcore 
     EventLogger.setReporter(new EventLoggingReporter()); 
     
     Security.addProvider(new AndroidKeyStoreProvider()); 
     
     Process.setArgV0("<pre-initialized>"); 
     
     Looper.prepareMainLooper(); 
     
     ActivityThread thread = new ActivityThread(); 
     thread.attach(false); 
     
     if (sMainThreadHandler == null) { 
         sMainThreadHandler = thread.getHandler(); 
     } 
     
     AsyncTask.init(); 
     if (false) { 
         Looper.myLooper().setMessageLogging(new 
             LogPrinter(Log.DEBUG, "ActivityThread")); 
     } 
     
     Looper.loop(); 
     throw new RuntimeException("Main thread loop unexpectedly exited"); 
 }

上面的代码都不用看,直接看最后两行代码。执行完Looper.loop()之后,直接抛出了异常。但是我们并没有见到这个异常,说明我们的Looper一直在执行这样保证我们的app不被kill掉。Android就是用这种方式来保证我们的app一直运行下去的。
(3)第三种方式不用过多解释,一直没有处理被抛出的异常,这样导致了程序崩溃。
(4)第四种方式是系统异常导致了jvm退出。其实jvm就是一个软件,如果我们的操作系统都出现了错误,那么运行在他上面的软件(jvm)必然会被kill。
结束并回顾
到这里,我们基本都清楚了类的加载机制。那么我们在第一篇文章中开头提到一个例子,我们这里来讲讲输出的是什么,并且为何如此输出.

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; 
} 
}

下面是我们的测试类TestSingleton

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); 
} 
}

输出是:
counter1=1
counter2=0
why?我们一步一步分析:

  1. 执行TestSingleton第一句的时候,因为我们没有对Singleton类进行加载和连接,所以我们首先需要对它进行加载和连接操作。在连接阶-准备阶段,我们要讲给静态变量赋予默认初始值。
    singleton =null
    counter1 =0
    counter2 =0
  2. 加载和连接完毕之后,我们再进行初始化工作。初始化工作是从上往下依次执行的,注意这个时候还没有调用Singleton.getSingleton();
  • 首先 singleton = new Singleton();这样会执行构造方法内部逻辑,进行++;此时counter1=1,counter2 =1 ;
  • 接下来再看第二个静态属性,我们并没有对它进行初始化,所以它就没办法进行初始化工作了;
  • 第三个属性counter2我们初始化为0 ,而在初始化之前counter2=1,执行完counter2=0之后counter2=0了;
  1. 初始化完毕之后我们就要调用静态方法Singleton.getSingleton(); 我们知道返回的singleton已经初始化了。
    那么输出的内容也就理所当然的是1和0了。这样一步一步去理解程序执行过程是不是让你清晰的认识了java虚拟机执行程序的逻辑呢。
那么我们接下来改变一下代码顺序,将 
public static int counter1; 
public static int counter2 = 0; 
private static Singleton singleton = new Singleton(); 
又会输出什么呢?为什么这样输出呢? 
这个问题留给大家去思考,主要还是理解为什么这样输出才是最重要的。
  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值