解析Java虚拟机中类的初始化及加载器的父委托机制

类的初始化
  在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。

  在程序中,静态变量的初始化有两种途径:

  1.在静态变量的声明处进行初始化;

  2.在静态代码块中进行初始化。

  没有经过显式初始化的静态变量将原有的值。

  一个比较奇怪的例子:

package com.mengdd.classloader;

class Singleton {

  // private static Singleton mInstance = new Singleton();// 位置1
  // 位置1输出:
  // counter1: 1
  // counter2: 0
  public static int counter1;
  public static int counter2 = 0;

  private static Singleton mInstance = new Singleton();// 位置2

  // 位置2输出:
  // counter1: 1
  // counter2: 1

  private Singleton() {
    counter1++;
    counter2++;
  }

  public static Singleton getInstantce() {
    return mInstance;
  }
}

public class Test1 {

  public static void main(String[] args) {

    Singleton singleton = Singleton.getInstantce();
    System.out.println("counter1: " + Singleton.counter1);
    System.out.println("counter2: " + Singleton.counter2);
  }
}

可见将生成对象的语句放在两个位置,输出是不一样的(相应位置的输出已在程序注释中标明)。

  这是因为初始化语句是按照顺序来执行的。

  静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。

类的初始化步骤
  1.假如这个类还没有被加载和连接,那就先进行加载和连接。

  2.假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。

  3.假如类中存在初始化语句,那就依次执行这些初始化语句。

类的初始化时机
  Java程序对类的使用方式可以分为两种:

  1.主动使用

  2.被动使用

  所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化它们。

  主动使用的六种情况:

  1.创建类的实例。

new Test();
  

  2.访问某个类或接口的静态变量,或者对该静态变量赋值。

int b = Test.a;
Test.a = b;

  

  3.调用类的静态方法

Test.doSomething();

  

  4.反射

Class.forName(“com.mengdd.Test”);

  

  5.初始化一个类的子类

class Parent{
}
class Child extends Parent{
   public static int a = 3;
}
Child.a = 4;

  

  6.Java虚拟机启动时被标明为启动类的类

java com.mengdd.Test

  除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

接口的特殊性
  当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。

    在初始化一个类时,并不会先初始化它所实现的接口。

    在初始化一个接口时,并不会先初始化它的父接口。

  因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。

final类型的静态变量
  final类型的静态变量是编译时常量还是变量,会影响初始化语句块的执行。

  如果一个静态变量的值是一个编译时的常量,就不会对类型进行初始化(类的static块不执行);

如果一个静态变量的值是一个非编译时的常量,即只有运行时会有确定的初始化值,则就会对这个类型进行初始化(类的static块执行)。

例子代码:

package com.mengdd.classloader;

import java.util.Random;

class FinalTest1 {
  public static final int x = 6 / 3; // 编译时期已经可知其值为2,是常量
  // 类型不需要进行初始化
  static {
    System.out.println("static block in FinalTest1");
    // 此段语句不会被执行,即无输出
  }
}

class FinalTest2 {
  public static final int x = new Random().nextInt(100);// 只有运行时才能得到值
  static {
    System.out.println("static block in FinalTest2");
    // 会进行类的初始化,即静态语句块会执行,有输出
  }
}

public class InitTest {

  public static void main(String[] args) {
    System.out.println("FinalTest1: " + FinalTest1.x);
    System.out.println("FinalTest2: " + FinalTest2.x);
  }
}

主动使用的归属明确性
  只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。

package com.mengdd.classloader;

class Parent {
  static int a = 3;

  static {
    System.out.println("Parent static block");
  }

  static void doSomething() {
    System.out.println("do something");
  }
}

class Child extends Parent {
  static {
    System.out.println("Child static block");
  }
}

public class ParentTest {

  public static void main(String[] args) {

    System.out.println("Child.a: " + Child.a);
    Child.doSomething();

    // Child类的静态代码块没有执行,说明Child类没有初始化
    // 这是因为主动使用的变量和方法都是定义在Parent类中的
  }
}

ClassLoader类
  调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

 package com.mengdd.classloader;

class CL {

  static {
    System.out.println("static block in CL");
  }
}

public class ClassLoaderInitTest {

  public static void main(String[] args) throws Exception {
    ClassLoader loader = ClassLoader.getSystemClassLoader();
    Class<?> clazz = loader.loadClass("com.mengdd.classloader.CL");
    // loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化

    System.out.println("----------------");
    clazz = Class.forName("com.mengdd.classloader.CL");
  }

}

