字节码文件(书接上回)
常用工具:
javap -v 字节码文件名(jar包需要使用jar -xvf解压)命令,jdk自带的反编译工具,控制台呈现字节码文件(有的时候还是非常方便的)。直接输入javap会给你很多用法的提示。
jclasslib的IDEA插件(这个非常的方便),代码修改之后要编译一下字节码才会给你更新哦。
阿里arthas,是一款线上监控诊断产品,通过全局视角实时查看应用 load 、 内存gc 、线程的状态信息,并能在不修 改应用代码的情况下,对业务问题进行诊断大提升线上排查效率。
dashboard面板-i设置时间间隔-n设置刷新次数(cls可以清楚黑窗的所有行)结果见下
dump可以把字节码报错到指定目录
jad可以反编译类的源代码
类的生命周期(加星)
一个类加载、使用、卸载的整个过程(加载、连接、初始化、使用、卸载),一张好看的图
加载阶段
1.加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道(本地文件、动态代理生成、通过网络传输)以二进制流的方式获取字节码信息。
2.类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中。
3.类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。生成一个InstanceKlass 对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
4.同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段(存在堆区)的数据(JDK8及之后)。
对于开发者,只需要访问堆区的Class对象,不需要访问方法区。
连接阶段
三个部分,验证(验证内容是否满足虚拟机规范)、准备(给静态变量赋初值)、解析(将常量池中的符号引用替换成指向内存的直接引用)
验证(了解一下即可)四个部分1.文件格式验证(比如之前学过的魔数,主次版本号)2.元信息验证,比如类必须有父类 3.验证程序执行指令的语义,比如指令执行到一半强制跳转到其他方法中 4.符号引用验证,比如是否访问了其他类中的private方法等。
主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过。
准备,只会给静态变量赋值,每一种基本数据类型和引用数据类型都有其初始值
final修饰的基本数据类型的静态变量,直接给值,反正它后面又不会变
解析,将常量池中的符号引用替换为直接引用,符号引用是在字节码文件中使用编号来访问常量池中的内容,直接引用使用的是内存中的地址,效率当然更高。
初始化阶段
执行静态代码块中的代码,并为静态变量赋值,会执行clinit部分的字节码指令,clint中字节码指令的执行顺序和java中的编写顺序是一致的
导致类的初始化的方式:
1.访问一个类的静态变量或者静态方法(final修饰的不会出发初始化)
2.调用Class.forName
3.new一个该类的对象时
4.执行Main方法的当前类
添加-XX:TraceClassLoading参数可以打印出加载并初始化的类
来两道题把,看这个输出啥
public class Test1 {
public static void main(String[] args) {
System.out.println("A");
new Test1();
new Test1();
}
public Test1(){
System.out.println("B");
}
{
System.out.println("C");
}
static {
System.out.println("D");
}
}
答案是DACBCB,DA我想到了,CB这个顺序我一开始没太理解
执行顺序是这样的:
- 静态块:在类首次加载到 JVM 时执行,只执行一次。
- 实例初始化块:在每次实例化对象之前执行,每创建一个实例就执行一次。
- 构造函数:在实例初始化块之后执行,用于初始化新创建的对象。
clinit指令在特定情况下不会出现如下:
1.无静态代码块且无静态变量赋值语句
2.有静态变量的声明,但是没有赋值语句
3.静态变量的定义使用final关键字,这类变量会在准备阶段进行初始化
涉及到继承时,直接访问父类的静态变量,不会触发子类的初始化;子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
这就引入了下一道题
public class Test2 {
public static void main(String[] args) {
new B02();
System.out.println(B02.a);
}
}
class A02{
static int a=0;
static {
a=1;
}
}
class B02 extends A02{
static {
a=2;
}
}
这里a=2应该没啥说的,如果去掉newB02()这句话就只有a=1了
再来两道小题
第一道会输出什么呢
public class Test3 {
public static void main(String[] args) {
Test3_A[] arr =new Test3_A[10];
}
}
class Test3_A{
static {
System.out.println("Test3_A的静态代码块运行");
}
}
这里什么都不会输出,数组的创建不会导致数组中的元素的类进行初始化
下一道,这会输出什么呢
public class Test4 {
public static void main(String[] args) {
System.out.println(Test4_A.a);
}
}
class Test4_A{
public static final int a=Integer.valueOf(1);
static {
System.out.println("Test4_A的静态代码块运行");
}
}
Test4_A的静态代码块运行,1。 final修饰的变量如果赋值内容需要执行指令才能得出结果,会执行clinit方法进行初始化,这个例子很好地展示了即使字段是 final
和 static
的,如果其初始化不是编译时常量表达式,类初始化仍然会按照预期进行。