首先来看下类的生命周期
其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一样。
在这个阶段,java虚拟机主要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流(没有指明必须从哪里获取)
- 解析成运行时数据,即instanceKlass实例,存放在方法区
- 堆内存中实例化一个java.lang.Class类的class对象,即instanceMirrorKlass实例。这个对象将作为程序访问方法区中的类型数据的外部接口。
对于数组类而言,情况则有所不同,数组类本身不通过类加载器创建,它是由java虚拟机直接在内存中动态构造出来的。当然,数组类去掉维度后的元素类型还是需要靠类加载器去创建。
这个阶段是开发人员可控性最强的阶段。
这一阶段的目的是确保class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身安全。它包括:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
为静态变量分配内存并设置变量初始值的阶段,注意这里说的是静态变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在堆中。并且这里的初始值指的是数据类型的零值
特殊情况:
public static final int value=123;
如果字段被final修饰,那么在编译的时候就会给该字段添加ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。通过idea插件jclasslib可以查看字段。
将常量池中的符号引用转换为直接引用
- 符号引用:静态常量池的索引
- 直接引用:内存地址
常量池:
- 静态常量池(class文件中的常量池)
- 运行时常量池(将class文件中的常量池载入虚拟机,并放入方法区,即class文件的常量池表构造而成)
- 字符串常量池(1.8在堆中)
解析后的信息存储在ConstantPoolCache类实例中
1、类或接口的解析
2、字段解析
3、方法解析
4、接口方法解析
何时解析?
1、类加载以后马上解析
2、使用的时候(也就是初始化的时候,会判断是否解析过)
openjdk使用第二种方法,在执行特定指令码之前解析(见深入理解jvm虚拟机p273)
在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的晚绑定(运行时绑定、动态绑定)
如何避免重复解析
借助缓存->ConstantPoolCache(运行时常量池的缓存),是一个Hashtable结构,
key:常量池的索引,value:ConstantPoolCacheEntry
如下代码:
public class Test1 {
public static void main(String[] args) {
System.out.println(Sub.value);
while (true);
}
}
class Super{
public static int value="123";;
static {
System.out.println("Super init...");
}
}
class Sub extends Super{
static {
System.out.println("Sub init...");
}
}
通过jclasslib查看:
0 ldc #2 <123>
2 putstatic #3 <com/cxylk/partone/Super.value>
5 getstatic #4 <java/lang/System.out>
8 ldc #5 <Super init...>
10 invokevirtual #6 <java/io/PrintStream.println>
13 return
其中字符串value的key就是常量池索引2,value就是将该String类型的字段包装成ConstantPoolCacheEntry
在该阶段,java虚拟机才真正开始执行类中编写的java代码,将主导权移交给应用程序。
初始化阶段就是执行类构造器<clinit>
方法的过程。
这里需要分清<clinit>
方法和<init>
方法:
-
<clinit>
():由编译器自动收集类中的所有静态变量的赋值动作(注意是赋值动作,这也是为什么一个静态变量如果没有对它赋值,cliint方法中就没有该变量出现的原因)和静态语句块中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。例:public class Test{ static{ i=0;//赋值可以正常编译通过 System.out.print(i)//报错“非法向前引用” } static int i=1; }
java虚拟机会保证在子类的
<clinit>
()方法执行前,父类的<clinit>
()方法已经执行完毕。也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。 -
<init>
():实例构造器方法,就是构造函数,它会显示的调用父类构造器,这是和<clinit>
方法所不同的。
总结如下:
1、如果没有静态属性、静态代码段,就不会生成clinit方法
2、final修饰,不会在clinit方法中,即使它被static修饰。当然,如果该变量所指向的对象,即使该变量的地址不会改变,但是所指向的对象是可能发生变化的,那么会出现在clinit方法中,被动引用中的第三点就是这种情况
3、一个字节码文件只有一个clinit方法,多线程环境下加锁同步,但会出现死锁的情况,并且不易查看。如下死锁代码,无法通过jconsole等工具查看
public class InitDeadLock {
public static void main(String[] args) {
new Thread(A::test).start();
new Thread(B::test).start();
}
}
class A{
static {
System.out.println("ClassA init...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new B();
}
public static void test(){
System.out.println("aaa");
}
}
class B{
static {
System.out.println("ClassB init...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new A();
}
public static void test(){
System.out.println();
}
}
4、clinit方法块中生成的代码顺序与java代码的顺序是一致的,这个会影响程序的最终结果。
5、如果静态字段没有赋初值,那么一样不会出现在clinit方法中,因为clinit方法收集的是静态变量的赋值操作。其实很好理解,静态变量在准备阶段就已经赋了初值,如果在初始化阶段没有对它再赋值,那么就没有必要将它放入clinit。没有再赋值,说明我就用初始值那还放入clinit方法干嘛呢?
测试代码:
public class Hello {
public static final int a=3;
public static int b=5;
public static String c="sdf";
public static int d;
}
jclasslib结果如下
可以看到,a因为加了final修饰,所以会加上ConstantValue属性,并且不会出现在clinit方法中,d因为没有赋初值,所以也没有出现在clinit方法中。在clinit方法中只有b和c(对应的字节码为1和4这两条,后面有字段名,没显示),并且顺序和java代码的顺序一致。
有静态语句块的情况:
static {
System.out.println("A...");
}
结果:
getstatic #2 <java/lang/System.out>
ldc #3 <A...>
invokevirtual #4 <java/io/PrintStream.println>
return
《深入理解jvm虚拟机》中分为主动引用和被动引用。
1:new、getstatic、putstatic或invokestatic这四个字节码指令。对应的java代码场景
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外,为什么是静态字段?不是静态字段,那么调用肯定会使用new)的时候
- 调用一个类型的静态方法的时候
2:在调用类库中的某些反射方法时,例如,Class类或java.lang.reflect包中的反射方法。
3:初始化子类会去加载父类,并执行父类方法
4:启动类(main函数所在的类)
5:当使⽤jdk1.7动态语⾔⽀持时,如果⼀个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic的⽅法句柄,并且这个⽅法句柄所对应的类没有进⾏初 始化,则需要先出触发其初始化
6:jdk8中接口的默认方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前进行初始化。
看一个例子:
public class Test6 {
public static void main(String[] args) {
Test6_A instance = Test6_A.getInstance();
System.out.println(instance.a);
System.out.println(instance.b);
}
}
class Test6_A{
public static int a;
public static Test6_A instance=new Test6_A();
public static int b=0;
Test6_A(){
a++;
b++;
}
public static Test6_A getInstance(){
return instance;
}
}
首先在准备阶段的时候,会将a和b赋初值成0,instance=null,初始化的时候执行new Test6_A(),将a和b的值都加1,接着执行int b=0,将b赋值为0,所以结果是
1 0,如果将int b=0放在int a下面,那结果就是1 1
1:通过子类引用父类的静态字段,不会导致子类初始化
public class Test1 {
public static void main(String[] args) {
System.out.println(Sub.value);
}
}
class Super{
public static int value=123;
static {
System.out.println("Super init...");
}
}
class Sub extends Super{
static {
System.out.println("Super init...");
}
}
Sub没有初始化,但是它已经被加载了,通过VM参数-XX:+TraceClassLoading可以看到:
[Loaded com.cxylk.partone.Super from file:/D:/workspace/learn-jvm/out/production/learn-jvm/]
[Loaded com.cxylk.partone.Sub from file:/D:/workspace/learn-jvm/out/production/learn-jvm/]
结果:
Super init... 123
2:通过数组定义来引用类,并不会触发此类的初始化
public class Test2 {
public static void main(String[] args) {
TestA[] testAS=new TestA[1];//只是将TestA当做数组类型来用,没有使用它,并不会初始化
System.out.println("end");
}
}
class TestA{
static {
System.out.println("testa");
}
}
结果:
end
如果改成这样
TestA testA=new TestA();
那么就会触发初始化
3:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化(主动引用第一点的第二条特例)
public class Test3 {
public static void main(String[] args) {
System.out.println(Const.HELLOWORD);
while (true);
}
}
class Const{
public static final String HELLOWORD="hello word";
static {
System.out.println("A...");
}
}
Test3的字节码如下:
0 getstatic #2 <java/lang/System.out>
3 ldc #4 <hello word>
5 invokevirtual #5 <java/io/PrintStream.println>
8 return
Const的字节码如下:
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <A...>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
这里有getstatic指令,这是因为System.out是静态方法,而不是初始化当前类。
并且值得注意的是,当查看当前所加载的类时,并没有找到Const,但Const确实已经被编译成了class文件,而且使用HSDB查看的时候,也没有发现Const类。
现在将上面的例子换成下面这样:
public class Test4 {
public static void main(String[] args) {
System.out.println(NotConst.RANDOM);
while (true);
}
}
class NotConst{
public static final String RANDOM= UUID.randomUUID().toString();//地址是常量,但是引用不是
static {
System.out.println("A...");
}
}
那么就会触发NotConst的初始化,虽然RANDOM被final修饰,但它的引用是个变化的。
clinit方法的字节码如下:
0 invokestatic #2 <java/util/UUID.randomUUID>
3 invokevirtual #3 <java/util/UUID.toString>
6 putstatic #4 <com/cxylk/partone/NotConst.RANDOM>
9 getstatic #5 <java/lang/System.out>
12 ldc #6 <A...>
14 invokevirtual #7 <java/io/PrintStream.println>
17 return
注意:0和9这条指令不是当前类的初始化指令,6这条指令才是,调用putstatic给当前静态变量赋值。可以看到,此时RANDOM虽然被final修饰,但是它还是会出现在clinit方法中
静态字段的存储:
以前面第一个被动引用的代码为例,查看HSDB结果:
value是Super类的静态属性,不会存储在子类Sub的镜像类中。
所以,当通过子类去访问父类的静态字段有2种方式:
1、先去父类的镜像类中去取,如果有就直接返回,没有会沿着继承链将请求网上抛。算法的性能会随着继承链的变长而上升,时间复杂度为O(N)
2、借助另外的数据结构,使用K-V的格式存储,查询时间为O(1)
Hotspot就是使⽤的第⼆种⽅式,借助另外的数据结构ConstantPoolCache,常量池类ConstantPool中 有个属性_cache指向了这个结构。
ConstantPoolCache* _cache; // the cache holding interpreter runtime information
每⼀条数据对应⼀个类ConstantPoolCacheEntry。 ConstantPoolCacheEntry在哪呢?在ConstantPoolCache对象后⾯,看代码 \openjdk\hotspot\src\share\vm\oops\cpCache.hpp
ConstantPoolCacheEntry* base() const { return (ConstantPoolCacheEntry*)((address)this + in_bytes(base_offset())); }
这个公式的意思是ConstantPoolCache对象的地址加上ConstantPoolCache对象的内存⼤⼩
ConstantPoolCache
常量池缓存是为常量池预留的运⾏时数据结构。保存所有字段访问和调⽤字节码的解释器运⾏时信息。在类被主动(即初始化)使⽤之前创建和初始化的。每个缓存项在解析时被填充
以上面被动引用第一个例子为例,我们看看子类和父类中的ConstantPool
左边的是子类的instanceKlass中的常量池信息,缓存显示的是null。右边是父类的instanceKlass中的常量池信息,_cache项指向的缓存是有值的。
如何读取
\openjdk\hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp
CASE(_getstatic):
{
u2 index;
ConstantPoolCacheEntry* cache;
index = Bytes::get_native_u2(pc+1);
// QQQ Need to make this as inlined as possible. Probably need to
// split all the bytecode cases out so c++ compiler has a chance
// for constant prop to fold everything possible away.
cache = cp->entry_at(index);
//判断是否解析过
if (!cache->is_resolved((Bytecodes::Code)opcode)) {
CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
handle_exception);
cache = cp->entry_at(index);
}
....
可以看到,是直接去获取ConstantPoolCacheEntry
java虚拟机的退出条件是,某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作。