常州微信网站建设服务,亚马逊网上购物商城,专做logo网站叫什么地方,WordPress百度MIP手机主题深入理解类加载机制
Klass模型 Java的每个类#xff0c;在JVM中都有一个对应的Klass类实例与之对应#xff0c;存储类的元信息如:常量池、属性信息、方法信息…从继承关系上也能看出来#xff0c;类的元信息是存储在元空间的。普通的Java类在JVM中对应的是InstanceKlass(C)…深入理解类加载机制
Klass模型 Java的每个类在JVM中都有一个对应的Klass类实例与之对应存储类的元信息如:常量池、属性信息、方法信息…从继承关系上也能看出来类的元信息是存储在元空间的。普通的Java类在JVM中对应的是InstanceKlass(C)类的实例再来说下它的三个子类:
1.InstanceMirrorKlass:用于表示java.lang.ClassJava代码中获取到的Class对象实际上就是这个C类的实例存储在堆区学名镜相类2.InstanceRefKlass:用于表示java/lang/ref/Reference类的子类3.InstanceClassLoaderKlass:用于遍历某个加载器的类
Java中的数组不是静态数据类型而是动态数据类型即是运行期生成的Java数组的元信息用ArrayKlass的子类来表示
1.TypeArrayKlass:用于表示基本类型的数组2.ObjArrayKlass:用于表示引用类型的数组
总结: 非数组: InstanceKlass - 普通的类在JVM中对应的C类 方法区 InstanceMirrorKlass - 对应的是Class对象 镜像类 堆区 数组: 基本类型数组 boolean、byte、char、short、int、float、long、double - TypeArrayKlass 引用类型数组: ObjArrayKlass
为什么还要有镜像类 是为了安全由JVM控制可以将哪些参数返回给用户
实操
public class Hello {public static void main(String[] args) {int[] a new int[] {1,2,3};Hello[] hello new Hello[2];Hello h new Hello();while(true);}
}利用HSDB查看main线程的调用栈由于栈的规则是先进后出也就是说意味着当前方法栈的栈底存放的是当前方法的参数args,其次是int数组Hello对象数组我们可以查看它们的内存地址中都包含了哪些内容 基本数据类型的klass模型,还可以看到数组的内容 引用类型数组的klass模型我们在代码中创建的Hello数组对象引用都是空的 也就是_java_mirror这里c上的注解也是说明了这个InstanceMirroKlass的存在
类加载的过程
类的加载由7个步骤完成如图所示。类的加载说的是前5个阶段。
加载
1.通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)2.解析成运行时数据即instanceKlass实例存放在方法去3.在堆区生成该类的Class对象即instanceMirrorKlass实例
程序随便你怎么写随便你用什么语言只要能达到这个效果即可。就是说你可以改写openjdk源码你写的程序能达到这三个效果即可。 预加载:包装类、String、Thread 因为没有指明必须从哪获取class文件脑洞大开的工程师们开发了这些:
1.从压缩包中读取。如jar、war2.从网络中获取如Web Applet3.动态生成,如动态代理、CGLIB4.由其他文件生成如JSP5.从数据库读取6.从加密文件中读取
验证
1.文件格式验证。如验证class文件中是否包含魔数(CAFE BABE)、主次版本号是否在当前虚拟机处理范围之内2.元数据验证。如这个类是否有父类、这个类的父类是否继承了不允许被继承的类(如被final修饰的类)3.字节码验证。整个验证过程中最复杂的一个阶段主要目的是通过数据流和控制流分析确定程序语义是合法的如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据使用时却按long类型来加载如本地变量表中4.符号引用验证。最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是堆类自身以外的(常量池中的各种符号引用)的信息匹配性校验如符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用中的类和字段一级方法的访问性是否可以被当前类访问(比如调用静态方法检查调用的方法是否可以被当前类调用)
准备
为静态变量分配内存、赋初值。实例变量是在创建对象的时候完成赋值的没有赋初值这一说。如果是被final修饰在编译的时候会给属性添加ConstantValue属性准备阶段直接完成赋值即没有赋初值这一步
public class MyClassLoadHello {public static int v 10;public static final int b 11;public static void main(String[] args) {int a 1;int b 2;System.out.println(a b);}
}可以看到变量b多出了一个ConstantValue的属性这个属性指向了常量池中11这个数值。准备阶段就会直接赋值 反观变量v则是在类的初始化方法块中
为什么要在准备阶段赋初值为何不直接赋值 (C对象)InstanceMirrorKlass对象只是创建出来并没有属性把这个变量写入到Class对象中去如果这个静态变量没有使用到也没有赋初值字节码指令中将不包含该变量. 如图所示变量m并没有在字节码指令中因为没有赋初值也没有进行使用
通过HSDB可以发现InstanceMirrorKlass对象是有变量m这个属性的但是InstanceKlass对象却显示只有两个静态属性。是不是很奇怪字节码指令中都没有这个变量InstanceMirrorKlass对象中却有这个属性。其实也不难理解InstsanceKlass对象是存储在方法区中的可以表示类的静态属性信息。由于这个属性没有赋值也没有使用字节码层面就直接优化掉了我们知道反射的时候可以获取到这个类的所有信息所有属性以及所有方法不管其作用域的范围是什么如果不给InstanceMirrorKlass对象赋值这个属性那么在反射的时候就会拿不到这其实违背了反射的规则。所以要有静态属性赋初值这个动作来给InstasnceMirrorKlass对象赋上这个属性。
解析 将常量池中的符号引用转为直接引用。解析后的信息存储在ConstantPoolCache类实例中。其中会涉及到如下:
1.类或接口的解析2.字段解析3.方法解析4.接口方法解析
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关引用的目标并不一定已经加载到内存中。可以理解为静态常量池的索引 直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的.某个变量的内存地址
解析的时机 1.加载阶段解析常量池时(类加载以后马上解析 resolve的参数需要改为 true) 2.用的时候 解析什么只要是直接引用都需要解析 1.继承的类、实现的接口
2.属性3.方法
如何避免重复解析: 借助缓存ConstantPoolCache(运行时常量池的缓存) if (klass - is_resolved()) {}如图所示 常量池缓存: key: 常量池的索引 2 value: String - ConstantPoolEntry 静态属性是存储在堆区中的静态属性的访问:
1.去缓存中去找如果有直接返回2.如果没有就触发解析 底层实现:1.会找到直接引用2.会存储到常量池缓存中 openjdk是第二种思路在执行特定的字节码指令之前进行解析:anewarray、checkcase、getfield、instanceof、invokeddynamic、invokeinterface、invokesepcial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield.
拓展知识:编译时常量池和运行时常量池
在Java中常量池是class文件的一部分它用于存储关于类和接口的常量以及一些符号引用。常量池分为两种:编译时常量池和运行时常量池。
1.编译时常量池(Constant Pool) 编译时常量池是在编译器生成的它包含了类文件中的字面量(Literal)和符号引用(Symbolic References). 字面量:如文本字符串、final常量值等 符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符。这些符号引用在类加载阶段或第一次使用时会被解析为直接引用。 编译时常量池时.class文件的一部分它随着类文件的生成而生成每个.class文件都有一个自己的编译时常量池2.运行时常量池(Runtime Constant Pool) 运行时常量池是类或接口在JVM运行时的一部分当类被JVM加载时JVM会根据.class文件中的编译时常量池来创建运行时常量池。运行时常量池是方法区中的一部分。 动态性:运行时常量池具有动态性它可以在运行期间想其中添加新的常量。例如String的intern()方法可以将字符串常量添加到运行时常量池中 解析:运行时常量池中的符号引用会在类加载过程中或第一次使用时被解析为直接引用。 简而言之编译时常量池是静态的是.class文件的一部分而运行时常量池是动态的是JVM运行时数据区的一部分。运行时常量池在JVM的规范中是方法区的一部分但在不同的JVM实现中可能会有所不同如在HotSpot虚拟机中它被放在了堆(Heap)中。
初始化 执行静态代码块完成静态变量的赋值。类初始化阶段时类加载过程的最后一步前面的类加载过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与之外其余动作完全由虚拟机主导和控制到了初始化阶段才真正开始执行类中定义的Java程序代码。在准备阶段变量已经赋过一次系统要求的初始值,而在初始化阶段则根据程序员通过程序制定的主观计划去初始化类变量和其他资源或者可以从另外一个角度表达:初始化阶段时执行类构造器()方法的过程。
1.()方法时由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的编译器收集的顺序是由语句在源文件中出现的顺序所决定的静态语句块中只能访问到定义在静态语句块之前的变量定义在它之后的变量可以在前面的静态语句块中赋值但是不能访问如图所示2.()方法与类的构造函数(或者说实例构造器()方法)不同它不需要显示地调用父类构造器虚拟机会保证在子类地()方法执行之前父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object3.由于父类的()方法先执行也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作4.()方法对于类或接口来说并不是必需的如果一个类中没有静态语句块也没有对变量的赋值操作那么编译器可以不为这个类生成()方法5.虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步如果多个线程同时去初始化一个类那么只会有一个线程去执行这个类的()方法其他线程都需要阻塞等待知道活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作就可能造成多个进程阻塞(需要注意的是其他线程虽然会被阻塞但如果执行()方法的那条线程退出()方法后其他线程唤醒之后不会再次进入()方法。同一个类加载器下一个类型只会初始化一次)。
何时初始化?主动使用时
1.new、getstatic、putstatic、invokestatic2.反射3.初始化一个类的子类会去加载其父类4.启动类(main函数所在类)5.当使用jdk1.7动态语言支持时如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_put_static、REF_invoke_Static的方法句柄并且这个方法句柄所对应的类没有进行初始化则需要先触发其初始化。
实例 clinit()方法执行死锁示例1
public class DeadLoopClass {static {if (true) {System.out.println(Thread.currentThread() init DeadLoopClass);while (true) {}}}public static void main(String[] args) {Runnable script new Runnable() {Overridepublic void run() {System.out.println(Thread.currentThread() start);DeadLoopClass dlc new DeadLoopClass();System.out.println(Thread.currentThread() end);}};Thread thread1 new Thread(script);Thread thread2 new Thread(script);thread1.start();thread2.start();}
}Thread[main,5,main]init DeadLoopClass一条线程在死循环模拟长时间操作另外一条线程在阻塞等待执行clinit方法执行完毕后触发唤醒但是一直等不到所以就发生了死锁 clinit ()方法执行死锁示例2
public class InitDeadLock {public static void main(String[] args) throws InterruptedException {new Thread(() - new A()).start();new Thread(() - new B()).start();}
}class A {static {System.out.println(class A init);new B();}
}class B {static {System.out.println(class B init);new A();}
}一个线程创建A对象进而触发A的初始化但是A的clinit方法中又创建B又触发B的初始化另一个线程的初始化则反过来资源获取顺序不当造成了死锁
卸载
判定一个类是否是无用的类的条件相对一个实例对象或者废弃常量要苛刻很多。类需要同时满足下面3个条件才能算是无用的类:
1.该类的所有实例都已经被回收也就是Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收3.该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法 虚拟机可以堆满足上述3个条件的无用类进行回收这里说的仅仅是可以而并不是和对象一样不使用了就必然会回收。
这也造成了很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的Java虚拟机规范中确实说过可以不要求虚拟机在方法区中实现垃圾收集而且在方法区中进行垃圾收集性价比一般比较低:在队中尤其是在新生代中常规应用进行一次垃圾收集一般可以回收70~95%的空间而永久代的垃圾收集效率远低于此