一. 源码到字节码
使用工具javac编译的过程中,其实会经历非常复杂的过程,这里不用去深入研究。我们只要关心编译之后的class字节码文件.
*** 字节码反汇编工具***
查看字节码文件如果直接使用二进制工具去读,可读性极差。可以借助一些工具帮我们查看类中的信息,而且还能看到反编译后的汇编指令。 通过汇编代码,可以深入的了解java代码的工作机制.
javap
javap是JDK自身携带的反编译工具,可以直接使用,很方便;在cmd或者在IDEA的Terminal中可以查看javap的帮助信息:
java -help
javap -p -v xxxx.class // 显示所有类和成员的
这个是最直接,最方便的查看工具了,但是可读性没有一些插件好.
IDEA插件工具
ASMPlugin:主要可以用来查看汇编指令,格式会比较友好.
二. 类加载机制
Java虚拟机会动态的加载、链接与初始化类和接口。
1. 加载
加载是根据特定的名称查找类或者接口类型的二进制表示,并由此二进制形式来创建类或接口的所对应Class对象的过程。
步骤:
①通过一个类的全限定名获取定义此类的二进制字节流。
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③在Java堆中生成一个代表这个类的java.lang.Class对象, 作为对方法区中这些数据的访问入口
2. 链接
链接过程中要做的三步:
①验证:保证被加载类的正确性
文件格式验证
元数据验证
字节码验证
符号引用验证
②准备
为类的静态变量分配内存, 并将其初始化为默认值
例如:
class A{
static int a=10;
}
//准备阶段会为a静态变量分配4个字节的空间,并初始值为0。
③解析
把类中的符号引用转换为直接引用
类中的符号引用就是class字节码中的原始信息,比如变量名,方法名,类名等,需要解析成为运行时内存中相关的地址引用,方便使用。
例如:调用一个方法时,在java语言层面就是一个方法名这个符号,但是在底层应该要根据这个符号找到内存中的直接地址来调用。
3. 初始化
对类的静态变量, 静态代码块执行初始化操作
class A{
static int a = 10;
}
//到了初始化阶段,a的值就可以赋值为10了
三.类的初始化过程
一个类要实例化,必须先要让自己先初始化,类初始化过程主要是对静态成分的初始化。如下:
public class Student{
public static String name="myoga"; //静态变量
static{//静态代码块
name = "myoga_cc";
}
}
使用反编译工具将Student类字节码反编译,如下:
// class version 52.0 (52)
// access flags 0x21
public class Student {
// compiled from: Student.java
// access flags 0x9
public static Ljava/lang/String; name
// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LStudent; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 2 L0
LDC "myoga"
PUTSTATIC Student.name : Ljava/lang/String;
L1
LINENUMBER 4 L1
LDC "myoga_cc"
PUTSTATIC Student.name : Ljava/lang/String;
L2
LINENUMBER 5 L2
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
1. 方法详解
当一个类编译之后,字节码文件中会产生一个类构造器方法:
汇编指令含义:
LDC:将整数,浮点数或者字符串常量冲常量池中推送到栈顶
PUTSTATIC:将栈顶的值,赋值给指定类的静态字段
2 静态变量和静态代码块初始化顺序
①静态变量直接赋值底层原理:
当字节码在加载过程中的链接阶段时,有三个步骤,分别是验证,准备,解析。在准备阶段,为类的静态变量分配内存, 并将其初始化为默认值。只有到了初始化阶段才会正式赋值。
public static int a = 10;
//在链接的准备阶段:开辟空间存储a的数据,初始为0
//在出初始化阶段:将静态变量a赋值为10
②静态代码块
静态代码块的逻辑也将会在类的初始化阶段执行。
例子一
静态变量的赋值和静态代码块中的逻辑,都会整合到
public class Student2 {
static int height;//0
static int age = 10;
static {
age = 20;
}
static {
name = "Jack";
}
static String name = "Rose";
public static void main(String[] args) {
System.out.println(height);//0
System.out.println(name);//Rose
System.out.println(age);//20
}
}
要解答这个问题,你要观察编译后的字节码中
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 3 L0
BIPUSH 10 //将单字节的常量值10推入到栈顶
PUTSTATIC Student2.age : I //将栈顶的值10取出赋值给 Student2.age
L1
LINENUMBER 6 L1
BIPUSH 20
PUTSTATIC Student2.age : I
L2
LINENUMBER 10 L2
LDC "Jack" //将字符串常量“Jack”推入栈顶
PUTSTATIC Student2.name : Ljava/lang/String;
L3
LINENUMBER 13 L3
LDC "Rose"
PUTSTATIC Student2.name : Ljava/lang/String;
RETURN //结束
MAXSTACK = 1
MAXLOCALS = 0
结论:
如果没有静态变量的直接赋值,也没有静态代码块,那么就不会产生
3 继承中类初始化分析
当一个类存在父类时,一定是先对父类进行初始化然后再初始化子类的。
class Fu {
static int a = getNum1();
static {
System.out.println("1");
}
private static int getNum1() {
System.out.println("2");
return 10;
}
}
class Zi extends Fu {
static int b = getNum2();
static {
System.out.println("3");
}
public static int getNum2() {
System.out.println("4");
return 20;
}
public static void main(String[] args) {
//main方法执行
}
}
class Zi extends Fu {
// compiled from: Fu.java
// access flags 0x8
static I b
// access flags 0x0
<init>()V
L0
LINENUMBER 15 L0
ALOAD 0
INVOKESPECIAL Fu.<init> ()V //父类
RETURN
定义一个Teacher类,里面含有实例变量,实例代码块,构造方法。定义另外一个测试类Test,在main方法中创建Teacher对象,研究其过程。
public class Teacher {
int age = 10;
{
age = 20;
}
public Teacher() {
}
public Teacher(int age) {
this.age = age;
}
}
class Test{
public static void main(String[] args) {
Teacher t = new Teacher();
}
}
1 .new对象底层字节码指令分析
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 19 L0
NEW Teacher //创建Teacher对象,并将引用值压入栈顶
DUP //复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址;
INVOKESPECIAL Teacher.<init> ()V //调用无参构造器进行初始化
ASTORE 1 //将栈顶引用值赋值给变量表中第二个变量
L1
LINENUMBER 20 L1
RETURN
L2 //本地变量表
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE t LTeacher; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,类中静态相关成分就在这个过程中完成初始化,这个过程可以体现在
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。对象内存分配同时,实例变量也会被赋予默认值(零值)。
在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序员的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化,不管是哪一种方式的初始化,编译之后都会在方法
中统一执行。
2 对象初始化过程详解
①
图中可以看出,开辟对象空间所用的指令是 new
完成的。当给实例变量直接赋值,或者使用实例代码块,构造方法去给实例变量初始化值,编译器最后都会统一的放到
public class A {
int c;
{
b = 200;
}
int b = 20;
int a = 10;
public A() { }
public A(int c) {
this.c = c;
}
}
反编译字节码,观察
// class version 52.0 (52)
// access flags 0x21
public class A {
// compiled from: A.java
// access flags 0x0
I c
// access flags 0x0
I b
// access flags 0x0
I a
// access flags 0x1
public <init>()V
L0
LINENUMBER 12 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 5 L1
ALOAD 0
SIPUSH 200
PUTFIELD A.b : I
L2
LINENUMBER 8 L2
ALOAD 0
BIPUSH 20
PUTFIELD A.b : I
L3
LINENUMBER 10 L3
ALOAD 0
BIPUSH 10
PUTFIELD A.a : I
L4
LINENUMBER 12 L4
RETURN
L5
LOCALVARIABLE this LA; L0 L5 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public <init>(I)V
L0
LINENUMBER 14 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 5 L1
ALOAD 0
SIPUSH 200
PUTFIELD A.b : I
L2
LINENUMBER 8 L2
ALOAD 0
BIPUSH 20
PUTFIELD A.b : I
L3
LINENUMBER 10 L3
ALOAD 0
BIPUSH 10
PUTFIELD A.a : I
L4
LINENUMBER 15 L4
ALOAD 0
ILOAD 1
PUTFIELD A.c : I
L5
LINENUMBER 16 L5
RETURN
L6
LOCALVARIABLE this LA; L0 L6 0
LOCALVARIABLE c I L0 L6 1
MAXSTACK = 2
MAXLOCALS = 2
}
从反编译后的结果可以推导出以下结论:
②实例变量初始化顺序分析
观察上面的代码实例变量直接赋值,及在实例代码块中赋值,在构造方法中赋值这三种方式顺序特点。
可以得到以下结论:
public class A {
//静态变量
static int a = getA();
private static int getA() {
System.out.print(1);
return 10;
}
//静态代码块
static {
System.out.print(2);
a = 20;
}
//非静态
int b = getB();
private int getB() {
System.out.print(3);
return 20;
}
//实例代码块
{
System.out.print(4);
b = 40;
}
//构造方法
public A() {
System.out.print(5);
}
public static void main(String[] args) {
System.out.print(6);
new A();
}
}
先完成类初始化,然后执行main方法,main方法中创建对象初始化对象。
我们很轻松就能得到结果: 126345
然而这不是一个绝对的情况,类初始和实例化可能会混合在一起完成==。如下:
增加一个静态变量,类型是本身,并创建对象直接赋值。
public class Aa {
//静态变量
static int a = getAa();
static Aa obj = new Aa();//******新加入********
private static int getAa() {
System.out.print(1);
return 10;
}
//静态代码块
static {
System.out.print(2);
a = 20;
}
//非静态
int b = getB();
private int getB() {
System.out.print(3);
return 20;
}
//实例代码块
{
System.out.print(4);
b = 40;
}
//构造方法
public Aa() {
System.out.print(5);
}
public static void main(String[] args) {
System.out.print(6);
new Aa();
}
}
当类加载后完成第二个阶段链接,其实就可以投入使用了。在初始化阶段中,如果涉及到本类的对象实例化也是可以完成的。我们可以从底层代码论证:
static <clinit>()V
L0
LINENUMBER 3 L0
INVOKESTATIC Aa.getAa ()I
PUTSTATIC Aa.a : I
L1
LINENUMBER 5 L1
NEW Aa
DUP
INVOKESPECIAL Aa.<init> ()V //在执行方法的过程中执行了方法
PUTSTATIC Aa.obj : LAa;
L2
LINENUMBER 15 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_2
INVOKEVIRTUAL java/io/PrintStream.print (I)V
L3
LINENUMBER 16 L3
BIPUSH 20
PUTSTATIC Aa.a : I
L4
LINENUMBER 17 L4
RETURN
MAXSTACK = 2
MAXLOCALS = 0