文章目录
三、类加载和初始化
面试题:
-
描述一下类加载器的层次?
-
双亲委派
-
为什么要双亲委派
Class文件 如何加载到内存中的 并且是如何执行的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8VDRdKSX-1641746615741)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image002-164174456853445.jpg)]
3.1 Class Cycle
Class文件在我们硬盘中,那么它是如何加载到内存中 总共需要三个大的步骤
- Loading步骤
Loading 是将本地的classfile的二进制的内容加载到内存中
-
Linking 步骤
- Verification 校验
Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。
-
Preparation
Preparation过程 主要作用就是将静态变量赋默认值 假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;
-
Resolution
该过程 将class文件中常量池用到的一些符号引用转换为内存地址
-
Initializing 步骤
静态变量在该步骤下进行赋值为初始值,才会调用静态代码块。
3.2 ClassLoader
JVM本身有个类加载器的层次 这个类加载器就是普通的Class,这个加载器的层次就是用来加载不同的class
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvr29nkh-1641746615741)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image004-164174456853446.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcsKFrqU-1641746615742)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image006-164174456853547.jpg)]
注意: 任何一个classfile被加载到内存中的都会存在两个部分,
第一个部分 二进制的classfile确实被load内存中
第二个部分 生成的class类的对象,class中还会存在其他对象,引用到class对象,而class类对象指向classfile的内存加载
扩展为 Class对象究竟存储在哪里?
Class对象存储在metaspace里面
Metaspace 是JDK1.8版本出现的,Metaspace 就是方法区methodarea 1.8版本移出了永久代,原本在1.8版本之前 PermGenerationspace部分变更成了metaspace 而这两个地方指代的都是方法区。
怎么才能够知道哪些类是由哪些加载器进行加载的呢?
最顶层
BootstrapClassLoader
加载lib/rt.jar charset.jar等核心类 C++实现。
主要负责加载jdk中最核心的jar ,例如runtime.jar 或者是我们平时锁说的String.class,Object.class 都是位于lib/rt.jar
会出现null值 调用的是最顶层加载器,在java的类中没有这样的对象去应对他。
第二层
ExtClassLoader
加载扩展的jar包,jre/lib/ext/*.jar
或由-Djava.ext.dirs指定
第三层
AppClassLoader
加载classpath指定的内容
第四层
自定义加载器
加载自定义的类的内容。
public class ClassLoaderTest01 {
public static void main(String[] args){
System.out.println(String.class.getClassLoader()); System.out.println(sun.awt.HKSCS.class.getClassLoader()); System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader()); System.out.println(ClassLoaderTest01.class.getClassLoader()); System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader()); System.out.println(ClassLoaderTest01.class.getClassLoader().getClass().getClassLoader()); } }
类加载器的加载过程叫双亲委派。
在双亲委派中存在一个概念 叫父加载器 这里的父加载器不是继承关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-un024O8k-1641746615744)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image008.gif)]
该图描述的是语法上一种继承关系,而继承关系和父加载器没关系。
父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent
3.3 双亲委派
Class文件通过自定义的classloader进行加载,如果他没有加载,那么则委托它的父加载器appclassloader 加载, appclassloader 判断是否为本地加载 如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器bootstrapClassLoader,但是当顶层的加载器,也没有加载,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。
双亲:指的有一个从子到父的过程 又有一从父到子的过程
委派:自己不想做的事情 委托别人去完成
向上委派的时候 父加载器都是到 Cache中取寻找
可以把这个缓存理解成是一个list或者是一个数组。
面试题 为什么要去使用双亲委派?
\1. 防止加载同一个class文件,保证数据的安全
\2. 保证核心的class文件不被篡改,即使被篡改了也不会加载,即使被加载也不会是同一个class对象 为了保证class的执行安全。
这部分代码是被写死的。
3.4 父加载器
父加载器不是了的加载器的加载器,也不是加载器的父类的加载器
父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent
public class ClassLoaderTest02 {
public static void main(String[] args){ System.out.println(ClassLoaderTest02.class.getClassLoader()); System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader()); System.out.println(ClassLoaderTest02.class.getClassLoader().getParent()); System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent()); *//System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent().getParent()); * } }
3.5 类加载器范围
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tzS4tOlI-1641746615745)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image010-164174456853548.jpg)]
从上个案例的执行结果中,我们可以看出appclassloader和extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类
在Launcher源码中
private static String bootClassPath = System.getProperty("sun.boot.class.path");
final String var1 = System.getProperty("java.class.path");
String var0 = System.getProperty("java.ext.dirs");
sun.boot.class.path 是BootstrapClassloader的加载路径
java.class.path 是AppClassloader的加载路径
java.ext.dirs 是ExtClassLoader的加载路径
package com.openlab; import sun.misc.Launcher; public class ClassLoaderTest03 { public static void main(String[] args){ String pathBoot = System.*getProperty*("sun.boot.class.path"); System.*out*.println(pathBoot.replaceAll(";",System.*lineSeparator*())); System.*out*.println("---------------------------------"); String pathExt = System.*getProperty*("java.ext.dirs"); System.*out*.println(pathExt.replaceAll(";",System.*lineSeparator*())); System.*out*.println("---------------------------------"); String pathApp = System.*getProperty*("java.class.path"); System.*out*.println(pathApp.replaceAll(";",System.*lineSeparator*())); } }
3.6 自定义加载器
小demo
package com.openlab; public class ClassLoaderTest04 { public static void main(String[] args) throws ClassNotFoundException { Class clazz = ClassLoaderTest04.class.getClassLoader().loadClass("com.openlab.Person"); System.*out*.println(clazz.getName()); *//* *类加载器也可以用来加载资源 // ClassLoaderTest04.class.getClassLoader().getResourceAsStream(); * } }
Tomcat 加载的Servlet
Spring框架中加载ApplicationContext
比如在写一些类库的时候或者修改底层框架时。想加载哪个类就可以加载谁。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {``// 加锁 synchronized (getClassLoadingLock(name)) { *// First, check if the class has already been loaded * Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.*nanoTime*(); try {``//继续使用parent的classloader 递归调用loadClass方法 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { *// ClassNotFoundException thrown if class not found // from the non-null parent class loader * } ``// 调用findClass方法去找class if (c == null) { *// If still not found, then invoke findClass in order // to find the class. * long t1 = System.*nanoTime*(); c = findClass(name); *// this is the defining class loader; record the stats * sun.misc.PerfCounter.*getParentDelegationTime*().addTime(t1 - t0); sun.misc.PerfCounter.*getFindClassTime*().addElapsedTimeFrom(t1); sun.misc.PerfCounter.*getFindClasses*().increment(); } } if (resolve) { resolveClass(c); } return c; } }
\1. 继承ClassLoader
\2. 重写模板方法 findClass
----调用defineClass方法
从目录中读取class文件,将class文件通过自定义加载器进行加载
利用IO流
Java语言是比较容易被反编译
-防止反编译
-防止篡改
可以给class文件进行加密 解密
作业:1.自定义加载器的实现 视频到群里
2.classfile解析的内容 需要整理 博客的形式 Xmind的形式
3.预习JVM的基础知识点
package com.openlab; import java.io.*; public class MacluClassLoader extends ClassLoader{ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = new File( "c:/test", name.replaceAll(".","/").concat(".class")); try { FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b = 0; while ((b = fis.read())!=0){ baos.write(b); } byte[] bytes = baos.toByteArray(); baos.close(); fis.close(); return defineClass(name,bytes,0,bytes.length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return super.findClass(name);*// throw ClassNotFoundException * } public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ClassLoader cl = new MacluClassLoader(); Class clazz = cl.loadClass("com.openlab.Person"); Person person = (Person) clazz.newInstance(); person.m(); System.*out*.println(cl.getClass().getClassLoader()); System.*out*.println(cl.getParent()); } }
我们可以定义自己格式的classloader,一般情况下class文件就是一个二进制文件流,可以采用一种比较简单的方式对class文件进行加密和解密
加密: 通过^ 异或 可以定义一个数字 在读取每一个字节的后的写入操作时,可以用流里面获取到的数据和这个数字进行异或的算法, 那么这种情况就可以进行加密的操作
解密: 字节数字这个数字这个数字 那么就完成了解密的操作。
package com.openlab; import java.io.*; public class MacluClassLoaderWithEncription extends ClassLoader{ public static int *seed* = 0B10110110; *//* *进行参加加密算法的数字 * @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = new File( "c:/test", name.replaceAll(".","/").concat(".class")); try { FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b = 0; while ((b = fis.read())!=0){ baos.write(b^*seed*); } byte[] bytes = baos.toByteArray(); baos.close(); fis.close(); return defineClass(name,bytes,0,bytes.length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return super.findClass(name);*// throw ClassNotFoundException * } public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, IOException { *encFile*("com.openlab.Person"); ClassLoader cl = new MacluClassLoaderWithEncription(); Class clazz = cl.loadClass("com.openlab.Person"); Person person = (Person) clazz.newInstance(); person.m(); System.*out*.println(cl.getClass().getClassLoader()); System.*out*.println(cl.getParent()); } private static void encFile(String name) throws IOException { File file = new File( "c:/test/", name.replace(".","/").concat(".class")); FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream( new File("c:/test",name.replaceAll(".","/").concat(".macluclass"))); int b = 0; while ((b = fis.read())!=-1){ fos.write(b^*seed*); } fis.close(); fos.close(); } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3Qyd8J2-1641746615745)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image012-164174456853549.jpg)]
生成加密好的文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbhIC0r9-1641746615746)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image014-164174456853550.jpg)]
验证加密文件 打开后是乱码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C9vkEySu-1641746615746)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image016-164174456853551.jpg)]
3.7 编辑器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ZRO5OWR-1641746615747)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image018-164174456853552.jpg)]
解释器: bytecode-interpreter
JIT 即时编辑器 Just In Time compiler
Java语言究竟是一个解释型语言还是编译式的语言
想解释器的可以用解释器,想编译也可以用编译器 看需求是怎么写的 可以通过JVM的一些参数进行设置。
默认的情况是一种混合模式
混合模式:使用解释器+热点编辑器 hotspot
起始阶段采用解释来执行
热点代码的检测 默认值为10000
多次被调用的方法(方法计数器:检测方法的执行频率)
多次被调用的循环(循环的计数器:检测循环的执行频率)
当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。
那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?
-
现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。
-
如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。
-Xmixed 为混合模式:
开始解释执行,启动速度比较快,对热点代码进行检测和编译。
-Xint 解释模式
启动速度很快,执行较慢
-Xcomp 纯编译模式,
启动较慢,执行较快
测试这三个jvm参数
public class WayToRunTest01 { public static void main(String[] args){
*//**这段代码被短时间执行很多次,请JVM虚拟机对其进行优化 *
for (int i = 0;i<10_0000;i++) *m*();
long start =System.*currentTimeMillis*();
for (int i = 0;i<10_0000;i++){ *m*(); }
long end = System.*currentTimeMillis*(); S
ystem.*out*.println(end-start);
}
*//* *该方法本身没有意义,就是耗时间用的。 *
public static void m(){
for (int i = 0;i<10_0000L;i++){
long j = i%3; } } }
默认的混合模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOlUxQpS-1641746615748)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image020-164174456853553.jpg)]
在JVM的执行参数中 -Xint 解释模式
很慢 回去洗洗睡吧
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTdkNqvI-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image022-164174456853554.jpg)]
纯编译的模式-Xcomp
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7Nc4xFC-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image024-164174456853555.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxaQtnKO-1641746615749)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image026-164174456853556.jpg)]
3.8 懒加载
严格来讲应该叫lazyInitializing
JVM规范中并没有规定什么时候加载
严格的规定了初始化的规则 扩展
\1. New对象 getstatic 访问静态变量时 putstatic 访问静态实例时,invokestatic指令
以上指令是必须要初始化这个类 访问final变量除外。
\2. 当反射调用的时候
\3. 初始化子类的时候 首先父类初始化
\4. 虚拟机启动时 被执行的主类必须要初始化
\5. 动态语言支持java.lang.invoke.MethodHandler解析结果为REF-getstatic REF-putstatic REF-invokestatic的方法句柄时 该类必须要初始化。
这个案例 主要看什么时候打印P和X
package com.openlab; public class LazyLoadingTest { public static void main(String[] args) throws ClassNotFoundException { *// P p; // X x = new X(); // System.out.println(P.i); // System.out.println(P.j); * Class.*forName*("com.openlab.LazyLoadingTest$P"); } public static class P{ final static int *i*=8;*//* *打印final的值是不需要加载整个类的 * static int *j* = 9; static{ System.*out*.println("P"); } } public static class X extends P{ static{ System.*out*.println("X"); } } }
面试题:
如何打破classloader的双亲委派模式?
去重写Classloader中的loadClass方法 而不是findClass方法 这个时候就能够打破双亲委派的机制。
什么时候需要打破需要去打破双亲委派的机制:
-
在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法
-
在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。
-
模块化的 热部署 热启动
像osgi和tomcat 都有自己的模块指定classloader,可以加载同一个类库的不同版本的对象,目前这个方式用的比较多。
Tomcat中 Webapplication 对象是可以存在多个的,有两个Webapplication 被加载,但是他们的版本不同,这种情况下可以打破双亲委派的。
注意:类的名字的是相同的 只是说版本不同 如果采用双亲委派的机制,那么这两个对象是不可能加载到同一个空间里面 因为加载的过程中,发现在同一空间有同名的类,那么他一定不会被加载。
所以tomcat的每一个Webapplication 都有一个classloader
双亲委派模式加载
package com.openlab; public class ClassReloadingTest { public static void main(String [] args) throws ClassNotFoundException { MacluClassLoader classloader = new MacluClassLoader(); Class clazz = classloader.loadClass("com.openlab.Person"); classloader = null; System.*out*.println(clazz.hashCode()); classloader = null; classloader = new MacluClassLoader(); Class clazz1 = classloader.loadClass("com.openlab.Person"); System.*out*.println(clazz1.hashCode()); System.*out*.println(clazz == clazz1); } }
从上案例中可以看出,双亲委派,即便重新创建了classloader对象,那么曾经被加载的对象,再次加载的时候,加载的还是这个对象。
热部署应该如何实现
import java.io.*;
public class T012_ClassReloading2 {
private static class MyLoader extends ClassLoader {
@Override public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File("C:/test/" + name.replace(".", "/").concat(".class"));
if(!f.exists()) return super.loadClass(name);
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()]; is.read(b);
return defineClass(name, b, 0, b.length);
} catch ( FileNotFoundException e) { e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} return super.loadClass(name);
}
}
public static void main(String[] args) throws Exception {
MyLoader m = new MyLoader();
Class clazz = m.loadClass("com.openlab.Person");
m = new MyLoader();
Class clazzNew = m.loadClass("com.openlab.Person");
System.out.println(clazz == clazzNew); } }