zhaoyu@home:~$

java执行过程

JVM启动

JVM通过调用Main方法执行程序。我们拿Test类来具体,大致步骤如下:

加载class

开始尝试执行Test的main方法时,发现Test类还没有加载,也没有这个类的二进制表示。jVM就会使用classloader 尝试寻找类的二进制表示。没找到则抛出一个Error。 一个正常的classloader拥有如下特性:

  1. 对于相同的类名,classloader永远返回相同的类对象。
  2. 如果一个classloader L1将类C托管给L2处理,那么对于C的直接父类或者父接口T,或C的字段类型,或C的方法的参数类型,或返回值类型, L1和L2都返回相同的类对象。

加载过程

ClassLoader及其子类负责类的记载过程。
ClassLoader不同的子类实现不同的加载策略。特殊情况下,一个类加载器可能缓存类或接口的二进制表示,当使用到或者几种加载一组关联类时 才获取他们。
在类加载期间,LinkageError的子类ClassCircularityError,ClassFormatError,NoClassDefFoundError等可能会被抛出。 加载也涉及到新的数据结构分配空间,所以OutOfMemoryError可能会被抛出。

链接Test:验证,准备,处理(可选)

在加载完Test后,在main调用前还需要执行链接,链接是将类或接口的二进制形式结合到JVM运行时状态中的过程。 链接包括了验证,准备,和处理阶段。
链接阶段涉及到新数据结构的空间分配,所以OutOfMemoryError可能会被抛出。

  • 验证
    检查Test的二进制表示格式是否正确,检查代码是否遵从语义需求和java语言需求即JVM需求,如果有问题被发现,会抛出一个Error。 验证异常后,VerifyError会被抛出。
  • 准备
    准备包括了静态内存的分配及JVM内部用到的内存分配,如方法表。准备阶段会创建静态字段(类变量和常量),并设置字段默认值。该阶段 不会执行任何代码,Initializer中设置的静态字段会在初始化阶段执行,不会在这个阶段。
  • 处理
    处理是检查Test引用其他类或接口的符号引用,及加载其他类和接口检查引用正确性的过程。处理符号引用的阶段可能很快就反生,也可能在 所有符号引用都被处理后再完成。也可能在一个符号引用被使用到的时候才去处理,执行延迟加载。 处理阶段抛出的大多是IncompatibleClassChangeError的子类,如:IllegalAccessError,InstantiationError, NoSuchFieldError,NoSuchMethodError。

初始化

执行main之前,还需要完成类的初始化。初始化包括了静态初始化和变量初始化。且在Test初始化之前,其父类需要递归地先执行初始化。 Test隐藏的父类为Object,所以需要先执行Object的初始化。 一个类的初始化过程包括执行类中声明的static initializers 和 静态字段的 initializers。
接口的初始化包括在接口中声明的常量的initializers。

初始化发生的时间

一个类或接口T在发生下列情况时,会立即初始化:

  1. 一个类的实例被创建。
  2. 静态方法被调用。
  3. 静态字段被赋值。
  4. 静态字段被使用且字段不是常量。
  5. T是一个顶级类,T内嵌套的assert语句被执行。

当一个类初始化后,它的父类会被初始化,所有有默认方法的接口会被初始化。
一个接口被初始化,不会导致它的所有父接口被初始化。
一个static字段的引用只会让真正声明它的类或者接口初始化。而不管它的子类或子接口引用了它。 调用反射也会引起类或接口的初始化。

初始化的详细过程

因为java程序是多线程的,需要慎重处理同步过程。多个线程可能会同时加载一个类,而一个类或接口的初始化可能递归依赖它本身的初始化。如A中一个变量的 初始化调用类B中的一个方法,该方法又返回调用A中的方法。JVM负责处理同步和递归初始化,过程如下: 该过程假设Class对象已经被验证和准备完成,并且处于如下四个状态之一:

  1. Class对象被验证和准备完成,还未被初始化。
  2. Class对象正在被线程T初始化。
  3. Class对象初始化完成并可以使用。
  4. Class对象处于错误状态,可能是初始化失败了。

每个类或者接口C,有唯一的一个所LC。C的初始化过程如下:

  1. 在LC锁同步,等待当前线程获取LC锁。
  2. 如果发现C的初始化已经被其他线程正在执行,那么释放LC锁,阻塞当前线程获取锁,直到其他线程处理完成。然后重复当前步骤。
  3. 如果发现C的初始化被当前线程执行,那么这必然是一个递归初始化请求,释放LC并正常执行。
  4. 如果C已经被初始化,释放LC并正常执行。
  5. 如果C初始化发生错误,释放LC锁并抛出NoClassDefFoundError。
  6. 否则,当前线程处理的C的初始化,然后释放LC。然后初始化是常量的static字段。
  7. 如果C是类而不是接口,它的父类还没有初始化,则递归地按照上面的过程初始化它的父类及有默认方法的父接口。
  8. 通过查询C的类加载器,确定C是否开启了assert。
  9. 按文本顺序执行类变量初始化和静态初始化,及接口的字段初始化。
  10. 如果初始化执行完成,然后获取LC,标记C初始化完成,通知所有等待的线程,释放LC。
  11. 否则,初始化意外结束,抛出异常。
  12. 获取LC,标记C为出错状态,通知所有等待的线程,释放LC。

调用Test.main

最终,在所有的初始化完成后,main方法会被调用。

新类实例的创建

一个类实例被显示创建时,会引起类被初始化。 一个类的新实例可能被隐式地通过下面几种情况被创建:

  1. 加载一个包含String文本的类或者接口会创建一个新的String对象表示一个文本。
  2. 引起封箱转换的操作,会创建一个包装类的对象。
  3. 非常量的表达式的string的+操作会创建一个新的String对象保存结果。String的+操作也可能对基本类型创建包装类对象。
  4. 一个方法引用表达式或一个lambda表达式可能创建实现了函数接口类型的类的实例。

当一个类的实例被创建时,会为类中声明的实例变量、所有父类中声明的实例变量(包括隐藏实例变量)分配内存

class实例的完结

Object有一个protected方法finalize;finalize方法可以被对象的finalizer调用。在对象的空间被GC收集前,JVM会调用对象的finalizer。 在finalizer中,我们有机会对GC不能自动回收的资源做释放。 java调用finalizer是无序的,不定时的,如果在finalization期间一个未捕获异常被抛出,异常会被忽略。 happen-before,任何对象的finalize方法都在构造方法之后调用。 Object的finalize方法没有执行任何动作,只是为了让class能够为其调用finalize方法。 包java.lang.ref描述了和GC交互的弱引用。

对象的状态

每个对象有两个属性:第一,reachable, finalizerreachable, or unreachable。第二,unfinalized, finalizable, or finalized。

  • reachable 从存活线程在任何可能的计算中可以访问到的对象。
  • finalizer-reachable 从任何可以完结的对象通过某些路径可以访问到。不能从存活线程访问到。
  • unreachable 任何途径不可以访问到的对象。

  • unfinalized 从来没有自动调用过它finalizer。
  • finalized 已经自动调用过finalizer。
  • finalizable 从来没有自动调用过它的finalizer,但是JVM最终会自动调用它的finalizer。

类和接口的卸载

类或接口可以被定义它的classloader卸载,被bootstrap类加载器加载的类无法被卸载。

程序的退出

一个程序在如下情况下会停止运行并退出:

  • 所有的非守护线程终结。
  • 类Runtime或System的exit方法被调用,且exit操作没有被安全管理器(security manager)禁止。