个人博客:www.letus179.com
案例分析
A、B类中均包含静态代码块,非静态代码块以及构造器,A类是B类的父类。
public class A {
static {
System.out.print("A中静态代码块>>>");
}
{
System.out.print("A中非静态代码块>>>");
}
public A() {
System.out.print("A中构造器>>>");
}
}
public class B extends A{
static {
System.out.print("B中静态代码块>>>");
}
{
System.out.print("B中非静态代码块>>>");
}
public B() {
System.out.print("B中构造器>>>");
}
}
那么看看下面代码的运行结果。
public class ABTest {
public static void main(String[] args) {
A ab = new B();
System.out.println("\n==========================\n");
ab = new B();
}
}
执行结果为:
A中静态代码块>>>B中静态代码块>>>A中非静态代码块>>>A中构造器>>>B中非静态代码块>>>B中构造器>>>
==========================
A中非静态代码块>>>A中构造器>>>B中非静态代码块>>>B中构造器>>>
总结:
1. 同一类中:静态代码块 => 非静态代码块 => 构造器
2. 父子类中:父类 => 子类;
3. 静态代码块只在第一次实例化(new)执行了,非静态代码块在每次实例化都执行。
看执行结果,上面的3条总结都没问题,对于第3点,需要注意下:静态代码块其实不是跟着实例走的,而是跟着类走。看如下测试,通过Class.forName()动态加载类:
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("B");
}
执行结果:
A中静态代码块>>>B中静态代码块>>>
这里并没有执行实例化过程,但是静态代码块却执行了,这也证明了静态static代码块并不是跟着实例走。下面将简单介绍下类加载相关概念及过程,介绍完后再看看上面的例子,印象会更深刻。首先得了解下几个比较重要的JVM的内存概念。
jvm的几个重要内存概念
方法区
专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域。
常量池
是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
堆区
用于存放类的对象实例,如new、数组对象。
栈区
由一个个栈帧组成的后进先出的结构,主要存放方法运行时产生的局部变量、方法出口等信息。
java类的生命周期
我们编写完一个.java
结尾的源文件后,经过编译后生成对应的一个或多个.class
后缀结尾的文件。该文件也称为字节码
文件,能在java虚拟机中运行。而类的生命周期正是:从类(.class文件)被加载到虚拟机内存,到从内存中卸载为止。整个周期一共分为7个阶段:
加载,验证,准备,解析,初始化,使用,卸载
其中
- 验证,准备,解析统称为
连接
; - 加载,验证,准备,初始化,卸载,这5个的顺序是确定的。
值得注意的是,通常我们所说的类加载
指的是:加载,验证,准备,解析,初始化,这5个阶段。
加载
该阶段虚拟机的任务主要是找到需要加载的类,并把类的信息加载到jvm的方法区中,然后堆中实例化一个java.lang.Class
对象,作为方法区中这个类的信息的入口。
连接
连接阶段有三个阶段:验证,准备,解析。主要任务是加载后的验证工作以及一些初始化前的准备工作。
验证
当一个类被加载后,需要验证下该类是否合法,以保证加载的类能在虚拟机中正常运行。
准备
该阶段主要是为类的静态变量分配内存并设置为jvm默认的初始值;对于非静态变量,则不会为它们分配内存。这里静态变量的初始值,不是由我们指定的,是jvm默认的。
- 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0;
- 引用类型默认值是null;
- 常量的默认值为我们设定的值。比如我们定义final static int a = 1000,则在准备阶段中a的初始值就是1000。
解析
这一阶段的任务是把常量池中的符号引用转换为直接引用,也就是具体的内存地址。在这一阶段,jvm会将所有的类、接口名、字段名、方法名等转换为具体的内存地址。譬如:我们要在内存中找到一个类里面的一个叫call的方法,显然做不到,但是该阶段,由于jvm已经将call这个名字转换为指向方法区中的一块内存地址了,也就是说我们通过call这个方法名会得到具体的内存地址,也就找到了call在内存中的位置了。
初始化
有且仅有 5种
情况必须立即对类进行“初始化”
:
- 使用
new
关键字实例化对象、读取或设置一个类的静态字段
(被final修饰、已经在编译器把结果放入常量池的静态字段除外),以及调用一个类的静态方法
的时候; - 使用
java.lang.reflect
包的方法进行反射调用
时,若类没有进行初始化,需要先触发其初始化; - 当初始化一个类时,若其
父类
还没有进行初始化,则需要先触发其父类的初始化; - 执行
main
方法,虚拟机会先初始化其包含的那个主类
; - 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化(这一点不是很懂)。
在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。(这正好解释了案例中第3点结论)
使用
初始化
阶段的5种情况用了很强烈的限定词:有且仅有,这5种行为称为对一个类进行“主动引用”
。其他所有引用类的方法(行为)都不会对类进行初始化,称之为“被动引用”
。
《学习深入Java虚拟机》一书中列举了3个被动引用
例子,我验证了下,确实如此,不过还得到了新的启发。这里列出其中的2个例子,如下:
例子1:通过子类引用父类的静态字段,不会导致子类初始化
package classloading;
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
package classloading;
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init!");
}
}
package classloading;
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
执行结果:
SuperClass init!
123
结论:
通过子类SubClass
来引用父类SuperClass
的静态字段value
,初始化的只是父类,并不会触发子类的初始化。
例子2:常量在编译阶段会存入调用类的常量池中,不会触发定义常量的类的初始化
package classloading;
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO_WORLD = "hello world";
}
package classloading;
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_WORLD);
}
}
执行结果:
hello world
结论:
从打印的结果可以看到,并没有初始化ConstClass
类;但是从源码上看是引用了ConstClass
类的常量。因为在NotInitialization
类的编译期中,通过常量优化,已经将常量 "hello world"
存储到了NotInitialization
类的常量池中了。也就是说,NotInitialization
中引用ConstClass.HELLO_WORLD
其实是对自身常量池中常量引用。
卸载
在使用完类后,需满足下面,类将被卸载:
1. 该类所有的实例都已经被回收,也就是java队中不存在该类的任何实例;
2. 加载该类的ClassLoader已经被回收了;
3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
当上面三个条件都满足后,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程本质上就是在方法区中清空类信息,结束整个类的生命周期。
jvm加载class文件的原理机制
面试题中经常会问到JVM加载Class文件的原理机制,结合上面的分析,引用下面网上的分析,更加容易理解。
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括: 1) 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类; 2) 如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。
下面是关于几个类加载器的说明:
- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
- System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。