类加载器的父委托机制

类加载器
  类加载器用来把类加载到Java虚拟机中。

类加载器的类型
  有两种类型的类加载器:

  1.JVM自带的加载器:

    根类加载器(Bootstrap)

    扩展类加载器(Extension)

    系统类加载器(System)

  2.用户自定义的类加载器:

    java.lang.ClassLoader的子类,用户可以定制类的加载方式。

JVM自带的加载器
  Java虚拟机自带了以下几种加载器。

  1.根(Bootstrap)类加载器:

  该加载器没有父加载器。

  它负责加载虚拟机的核心类库,如java.lang.*等。

  根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。

  根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类,它是用C++写的。

  2.扩展(Extension)类加载器:

  它的父加载器为根类加载器。

  它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。

  扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。

  3.系统(System)类加载器:

  也称为应用类加载器,它的父加载器为扩展类加载器。

  它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。

  系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。

  注意:这里的父加载器概念并不是指类的继承关系,子加载器不一定继承了父加载器(其实是组合的关系)。

用户自定义类加载器
  除了以上虚拟机自带的类加载器以外,用户还可以定制自己的类加载器(User-defined Class Loader)。

  Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

类加载的父委托机制
  从JDK 1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。

  在父委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器,各个加载器按照父子关系形成了树形结构。

  当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由loader1本身加载Sample类。

  说明具体过程的一个例子:

loader2首先从自己的命名空间中查找Sample类是否已经被加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。

  如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。

  若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将Sample类所对应的Class对象的引用返回给loader1,loader1再返回给loader2,从而成功将Sample类加载进虚拟机。

  若系统加载器不能加载Sample类,则loader1尝试加载Sample类,若loader1也不能成功加载,则loader2尝试加载。

  若所有的父加载器及loader2本身都不能加载,则抛出ClassNotFoundException异常。

  总结下来就是:

  每个加载器都优先尝试用父类加载,若父类不能加载则自己尝试加载;若成功则返回Class对象给子类,若失败则告诉子类让子类自己加载。所有都失败则抛出异常。

定义类加载器和初始类加载器
  若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器。

  所有能成功返回Class对象的引用的类加载器(包括定义类加载器,即包括定义类加载器和它下面的所有子加载器)都被称为初始类加载器。

  假设loader1实际加载了Sample类,则loader1为Sample类的定义类加载器,loader2和loader1为Sample类的初始类加载器。

父子关系
  需要指出的是,加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。

  一对父子加载器可能是同一个加载器类的两个实例,也可能不是。

  在子加载器对象中包装了一个父加载器对象。

  例如loader1和loader2都是MyClassLoader类的实例,并且loader2包装了loader1,loader1是loader2的父加载器。

  当生成一个自定义的类加载器实例时,如果没有指定它的父加载器(ClassLoader构造方法无参数),那么系统类加载器就将成为该类加载器的父加载器。

父委托机制优点
  父亲委托机制的优点是能够提高软件系统的安全性。

  因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。

  例如,java.lang.Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。

命名空间
  每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。

  在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

  在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

运行时包
  由同一类加载器加载的属于相同包的类组成了运行时包。

  决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。

  只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。

  这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。

  假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的类加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。

(0)

