java虚拟机(HotSpot)之类加载器

it2024-04-04  60

文章目录

Jvm类加载器一、Jvm生命周期二,类加载1.类的加载过程2.四种验证方式3.类加载器的分类一,Bootstarap 加载器二,扩展类加载器 ExtClassLoader三,应用类加载器 AppClassloader四,自定义类加载器五,获取classloader的方式 4.双亲委派机制5.类的主动使用和被动使用(重点)总结类加载顺序的简单代码示例 运行时数据区

Jvm类加载器

一、Jvm生命周期

虚拟机的启动: 虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的 虚拟机的执行: 一个运行中的java虚拟机有着一个清晰的任务, 执行java程序程序开始执行时它才运行,程序结束时停止 执行一个所谓的java程序的时候,真真正正在执行的·是一个叫做java虚拟机的进程 退出: 正常结束 异常 System.exit()

二,类加载

类加载器只负责加载class文件,至于是否可以运行,由执行引擎来讲决定的。 加载的类信息存放在于一块称为方法区的内存空间,除了类的信息外,方法区中还会 会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是 class文件中常量池部分的内存映射)。

1.类的加载过程

加载 -- (验证 -- 准备 --- 解析) --- 初始化 链接 加载: 1.通过一个类的全限定名获取此类的二进制字节流 2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口 验证: 1.目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性, 不会危害虚拟机自身安全。 2.主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证 准备: 1.为类变量分配内存并且设置该类静态变量的默认初始值,即零值。 2.这里不包含用fina修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化,。 3.这里不会实例化变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中 解析 : 1.将常量池内的符号引用转换为直接引用的过程。 2.事实上,解析操作往往会伴随着jvm在执行完初始化之后再执行。 3.符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在class文件格式中, 4.直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。 5.解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等, 初始化: 初始化阶段就是执行类构造器方法<clinit>()的过程。带有static修饰的也会初始化,如静态方法,静态块, 此方法不需要定义,是java编译器自动收集类中的所有静态类变量的赋值动作和静态代码块中的语句合并而来。 并且给静态变量赋予初始值,如果程序对静态变量的值进行了赋值或修改,都是以代码的顺序来的,而且注意, 只能执行带有static关键字的属性或方法和静态方法 构造器方法中的指令按语句在源文件中出现的顺序执行。 <clinit>()不同于类的构造器(构造器是虚拟机视角下的<init>())。 若该类具有父类,jvm会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁, 如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(), 其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。 特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后, 其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下, 一个类型只会被初始化一次。 如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞, 在实际应用中这种阻塞往往是隐藏的 注意:带有static修饰的代码都是被clinit来执行的,因此,延申一个问题就是, 单例模式使用静态内部类是线程安全的,

2.四种验证方式

文件格式验证,元数据验证,字节码验证,符号引用验证 文件格式验证: 是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。 如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内; 常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能 正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储, 所以后面的三个验证阶段都是基于方法区的存储结构进行的。 元数据认证: 是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。 可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类; 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法 字节码验证: 主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。 如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的; 但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的 符号引用验证: 发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。 验证符号引用中通过字符串描述的权限定名是否能找到对应的类; 在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段; 符号引用中的类、字段和方法的访问性(privateprotectedpublicdefault)是否可被当前类访问 验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。 如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施, 以缩短虚拟机类加载时间。

3.类加载器的分类

引导类加载器 bootstrapt class loader 扩展类加载器 Extension class loader 系统类加载器(应用类加载器) System class loader 自定义类加载器 注意:这四中加载器是包含的关系,不是上下层,也不是继承的关系 可以使用Classloader来获取这些更大范围的加载器 引导类加载器是获取不到的null 我们自定义的类是被系统类加载器加载的,java核心类库的类是由引导类加载器加载的

一,Bootstarap 加载器

这个类加载器使用c/c++语言实现的,嵌套在jvm内部。 它用来加载java的核心库 JAVA_HOME/jre/lib/rt.jar, resources.jar或 sun.boot.class.path路径下的内容。 没有父类加载器。他本身属于顶级的父类加载器 加载应用类和扩展类加载器。

二,扩展类加载器 ExtClassLoader

是sun.misc.Launcher类的内部类。 派生于ClassLoader类。 父类加载器为启动类加载器。 从java.ext.dirs系统属性所指定的目录中加载类库。或从jdk的安装目录的jre/lib/ext子 目录(扩展目录)下加载类库,如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

