JVM底层之类加载器
前言
这篇博客记录下JVM底层的类加载器,关于类加载器网络上有不少博客写到,我这边只是根据自己的理解记录下jvm类加载器的过程,可能不会太详细,只是用于自己平时偶尔翻翻回忆下自己的曾经所获所得。
JVM中的Klass模型
java中的每一个类在JVM中都是以Klass形式存在的,其中包含了元数据信息,包含属性信息、方法信息以及常量池。
Klass的模型结构如下:
最顶层的父类是MeaSpaceObj,然后元信息类Metadata是子类,而我们的Klass是是MetaspaceObj的子类,上图所描述的类继承关系指的都是C++对实现了JVM的类结构图,其中Klass有两个实现类InstanceKlass和ArrayKlass,
InstanceKlass有三个子类,分别是InstanceMirrorKlass、InstanceRefKlass、InstanceClassLoaderKlass
1.InstanceMirrorKlass:用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类
2.InstanceRefKlass:用于表示java/lang/ref/Reference类的子类
3.InstanceClassLoaderKlass:用于遍历某个加载器加载的类
Java中的数组不是静态数据类型,是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:
1.TypeArrayKlass:用于表示基本类型的数组
2.ObjArrayKlass:用于表示引用类型的数组
比如我们在代码中这样写:
public static void main(String[] args) {
int [] a = new int[2];
Test [] tests = new Test[2];
System.out.println(a);
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
其中a和tests都是一个数组,而a是基本类型数组,tests是引用类型数组
在java的字节码中a是newarray类型,tests是anewarray类型,我们看下字节码:
public static main([Ljava/lang/String;)V
TRYCATCHBLOCK L0 L1 L2 java/io/IOException
L3
LINENUMBER 9 L3
ICONST_2
NEWARRAY T_INT
ASTORE 1
L4
LINENUMBER 10 L4
ICONST_2
ANEWARRAY com/luban/Test
ASTORE 2
L5
以上的NEWARRAY T_INT就是a
NEWARRAY T_INT就是tests
我们再来看在jvm中以C++形式存在的类型
我们打开cmd执行java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
打开HSDB jvm调试工具:
我们启动我们刚刚写的代码找到进程号7172:
查看我们的main进程的内存堆栈信息:
最后我们找到PSYounGen[I后面的地址就是我们的int数组
我们拿到INT数组的内存地址*(后面的0x开头的),打开HSDB菜单的Tools下Inspector菜单
然后输入我们的int数组的内存地址:
可以看到我们的int数组加载到JVM过后,C++对它的处理是类型是TypeArrayKlass,而TypeArrayKlass是Klass的子类,我们从上图也可以看出他们的继承关系;所以TypeArrayKlass其实就是C++用来描述我们JAVA中的基本类型数组的。
同理,我们根据ObjArray找到我们的引用类型的数组:
可以看到ObjArrayKlass就是用来描述我们java类中引用类型数组
这个就是我们的引用数组tests是ObjArrayKlass
同样的ObjArrayKlass也是Klass的子类
所以我们的int数组在jvm中的存在类型是TypeArrayKlass
引用数组在我们的jvm中存在类型是ObjArrayKlass
加载:
1、通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)
2、解析成运行时数据,即instanceKlass实例,存放在方法区
3、在堆区生成该类的Class对象,即instanceMirrorKlass实例
何时加载:
主动使用时
1、new、getstatic、putstatic、invokestatic(字节码层面的指令)
2、反射
3、初始化一个类的子类会去加载其父类
4、启动类(main函数所在类)
5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
预加载:包装类、String、Thread
故:
当我们定义了一个类,在本来中有静态 属性和静态方法,就算这个类有静态代码块,那么我们如果通过类.静态属性或者类.静态方法,如果这些静态的属性或者方法不在本来,是在父类,那么初始化的工作是不会进行的,看下面例子:
public class T0804 {
public static void main(String[] args) {
System.out.println(T0804_01.str);
}
}
class T0804_01 extends T0804_p{
static {
str +="#456";
}
}
class T0804_p{
public static String str = "123";
}
最后输出的是123而不是123#456
这个例子就是getstatic
但是如果我们在T0804_p有静态代码块,也会执行静态代码块,而且顺序是先执行,如:
class T0804_p{
public static String str = "123";
static {
System.out.println("T0804_p init");
}
}
最后输出
T0804_p init
123
我们再修改下代码如下:
public class T0804 {
public static void main(String[] args) {
new T0804_01();
System.out.println(T0804_01.str);
}
}
class T0804_01 extends T0804_p{
public T0804_01(){
System.out.println("T0804_01 ..");
}
static {
System.out.println("T0804_01 init...");
str +="#456";
}
}
class T0804_p{
public static String str = "123";
static {
System.out.println("T0804_p init");
}
public T0804_p(){
System.out.println("T0804_p ...");
}
}
最后输出:
T0804_p init
T0804_01 init…
T0804_p …
T0804_01 …
123#456
所以当通过new初始化一个类的时候
1.先初始化父类的静态代码块;
2.再初始化本类的静态代码块;
3.然后又初始化父类的构造;
4.最后初始化自己的构造;
而调用static的时候就要判断你调用的这个类中的某个属性或者方法是否在本类,如果在父类,那么只会初始化父类的静态代码块,不会初始化本类的静态代码块,比如:
public class T0804 {
public static void main(String[] args) {
System.out.println(T0804_01.getstr());
}
}
class T0804_01 extends T0804_p{
public T0804_01(){
System.out.println("T0804_01 ..");
}
static {
System.out.println("T0804_01 init...");
str +="#456";
}
}
class T0804_p{
public static String str = "123";
static {
System.out.println("T0804_p init");
str="1111";
}
我们通过T0804_01.getstr()调用静态犯法getstr(),而getStr这个方法没有在T0804_01中,而是在它的父类T0804_p中,所以调用完成过后,不会执行T0804_01中的静态代码块,只会执行父类的静态代码块,最后输出如下:
T0804_p init
1111
验证
1、文件格式验证
2、元数据验证
3、字节码验证
4、符号引用验证
准备
为静态变量分配内存、赋初值
实例变量是在创建对象的时候完成赋值的,没有赋初值一说
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
初始化
执行静态代码块,完成静态变量的赋值
静态字段、静态代码段,字节码层面会生成clinit方法
方法中语句的先后顺序与代码的编写顺序相关
何时解析
思路:
1、加载阶段解析常量池时
2、用的时候
还有一个比较经典的知识点就是我们我们要使用一个类的时候,什么情况下这个类加载了要执行静态代码块,那些情况不执行,我们都知道我们手动加载一个类的时候常用的有两种,
用Class.forName和ClassLoader中的loadClass
1. Class<?> clzz = T0804.class.getClassLoader().loadClass("com.luban.t0806.T0804_p");
2.Class<?> clzz2 = Class.forName("com.luban.t0806.T0804_p");
最后只有2能初始化T0804_p中静态代码块
所以综上所述:通过Class.forName会加载class的时候是会执行我们的静态初始化操作;
而使用classloader的loadClass仅仅是给我们加载了我们所要求的class,不会执行任何初始化方法
下一篇我们来扯一下类加载系统和SPI机制(文采不行_)