`

java 类的加载、链接和初始化(静态属性的加载顺序)

    博客分类:
  • java
 
阅读更多
Java类的加载
Java类的加载是由类加载器来完成的。
一般来说,类加载器分成两类:启动类加载器(bootstrap)用户自定义的类加载器(user-defined)
两者的区别在于启动类加载器是由JVM的原生代码实现的,而用户自定义的类加载器都继承自Java中的java.lang.ClassLoader 类。
在用户自定义类加载器的部分,一般JVM都会提供一些基本实现。应用程序的开发人员也可以根据需要编写自己的类加载器。JVM中最常使用的是系统类加载器(system),它用来启动Java应用程序的加载。通过java.lang.ClassLoader的getSystemClassLoader() 方法可以获取到该类加载器对象。
类加载器需要完成的最终功能是定义一个Java类,即把Java字节代码转换成JVM中的java.lang.Class类的对象。但是类加载的过程并不是这么简单。
Java类加载器有两个比较重要的特征:层次组织结构和代理模式。
层次组织结构指的是每个类加载器都有一个父类加载器,通过getParent() 方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。
代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。
由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。前者称为初始类加载器,而后者称为定义类加载器。
两者的关联在于:一个Java类的定义类加载器是该类所导入的其它Java类的初始类加载器。比如类A通过import导入了类 B,那么由类A的定义类加载器负责启动类B的加载过程。
一般的类加载器在尝试自己去加载某个Java类之前,会首先代理给其父类加载器。当父类加载器找不到的时候,才会尝试自己加载。这个逻辑是封装在java.lang.ClassLoader类的loadClass() 方法中的。一般来说,父类优先的策略就足够好了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是Servlet规范 推荐的做法。比如,Apache Tomcat 为每个Web应用都提供一个独立的类加载器,使用的就是自己优先加载的策略。IBM WebSphere Application Server 则允许Web应用选择类加载器使用的策略。
类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的二进制名称 ,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出java.lang.ClassCastException 。这个特性为同样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求同一名称的Java类的不同版本在JVM中可以同时存在。通过类加载器就可以满足这种需求。


Java类的链接
Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。
在链接之前,这个类必须被成功加载。
类的链接包括验证、准备和解析等几个步骤。
验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError 错 误。
准备过程则是创建Java类中的静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其它类或接口的形式引用,包 括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。
不同的JVM实现可能选择不同的解析策略。
一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引 用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。考虑下面的代码:

public class LinkTest {   

   public static void main(String[] args) {       

      ToBeLinked toBeLinked = null;       

      System.out.println("Test link.");   

   }

}


类 LinkTest引用了类ToBeLinked,但是并没有真正使用它,只是声明了一个变量,并没有创建该类的实例或是访问其中的静态域。
在Oracle的JDK 6中,如果把编译好的ToBeLinked的Java字节代码删除之后,再运行LinkTest,程序不会抛出错误。这是因为ToBeLinked类没有

被真正用到,而Oracle的JDK 6所采用的链接策略使得ToBeLinked类不会被加载,因此也不会发现ToBeLinked的Java字节代码实际上是不存在的。如果把代码改成

ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相同的方法运行,就会抛出异常了。因为这个时候ToBeLinked这个类被真正使用到了,会需要加载这个类。


Java类的初始化
当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域。
在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代 码块和初始化静态域。考虑下面的代码:

public class StaticTest {   

   public static int X = 10;   

   public static void main(String[] args) {       

      System.out.println(Y); //输出60   

   }   

   static {       

      X = 30;   

   }  

   public static int Y = X * 2;

}

在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行。
因此变量X的值首先初始化成10,后来又被赋值成30;而变量Y的值则被初始化成60。
Java类和接口的初始化只有在特定的时机才会发生,这些时机包括:
创建一个Java类的实例。

MyClass obj = new MyClass()
调用一个Java类中的静态方法。如
MyClass.sayHello()
给Java类或接口中声明的静态域赋值。

MyClass.value = 10
访问Java类或接口中声明的静态域,并且该域不是常值变量。如
int value = MyClass.value
在顶层Java类中执行assert语句。
通过Java反射API也可能造成类和接口的初始化。需要注意的是,当访问一个Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才

会被初始化。考虑下面的代码:

class B {   

   static int value = 100;   

   static {       

      System.out.println("Class B is initialized."); //输出   

   }

}

class A extends B {   

   static {       

      System.out.println("Class A is initialized."); //不会输出   

   }

}

public class InitTest {   

   public static void main(String[] args) {       

      System.out.println(A.value); //输出100   

   }

}


在上述代码中,类InitTest通过A.value引用了类B中声明的静态域value。由于value是在类B中声明的,只有类B会被初始化,而类A则不会被初始化。
但如果将类B修改如下:

class B {   

   final static int value = 100;   

   static {       

      System.out.println("Class B is initialized."); //输出   

   }

}


则最后的输出结果又当如何呢?

只输出 100

创建自己的类加载器

在 Java应用开发过程中,可能会需要创建应用自己的类加载器。
典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名 Java类的隔离等。
创建自己的类加载器并不是一件复杂的事情,只需要继承自java.lang.ClassLoader类并覆写对应的方法即可。
java.lang.ClassLoader中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:

defineClass() :这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
findLoadedClass() :这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
findClass() :这个方法用来根据名称查找并加载Java类。
loadClass() :这个方法用来根据名称加载Java类。
resolveClass() :这个方法用来链接一个Java类。

这里比较 容易混淆的是findClass()方法和loadClass()方法的作用。
前面提到过,在Java类的链接过程中,会需要对Java类进行解析,而解析可能会导致当前Java类所引用的其它Java类被加载。在这个时候,JVM就是通过调用当前类的定义类加载器的loadClass()方法来加载其它 类的。findClass()方法则是应用创建的类加载器的扩展点。应用自己的类加载器应该覆写findClass()方法来添加自定义的类加载逻辑。

loadClass()方法的默认实现会负责调用findClass()方法。
前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是封装在loadClass()方法中的。如果希望修改此策略,就需要覆写loadClass()方法。
下面的代码给出了自定义的类加载的常见实现模式:
public class MyClassLoader extends ClassLoader {   

   protected Class<?> findClass(String name) throws ClassNotFoundException {       

      byte[] b = null; //查找或生成Java类的字节代码       

	  return defineClass(name, b, 0, b.length);   

   }

}



转自:http://blog.csdn.net/xhh198781/article/details/6338458
分享到:
评论
发表评论

文章已被作者锁定,不允许评论。

相关推荐

    Java静态初始化块和对象初始化块

    总之,理解并有效地使用静态和对象初始化块是Java开发中的关键技能,它们可以帮助我们更好地控制类和对象的初始化过程,确保代码的高效性和正确性。在实际编程中,我们应该根据需求谨慎选择合适的初始化策略,以优化...

    java面试题静态加载顺序构造方法

    4.知道了static的作用,那么X类被加载,那么就会先执行X类的静态属性和静态语句块(static),执行先后顺序看谁在前面就先执行谁。只在此时执行,以后都不会。 5.所以一个输出结果为tttt,没问题了吧。 6.X类的...

    Java初始化块Java初始化块.doc

    - 类初始化:在类初始化阶段,静态属性的内存被分配,然后执行静态初始化块来初始化静态属性。这个过程是自顶向下的,从`java.lang.Object`开始,逐级执行每个类的静态初始化块。 总结起来,Java初始化块是Java中...

    java类中静态域、块,非静态域、块,构造函数的初始化顺序

    - **静态块**:在类加载时执行的代码块,主要用于初始化静态域。 - **非静态块(实例初始化块)**:在每次创建类的新实例时执行的代码块,用于初始化非静态域。 - **构造函数**:用于初始化新创建的对象的方法。 ##...

    java 继承关系的加载顺序

    初始化阶段执行类的静态初始化块和父类的初始化块。只有当类的初始化方法被执行时,才会触发子类的初始化。这意味着,如果子类没有自己的初始化块或静态变量,那么子类不会在这个阶段进行任何操作。 7. 实例化过程...

    对象初始化流程梳理对象初始化流程梳理

    Java中的对象初始化流程是编程实践中一个非常重要的概念,它涉及到类加载、静态初始化块、实例初始化块、构造器等多个方面。下面将详细解释这个过程。 首先,对象初始化流程的起点是程序的入口点,即`main`方法。当...

    Java中static静态变量的初始化完全解析

    Java中的静态变量(static变量)是在类加载时初始化的,而不是在对象创建时。静态变量属于类,而不属于任何特定的对象,因此它们是共享的,所有类实例都可以访问。了解静态变量的初始化顺序对于理解和避免潜在的编程...

    Java类的基本运行顺序

    本文将详细解析Java类的基本运行顺序,包括加载、初始化、执行等关键步骤,以及如何通过源码和工具进行分析。 1. 类的加载: 当Java虚拟机(JVM)需要使用一个类时,它会通过类加载器首先查找并加载这个类的字节码...

    spring加载顺序讨论

    在Spring框架中,加载顺序是理解应用程序启动过程的关键部分,涉及到bean的实例化、初始化以及依赖注入等多个环节。本文将详细探讨Spring加载顺序,并结合`@PostConstruct`、`构造方法`以及`@Autowired`等关键注解...

    Java基础练习题练习下静态块的用处

    通过上述知识点,我们可以理解Java中的静态块是类加载过程中的一个重要组成部分,它在初始化静态变量和管理类级别资源等方面发挥着关键作用。在学习Java基础时,掌握静态块的使用能够帮助我们编写更加高效和整洁的...

    Java中初始化块详解及实例代码

    Java中初始化块是Java语言中的一种特殊的代码块,它可以在类加载或对象创建时执行某些操作。本文将详细介绍Java中初始化块的概念、种类、特点和应用场景。 什么是初始化块 初始化块是Java语言中的一种特殊的代码块...

    构造函数与静态块的执行顺序

    静态块常用于初始化静态变量或执行一次性的初始化操作。 ### 继承中构造函数与静态块的执行顺序 在Java中,当一个类继承自另一个类时,实例化子类对象时构造函数与静态块的执行顺序遵循以下规则: 1. **静态块的...

    浅谈Java 类中各成分加载顺序和内存中的存放位置

    Java 类的加载过程是程序运行中的重要环节,它涉及到类...总结,理解Java类的加载顺序和内存布局对于优化代码性能和避免内存泄漏至关重要。通过合理的初始化策略,我们可以确保程序高效地运行,并有效地管理内存资源。

    java 静态块实例块构造器调用

    静态块是Java类中用于初始化静态变量的代码块。它们在类加载时执行,且只执行一次。无论创建多少个类的实例,静态块只会被执行一次。这是因为在Java中,类的静态成员是与类相关的,而非类的实例。 ```java public ...

    Java虚拟机类装载:原理、实现与应用

    链接阶段则包括校验、准备和解析,校验确保二进制数据的正确性,准备阶段为类的静态变量分配内存并初始化,解析则将符号引用转化为直接引用。初始化阶段激活类的静态变量初始化代码和静态代码块。这一过程遵循一定的...

    java中静态与非静态的区别

    此外,它也可以初始化静态成员,但不包括静态只读字段。 2. **执行时机**: - **静态构造函数**在.NET运行库加载类时执行,通常是在第一次调用类成员之前。 - **实例构造函数**在创建对象实例时执行。 3. **初始...

    Java面试题解惑系列

    对于静态变量和静态初始化块之间,以及非静态变量与非静态初始化块之间的顺序,则取决于它们在类中的定义顺序。例如: ```java public class TestOrder { // 静态变量 public static TestA a = new TestA(); /...

    Java中的static关键字

    Java 中的 static 关键字 ...在 Java 中,类装载器把一个类装入 Java 虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的。

    Java常见笔试、面试题目深度剖析,方法重写详解、静态代码块与构造方法执行顺序问题

    这个过程确保了对象在被创建之前,其类的静态属性和父类的静态属性已经被正确初始化。同时,通过构造方法链确保了父类的状态在子类状态之前完成初始化。 在实际面试或笔试中,理解这些基本概念并能够熟练运用是非常...

Global site tag (gtag.js) - Google Analytics