三,应用类加载器 AppClassloader

AppClassloader是Launcher的内部类。 java语言编写,由sun.misc.launcher$AppClassLoader实现派生于classLoader类。 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。 该类加载是程序中默认的类加载器,一般来说,java应用的类都是由他来完成得的 通过ClassLoader#getSystemClassloade()可以获取该加载器

四,自定义类加载器

继承classLoader抽象类 1.2之前需要继承该类重写loadclass方法,从而实现自定义的类加载, 但在1.2后建议将自定义的类加载逻辑写在findClass方法中。 在编写自定义加载器时,如果没有过于复杂的需求时,可以直接继承URLClassloader类, 避免了去写findclass方法和获取字节流的方式。

五,获取classloader的方式

Cla.getClassLoader() 获取当前类的classloader Thread.currentThread().getContextClassLoader()获取当前上下文的类加载器 ClassLoader.getSystemClassloader() 获取系统类加载器,还可以获取更上级的扩展类 DriverManager.getCallerClassLoader() 获取调用者的类加载器

4.双亲委派机制

加载类的class文件时,虚拟机采用双亲委派机制,将请求交由父类处理,是一种任务委派模式 工作原理: 如果一个类加载器收到了类加载器请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器。 如果父类加载器可以完成类加载任务,就成功返回,如果无法完成加载,子类才会去尝试加载。 一般的核心类库的接口是由启动类加载器加载的,但是接口的实现类是系统类加载器加载的, 也可以认为是当前线程上下文加载器加载地。 双亲委派机制的优势: 避免类的重复加载,保护程序安全,防止核心api被随意篡改 如我们自定义一个类,包名路径和核心类库一样时,该类是不起作用的, 因为双亲委派会向上委托,最终引导类可以加载,所有自定义的不起作用 因此,也起到了一个安全的作用 JVM中表示两个class对象是否为同一个类存在两个必要条件: 类的完整类名必须一致,包括包名。 加载这个类的classloader指的classloader实例对象必须相同。 也就是说,就算是来源于同一个class文件,被同一个虚拟机加载,但只要加载他们的classloader实例对象不同, 那么这两个对象也不相等。 类加载器的引用: jvm必须知道一个类型是由启动加载器加载的还是由用户加载器加载的,如果一个类型是由用户类加载器加载的, 那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中, 当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的 就如同我们获取当前类的类加载对象一样 ClassLoader classLoader = test.class.getClassLoader(); 我们也可以使用当前类的类加载器区加载一些其他的类

5.类的主动使用和被动使用(重点)

主动使用: 》创建类的实例 》访问某个类或接口的静态变量,或者对静态变量赋值 》调用类的静态方法 》反射 》初始化一个类的子类 》java虚拟机启动时被标明为启动类的类 》jdk 7开始提供的动态语言支持 注意,注意:除了上面的七种,都认为是被动使用,都不会导致类的初始化 ,注意

放个网图,看个大概

最后,有一些代码示范

