`

深入理解Java虚拟机读书笔记之:第7章 类型的生命周期

阅读更多
类型装载、连接与初始化
    Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。其中,装载就是把二进制形式的Java类型读入Java虚拟机中;而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接阶段分为三个子步骤——验证、准备和解析。“验证”步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。而“准备”步骤则负责为该类型分配它所需的内存,比如为它的类变量分配内存。“解析”步骤则负责把常量池中的符号引用转换为直接引用。虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它(把该符号引用转换为直接引用)。当验证、准备和(可选的)解析步骤都完成了时,该类型就已经为初始化做好了准备。在初始化期间,都将给类变量赋予适当的初始值。整个过程下如图所示:


 
    Java虚拟机严格地定义了初始化的时机。所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面这六种情形符合主动使用的要求。
  • 当创建某个类的新实例时(或者通过在字节码中执行new指令;或者通过不明确的创建、反射、克隆或者反序列化)。
  • 当调用某个类的静态方法时(即在字节码中执行invokestatic指令时)。
  • 当使用某个类或接口的静态字段,或者对该字段赋值时(即在字节码中,执行getstatic或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
  • 当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的类的方法。
  • 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。
  • 当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)。
    除上述这六种情形外,所有其他使用Java类型的方式都是被动使用,它们都不会导致Java类型的初始化。
    任何一个类的初始化都要求它的所有祖先类(而不是祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化。
 
装载
    装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须:
  • 通过该类型的完全限定名,产生一个代表该类的二进制数据流。
  • 解析这个二进制数据流为方法区内的内部数据结构。
  • 创建一个表示该类型的java.lang.Class类的实例。
    装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。
 
验证
    确认类型符合Java语言的语义,并且它不会危及虚拟机的完整性。
    Java虚拟机规范列出了虚拟机可以抛出的异常以及在何种条件下必须抛出它们。不管Java虚拟机可能遇到了什么样的麻烦,都应该有一个异常或者错误可以抛出。
    
    在装载过程中,虚拟机必须解析代表类型的二进制数据流,在这个解析期间,虚拟机大多会检查二进制数据以确保数据全部是预期的格式。可能检查魔数,确保每一个部分都在正确的位置,拥有正确的长度,验证文件不是太长或者太短,等等。虽然这些检查在装载期间完成,但它们在逻辑上仍然属于验证阶段。检查被装载的类型是否有任何问题的整个过程都属于验证。
 
    在正式的验证阶段需要完成的候选检查在下面列出。首先列出确保各个类之间二进制兼容的检查:
  • 检查final的类不能拥有子类。
  • 检查final的方法不能被覆盖。
  • 确保在类型和超类型之间没有不兼容的方法声明(比如两个方法拥有同样的名字,参数在数量、顺序、类型上都相同,但是返回类型不同)。
    在验证期间,这个类和它所有的超类型都需要确保互相之间仍然二进制兼容。
  • 检查所有的常量池入口相互之间一致(比如,一个CONSTANT_String_info入口的string_index项目必须是一个CONSTANT_Utf8_info入口的索引)。
  • 检查常量池中的所有的特殊字符串(类名、字段名和方法名、字段描述符和方法描述符)是否符合格式。
  • 检查字节码的完整性。
 
准备
    在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到达初始化阶段之前,类变量都没有被初始化为真正的初始值。(在准备阶段是不会执行Java代码的)。

主要类型和引用类型的默认初始值

类 型

默认初始值

int

0

long

0L

short

(short)0

char

'\u0000'

byte

(byte)0

boolean

false

reference

null

float

0.0f

double

0.0d

 
解析
    解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
 
初始化
    为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。这里的“正确”初始值指的是程序员希望这个类变量所具备的起始值。
 
    所有的类变量初始化语句和类型的静态初始化语句都被Java编译器收集在一起,放到一个特殊的方法中。对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的Java class文件中,这个方法被称为“<clinit>”。这种方法只能被Java虚拟机调用。
 
    第一个被初始化的类永远是Object。超类总是在子类之前被初始化。
 
1.<clinit>()方法
    思考下面的类的例子:
class Example1c {
    static int width;
    static int height = (int) (Math.random() * 2.0);
    // This is the static initializer
    static {
        width = 3 * (int) (Math.random() * 5.0);
    }
}
 
    Java编译器生成了下面的<clinit>()方法(命令:javap -c Example1c):
   0:   invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   3:   ldc2_w  #8;         //double 2.0d,将double常量2.0入栈
   6:   dmul                //将前两步的两个值出栈,相乘后将结果入栈
   7:   d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   8:   putstatic       #5; //Field height:I,将上一步的结果出栈,存储到类变量height中
   11:  iconst_3            //将int常量3入栈
   12:  invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   15:  ldc2_w  #10;        //double 5.0d,将double常量5.0入栈
   18:  dmul                //将前两步的两个值出栈,相乘后将结果入栈
   19:  d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   20:  imul                //将偏移量11入栈的值和上一步的值出栈,相乘后将结果入栈
   21:  putstatic       #7; //Field width:I,将上一步的结果出栈,存储到类变量width中
   24:  return              //<clinit>方法返回void
 
    其中,偏移量0~8是对类变量height的初始化;11~24是执行静态代码块的初始化。
 
    并非所有的类都需要在它们的class文件中拥有一个<clinit>()方法。以下情况不会有<clinit>()方法:
  • 如果类没有声明任何类变量,也没有静态初始化语句
  • 如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们
  • 如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式
    下面是一个不会产生<clinit>()方法的例子:
class Example1d {
    static final int angle = 35;
    static final int length = angle * 2;
}
 
    angle和length字段并非类变量,它们是常量,被Java编译器特殊处理了。Java虚拟机在使用它们的任何类的常量池或者字节码流中直接存放的是它们表示的常量的int值。
 
    下面是一个同时使用一个常量和一个其他类的类变量的例子:
class Example1e {
    // The class variable initializer for symbolicRef uses a symbolic
    // reference to the size class variable of class Example1a
    static int symbolicRef = Example1a.size;
    // The class variable initializer for localConst doesn't use a
    // symbolic reference to the length field of class Example1d.
    // Instead, it just uses a copy of the constant value 70.
    static int localConst = Example1d.length * (int) (Math.random()
        * 3.0);
}
 
class Example1a {
    // "= 3 * (int) (Math.random() * 5.0)" is the class variable
    // initializer
    static int size = 3 * (int) (Math.random() * 5.0);
}
 
    Java编译器为类Example1e生成了下面的<clinit>()方法(命令:javap -c Example1e):
   0:   getstatic       #9;  //Field Example1a.size:I,将Example1a.size的值入栈,使用一个符号引用指向类Example1a的size字段
   3:   putstatic       #10; //Field symbolicRef:I,将上一步的结果出栈,存储到类变量symbolicRef中
   6:   bipush  70           //将常量Example1d.length的拷贝入栈
   8:   invokestatic    #8;  //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   11:  ldc2_w  #11;         //double 3.0d,将double常量3.0入栈
   14:  dmul                 //将前两步的两个值出栈,相乘后将结果入栈
   15:  d2i                  //将上一步的结果出栈,并强制转换为int类型,再入栈
   16:  imul                 //将偏移量6入栈的值和上一步的值出栈,相乘后将结果入栈
   17:  putstatic       #7;  //Field localConst:I,将上一步的结果出栈,存储到类变量localConst中
   20:  return               //<clinit>方法返回void
 
    其中,偏移量0~3是对类变量symbolicRef的初始化;6~20是对类变量localConst的初始化。
 
    所有在接口中声明的隐式公开(public)、静态(static)和最终(final)字段都必须在字段初始化语句中初始化。如果接口包含任何不能在编译时被解析成为一个常量的字段初始化语句,接口就会拥有一个<clinit>()方法。下面是一个例子:
interface Example1f {
    int ketchup = 5;
    int mustard = (int) (Math.random() * 5.0);
}
 
    Java编译器为接口Example1f生成了下面的<clinit>()方法(命令:javap -c Example1f):
   0:   invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   3:   ldc2_w  #7;         //double 5.0d,将double常量5.0入栈
   6:   dmul                //将前两步的两个值出栈,相乘后将结果入栈
   7:   d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   8:   putstatic       #5; //Field mustard:I,将上一步的结果出栈,存储到类变量mustard中
   11:  return              //<clinit>方法返回void
 
2.主动使用和被动使用
    使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用。比如,类中声明的字段可能会被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的类引用 。对于子类、子接口和实现了接口的类来说,这就是被动使用——使用它们并不会触发它们的初始化。只有当字段的确是被类或者接口声明的时候才是主动使用。下面的例子说明了这个原理:
class NewParent {
    static int hoursOfSleep = (int) (Math.random() * 3.0);
    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {
    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);
    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

class Example2 {
    // Invoking main() is an active use of Example2
    public static void main(String[] args) {
        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        int hours = NewbornBaby.hoursOfSleep;
        System.out.println(hours);
    }
    static {
        System.out.println("Example2 was initialized.");
    }
}

 

    在上面的例子中,执行Example2的main()方法只会Example2和NewParent被初始化。NewbornBaby没有被初始化,也不需要被装载。执行结果:
Example2 was initialized.
NewParent was initialized.
0

 

    如果一个字段既是静态(static)的又是最终(final)的,并且使用一个编译时常量表达式初始化,使用这样的字段,就不是对声明该字段的类的主动调用。下面是一个说明这种对静态final字段特殊处理的例子:
interface Angry {
    String greeting = "Grrrr!";
    int angerLevel = Dog.getAngerLevel();
}

class Dog {
    static final String greeting = "Woof, woof, world!";
    static {
        System.out.println("Dog was initialized.");
    }
    static int getAngerLevel() {
        System.out.println("Angry was initialized");
        return 1;
    }
}

class Example3 {
    // Invoking main() is an active use of Example3
    public static void main(String[] args) {
        // Using Angry.greeting is a passive use of Angry
        System.out.println(Angry.greeting);
    
        // Using Dog.greeting is a passive use of Dog
        System.out.println(Dog.greeting);
    }
    static {
        System.out.println("Example3 was initialized.");
    }
}

 

 

    运行Example3程序,执行结果:
Example3 was initialized.
Grrrr!
Woof, woof, world!
 
 
对象的生命周期
 
类实例化
    在Java程序中,类可以被明确或者隐含地实例化。实例化一个类有四种途径:明确地使用new操作符;调用Class或者java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;或者通过java.io.ObjectInputStream类的getObject()方法反序列化。
    下面的例子中演示了其中三种创建新的类实例的方法:
class Example4 implements Cloneable {
    Example4() {
        System.out.println("Created by invoking newInstance()");
    }
    Example4(String msg) {
        System.out.println(msg);
    }
    public static void main(String[] args)
        throws ClassNotFoundException, InstantiationException,
        IllegalAccessException, CloneNotSupportedException {
        // Create a new Example4 object with the new operator
        Example4 obj1 = new Example4("Created with new.");
        // Get a reference to the Class instance for Example4, then
        // invoke newInstance() on it to create a new Example4 object
        Class myClass = Class.forName("Example4"); 
        Example4 obj2 = (Example4) myClass.newInstance();
        // Make an identical copy of the the second Example4 object
        Example4 obj3 = (Example4) obj2.clone();
    }
}
 
    执行结果:
Created with new.
Created by invoking newInstance()
 
    还有几种情况下对象会被隐含地实例化:
  • 在任何Java程序中第一个隐含实例化对象可能就是保存命令行参数的String对象(main(String[] args))。每一个命令行参数都会有一个String对象的引用
  • 对于Java虚拟机装载的每一个类型,它会暗中实例化一个Class对象来代表这个类型
  • 当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串
  • 执行包含字符串连接操作符的表达式产生对象
 
垃圾收集和对象的终结
    垃圾收集器(最多)只会调用一个对象的终结方法一次——在对象变成不再被引用的之后的某个时候,在占据的对象被重用之前。如果终结方法代码执行后,对象重新被引用了(复活了),随后再次变得不被引用,垃圾收集器不会第二次调用终结方法。
 
卸载类型
    虚拟机装载、连接并初始化类,使程序能使用类,当程序不再引用它们的时候可选地卸载它们。
 
(注:本文中<clinit>()方法的内容是在JDK1.6环境下使用javap命令生成的,与原书提供的内容有细微的差别。另外,原书对<clinit>()方法的各行使用英文注释,我使用中文注释
 
    在此说明,本系列文章的内容均出自《深入理解Java虚拟机》一书,除了极少数的“注”或对内容的裁剪整理外,内容原则上与原书保持一致。由于这是一本原理性的书籍,本人不想因为自己能力与理解的问题对大家造成误解,所以除了对原书内容的裁剪整理之外,基本不做任何内容的延伸思考与扩展。
    另外,如果您对本系列文章的内容感兴趣,建议您去阅读原版书籍,谢谢!
 
(转载请注明来源:http://zhanjia.iteye.com/blog/1877236)
  • 大小: 10.2 KB
1
1
分享到:
评论
发表评论

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

相关推荐

    深入java虚拟机笔记

    - **Java虚拟机的生命周期**:JVM的生命周期从启动到关闭的过程,涉及到类的装载、初始化等多个阶段。 - **Java虚拟机的体系结构**:JVM的体系结构主要包括内存区域划分、执行引擎等方面。 - **数据类型**:JVM支持...

    Java虚拟机讲解笔记.docx

    Java虚拟机(JVM)是运行Java字节码的虚拟环境,它位于操作系统之上,硬件之下,提供了一层软件抽象,使得Java程序可以在多种平台上运行而无需重新编译。JVM的核心功能包括内存管理、垃圾收集、安全性和平台独立性。...

    毕向东Java笔记

    - 局部变量:定义在方法或语句块内的变量,生命周期与该方法或语句块相同。 - 成员变量:定义在类内部、方法外部的变量,与对象关联。 - 静态变量:定义时使用`static`关键字,与类关联。 8. **常量**: - 初始...

    java面向对象和第一章整理笔记

    它的设计目标是“一次编写,到处运行”(Write Once, Run Anywhere, WORA),通过Java虚拟机(JVM)实现了这一目标,可以在不同操作系统上运行Java程序。 2. **与.NET框架的区别**: - 平台独立性:Java代码编译成...

    java笔记

    学习如何定义和使用类,理解对象的生命周期,以及如何通过继承和多态来实现代码复用,是Java学习的关键部分。 3. **异常处理**: Java中的异常处理机制允许程序在遇到错误时进行恢复或记录错误信息。了解如何使用try...

    Java基础笔记_Java基础笔记_Java笔记_worthnwg_

    - **变量**:声明变量并赋值,理解其作用域和生命周期。 - **运算符**:包括算术、比较、逻辑、位操作和三元运算符等。 - **控制结构**:if语句、switch语句、for循环、while循环和do...while循环。 - **方法**...

    传智播客毕向东Java基础课堂笔记

    #### 第一章:编程基础 1. **Java的特性和优势** - **简单性**:Java的设计使得语法清晰、简洁,易于学习和理解。 - **面向对象**:支持封装、继承、多态等面向对象编程特性,有助于构建灵活且可扩展的软件系统。...

    传智播客Java_SE基础毕向东老师全程笔记

    多线程是Java并发编程的基础,本章探讨了如何在Java中创建与管理线程,理解线程生命周期,以及如何实现线程同步与通信,提高程序的执行效率与响应速度。 #### 第六章:常用类API 本章涵盖了Java标准库中的重要类与...

    java学习笔记(内部资料)

    Java学习笔记是深入理解并掌握Java编程语言的重要资源,尤其对于初学者和准备Java面试的开发者来说,这些内部资料提供了丰富的知识和实践经验。本篇将根据"corejava"这一核心标签,结合压缩包中的文件"java学习笔记...

    java实战经典学习笔记

    ### Java实战经典学习笔记知识点概览 #### 一、Java概述及开发环境搭建 - **Java概述** - Java是一种广泛使用的高级编程语言,由Sun Microsystems于1995年发布。 - Java的设计目标是“一次编写,到处运行”,这...

    scjp笔记 考过scjp的学生的第一手资料 强烈推荐

    10. **JVM原理**:理解Java虚拟机的工作原理,包括类加载机制、类的生命周期、JVM内存模型(堆、栈、方法区、本地方法栈)、JVM优化技术。 11. **Java标准库**:熟悉常用Java API,如String类、Date/Calendar、...

    java学习笔记

    Java程序的运行依赖于Java虚拟机(JVM),这意味着只要目标系统安装了相应的JVM,Java程序就能在其上运行。这种特性确保了Java“一次编写,到处运行”的跨平台性。 - **开发步骤**: 1. **编写源文件**:使用`.java`...

    Java入门学习笔记

    - **平台无关性:** Java编写的程序可以运行在任何支持Java虚拟机(JVM)的平台上。 **1.2 运行原理** Java程序的执行过程主要包括编译、加载、运行三个阶段: - **编译:** 将Java源代码编译成字节码(.class文件)。...

    常用文档Java4.rar

    【Java开发常用资料】这份压缩包“Java4.rar”包含了丰富的Java编程学习资源,涵盖了从基础知识到高级框架的多个方面,旨在帮助开发者深入理解和掌握Java技术。以下是对这些文件内容的详细解读: 1. **JVM(Java...

    JAVASE 笔记

    理解线程的生命周期、同步机制(如synchronized关键字、wait/notify机制)以及并发工具类(如ExecutorService、Semaphore)是提高程序并发性能的关键。 8. **反射**:Java反射机制允许在运行时检查类的信息,动态...

    2023年JAVA课堂笔记基础详细解释.doc

    - **栈内存**:存放基本类型变量和方法调用时的局部变量,内存分配速度快,生命周期短。 以上是Java编程的基础知识,包括开发环境设置、程序编译与运行、面向对象特性、文档生成、数据类型以及内存管理等方面。...

    Java Instrumentation笔记

    ### Java Instrumentation 深入理解 #### 一、引言 Java Instrumentation 是 Java 平台的一个强大特性,允许开发者在不修改源代码的情况下,动态地修改正在运行的 Java 应用程序的行为。这一特性最早出现在 Java SE...

    《疯狂Android讲义》02章读书笔记(含源码)

    1. Activity生命周期:了解Activity的创建、运行、暂停、停止和销毁等状态及其转换,是理解应用行为的关键。 2. Intent:Intent是Android中启动组件和传递数据的主要方式,它可以启动Activity、Service,或发送广播...

    JVM历史发展和内存回收笔记

    Java虚拟机(JVM)是Java程序运行的基础,它的历史发展和内存回收机制是Java开发者必须深入了解的关键领域。本文将详细探讨JVM的发展历程以及内存管理中的垃圾回收机制。 一、JVM的历史发展 1. **早期阶段**:1995...

    JSP-Servlet学习笔记第2版.pdf

    JSP页面在第一次被请求时会翻译成一个Servlet类,然后由Java虚拟机执行该类的代码以生成响应。 2. JSP的生命周期:包括JSP页面的加载、初始化、处理客户请求、销毁等各个阶段,每个阶段涉及的生命周期方法如jspInit...

Global site tag (gtag.js) - Google Analytics