相关推荐

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • 了解Java虚拟机JVM的基本结构及JVM的内存溢出方式

    JVM内部结构图 Java虚拟机主要分为五个区域:方法区.堆.Java栈.PC寄存器.本地方法栈.下面 来看一些关于JVM结构的重要问题. 1.哪些区域是共享的?哪些是私有的? Java栈.本地方法栈.程序计数器是随用户线程的启动和结束而建立和销毁的, 每个线程都有独立的这些区域.而方法区.堆是被整个JVM进程中的所有线程共享的. 2.方法区保存什么?会被回收吗? 方法区不是只保存的方法信息和代码,同时在一块叫做运行时常量池的子区域还 保存了Class文件中常量表中的各种符号引用,以及翻译出来的

  • 浅谈Java的虚拟机结构以及虚拟机内存的优化

    工作以来,代码越写越多,程序也越来越臃肿,效率越来越低,对于我这样一个追求完美的程序员来说,这是绝对不被允许的,于是除了不断优化程序结构外,内存优化和性能调优就成了我惯用的"伎俩". 要对Java程序进行内存优化和性能调优,不了解虚拟机的内部原理(或者叫规范更严谨一点)是肯定不行的,这里推荐一本好书<深入Java虚拟机(第二版)>(Bill Venners著,曹晓刚 蒋靖 译,实际上本文正是作者阅读本书之后,对Java虚拟机的个人理解阐述).当然了,了解Java虚拟机的好处

  • Java虚拟机最多支持多少个线程的探讨

    McGovernTheory在StackOverflow提了这样一个问题: Java虚拟机最多支持多少个线程?跟虚拟机开发商有关么?跟操作系统呢?还有其他的因素吗? Eddie的回答: 这取决于你使用的CPU,操作系统,其他进程正在做的事情,你使用的Java的版本,还有其他的因素.我曾经见过一台Windows服务器在宕机之前有超过6500个线程.当然,大多数线程什么事情也没有做.一旦一台机器上有差不多6500个线程(Java里面),机器就会开始出问题,并变得不稳定. 以我的经验来看,JVM容纳的

  • 深入理解java虚拟机的故障处理工具

    前言 本文主要给大家介绍的是java虚拟机的故障处理工具,文中提到这些工具包括: 名称 主要作用 jps JVM process Status Tool, 显示指定系统内所有的HotSpot虚拟机进程.通常是本地主机 jstat JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据 jinfo Configuration Info for java, 显示虚拟机配置信息 jmap Memory Map for Java, 生成虚拟机的内存存储

  • Java虚拟机JVM性能优化(二):编译器

    本文将是JVM 性能优化系列的第二篇文章(第一篇:传送门),Java 编译器将是本文讨论的核心内容. 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比.然后,在文章的最后介绍了几种常见的JVM优化方法,如死代码消除,代码嵌入以及循环体优化. Java最引以为豪的特性"平台独立性"正是源于Java编译器.软件开发人员尽其所能写出最好的java应用程序,紧接着后台运行的编译器产生高效的基于目标平台的可执行代

  • java虚拟机

    众所周知,jvm的内存是受限的,一为机器的体系架构,二为操作系统本身.x86,x86-64,SPARC,.....的内存映射是不同,而各操作系统的内存管理机制也有区别.以下是来自http://fengyouhua.javaeye.com/blog/58170 1. Heap设定与垃圾回收Java Heap分为3个区,Young,Old和Permanent.Young保存刚实例化的对象.当该区被填满时,GC会将对象移到Old区.Permanent区则负责保存反射对象,本文不讨论该区.JVM的Hea

  • 深入解析java虚拟机

    java虚拟机是什么 "java虚拟机"可能指以下三种东西:1).抽象规范:2).一个具体的实现:3).一个运行中的虚拟机实例: java虚拟机生命周期 启动当启动一个java程序时,一个虚拟机实例诞生.虚拟机实例通过调用某个初始类的public static void main(String[] args)方法来运行一个java程序.任何拥有这样一个main方法的类都可以作为java程序运行的起点,所以必须要告诉虚拟机初始类的名称,整个程序将从它的main方法开始运行.消亡初始类的m

  • Java虚拟机工作原理

    首先我想从宏观上介绍一下Java虚拟机的工作原理.从最初的我们编写的Java源文件(.java文件)是如何一步步执行的,如下图所示,首先Java源文件经过前端编译器(javac或ECJ)将.java文件编译为Java字节码文件,然后JRE加载Java字节码文件,载入系统分配给JVM的内存区,然后执行引擎解释或编译类文件,再由即时编译器将字节码转化为机器码.主要介绍下图中的类加载器和运行时数据区两个部分. 类加载 类加载指将类的字节码文件(.class)中的二进制数据读入内存,将其放在运行时数据区

  • java 虚拟机深入了解

    什么是Java虚拟机 作为一个Java程序员,我们每天都在写Java代码,我们写的代码都是在一个叫做Java虚拟机的东西上执行的.但是如果要问什么是虚拟机,恐怕很多人就会模棱两可了.在本文中,我会写下我对虚拟机的理解.因为能力所限,可能有些地方描述的不够欠当.如果你有不同的理解,欢迎交流. 我们都知道Java程序必须在虚拟机上运行.那么虚拟机到底是什么呢?先看网上搜索到的比较靠谱的解释: 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机有自己完善的硬

随机推荐