public class test { static int p=1; static { i = 0; //给变量赋值可以正常编译通过 System.out.print("p"+i); //编译器会提示“非法向前引用” System.out.println(p); } static int i = 1; public static void main(String[] args) { } } 这个demo无法正常编译,原因是打印i的是会出编译错误,提示“非法向前引用”, 如果把打印i的语句注释掉,就可以打印了, 那么打印的p是1默认值为0, 如果在静态块里面定义普通的非静态变量可以吗,答案是可以。 demo2 public class Demo { public static void main(String[] args) { System.out.println(b.str); } } class a{ public static String str="父类静态变量"; static { System.out.println("父类静态块"); } } class b extends a{ static { System.out.println("子类静态块"); } } 打印的是: 父类静态块 父类静态变量 那是因为访问的是str,它是静态变量,就开始初始化a类,就调用了静态块 但为什么没有调用子类的静态块呢,那是因为它没有符合主动访问的任何一种,所有没有初始化, 子类是不会被初始化的,因为没有主动使用 demo3 public class demo_02 { public static void main(String[] args) { System.out.println(demo_02s.str); } } class demo_02s{ public static final String str="静态字符串常量"; static { System.out.println("静态块"); } } 输出的是: 静态字符串常量 这是为什么呢,加了一个final后就没有执行静态块 在编译阶段,常量就会被存入到调用这个常量的方法所在的类的常量池中 也就是demo_02类 本质上,调用类没有直接引用到定义常量的类,因此不会触发常量所在类的初始化 当常量存入常量池后,两个类就没有任何关系了,我们甚至可以把demo_02s的字节码文件删除,也不会影响程序的运行 demo4 我们将上面的代码改一下 public class demo_03 { public static void main(String[] args) { System.out.println(demo_03s.str); } } class demo_03s{ public static final String str= UUID.randomUUID().toString(); static { System.out.println("静态代码块"); } } 输出:静态代码块 以及str的内容 这是为什么呢,明明和上个demo一样,除了str的值不一样 注意 当一个常量的值并非编译期间可以确定地,那么其值就不会放到调用类地常量池中, 这时在程序运行时,会导致主动使用这个常量所在地类,显然会导致这个类被初始化, 所以这个例子地常量所在地类会被初始化 那么问题来了: 如果使用创建该类地数组,会不会实例化对象呢 答案是不会的,对于数组实例来说,其类型是由JVM在运行期动态生成的 然后反编译后,又会涉及到几个助记符: anewarray:表示创建一个引用类型的(如类,接口,数组)数组,并将其引用值压入栈 newarray:表示创建一个指定的原始类型(int,float,char)的数组,并将其引用值压入栈顶

总结

现在我们来总结一下 1子类(普通类) 父类(普通类): 变量是编译期确定的值: 不加final: 当访问子类静态变量时,会加载两个类的class文件, 并且先加载父类的,再是子类,然后父类也会先被初始化 加上final修饰变量: 不会初始化这两个父子类,也不会加载这两个的class文件, 所以可以删除 变量是不确定的值: 不加final: 当访问子类静态变量时,会加载两个类的class文件, 并且先加载父类的,再是子类,然后父类也会先被初始化,变量是再运行期时加上的 加上final修饰变量:和不加是一样的 2子类(普通类) 父类(接口): 变量是确定的: 不加final: 会加载父接口,子类字节码文件,但不会初始化父接口, 注意:接口变量是final修饰的,但是,不能删除父接口字节码文件, 会报加载class文件的异常NoClassDefFoundError,当然子类更加不能删除, 加final: 不会加载父接口和子类的class文件,不会初始化, 此时可删class文件 变量不确定: 不加final: 会加载父接口class文件,注意,是先加载父接口的, 但是不会初始化父接口,不确定的变量是在运行期加上的 加final: 和不加是一样的 3 子类(接口) 父类(接口): 接口常量都是final修饰的 常量是确定的: 不会加载它们的字节码文件 可以删除 变量不确定的: 会加载父接口和子接口的字节码文件,但不会初始化父接口 变量会在运行期加上,所以子接口肯定会初始化的,不能删class文件 注意:接口只有直接访问静态变量时,属性时,才会初始化 访问的静态变量,方法定义在哪个类就表示对那个类的主动使用,就算定义在父类的静态变量方法, 通过子类来调用,仍然不会初始化子类

类加载顺序的简单代码示例

class demo_4 { public static void main(String[] args) { demo4s demo4s = com.test.demo4s.getdemo4(); System.out.println(demo4s.a); System.out.println(demo4s.b); } } class demo4s{ public static int a=1; private static demo4s ds=new demo4s(); private demo4s(){ System.out.println(111); a++; b++; System.out.println(a+":"+b); } public static int b=0; public static demo4s getdemo4(){ return ds; } } 输出的内容是main方法输出的是2,0 ;但是demo4s方法输出的却是2,1 那么是怎么输出来的呢 那是因为在调用getdemo4静态方法时,要初始化该类,但是初始化前还要做准备 就是将静态变量设置默认值,由于示例中的是int ,所以默认是0, 然后静态的引用类型为null,然后再进行初始化,从上到下按照顺序执行 先是给a赋值,然后就引用类型调用构造方法,赋值,执行了构造后,a为2,b为1, 然后在对b进行初始化 所以最后a=2,b=0; 那么初始化后,就是使用了 类实例化: 为新的对象分配内存 为实例变量赋默认值 为实例变量赋正确的初始值 注意:对于数组类型来说,它的class对象并不是类加载器创建的或加载的,而是由java虚拟机在运行期的时候根据需要,动态创建的。只有数组是这样

运行时数据区

移步到-》运行时数据区

最新回复(0)