0
点赞
收藏
分享

微信扫一扫

jvm 之 类加载和初始化

SDKB英文 2022-01-10 阅读 78

文章目录

三、类加载和初始化

面试题:

  1. 描述一下类加载器的层次?

  2. 双亲委派

  3. 为什么要双亲委派

Class文件 如何加载到内存中的 并且是如何执行的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8VDRdKSX-1641746615741)(C:/Users/HX/Desktop/xuexi/%E6%94%BE%E5%9B%BE/clip_image002-164174456853445.jpg)]

3.1 Class Cycle

Class文件在我们硬盘中,那么它是如何加载到内存中 总共需要三个大的步骤

  1. Loading步骤

Loading 是将本地的classfile的二进制的内容加载到内存中

  1. Linking 步骤

    1. Verification 校验

    Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。

    1. Preparation

      Preparation过程 主要作用就是将静态变量赋默认值 假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;

    2. Resolution

      该过程 将class文件中常量池用到的一些符号引用转换为内存地址

  2. 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

多次被调用的方法(方法计数器:检测方法的执行频率)

多次被调用的循环(循环的计数器:检测循环的执行频率)

当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。

那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?

  1. 现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。

  2. 如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。

-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方法 这个时候就能够打破双亲委派的机制。

什么时候需要打破需要去打破双亲委派的机制:

  1. 在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法

  2. 在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。

  3. 模块化的 热部署 热启动

像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);   } }
举报

相关推荐

0 条评论