JVM----类加载和初始化

JVM-类加载和初始化

  • JVM-类加载和初始化
    • 类加载器
      • loading
      • 双亲委派
      • 类加载过程
      • 类加载器范围
    • ClassLoader类加载器
      • 自定义类加载器
      • 准备阶段
      • lazyloading
      • 混合模式
    • 初始化(initializing)
      • 扩展
    • 相关问题
      • 如何打破双亲委派机制
      • Tomcat为什么要重写类加载器?
    • 总结

JVM-类加载和初始化

JVM----类加载和初始化_第1张图片
类加载-初始化

  1. loading 把class文件加载到内存
  2. linking
  3. Verification:校验class文件是否符合标准
  4. preparation:给静态变量赋默认值,如给static int i = 8赋值为i=0
  5. resolution:常量池中的用到的那些符号引用要准换成能访问到的内存地址
  6. initializing :这时候才会调用静态代码块给静态变量赋值

类加载器

JVM----类加载和初始化_第2张图片

loading

jvm中所有的class都是被classloader加载到内存
以上几个类加载器的关系不是继承,是父加载器与自加载器的关系。

双亲委派

  • 父加载器
    父加载器不是“类加载器的加载器”
  • 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程

那么问题来了, 为什么要搞双亲委派
java.lang.String类由自定义加载器加载行不行?

回答这个问题, 首先要弄明白class的加载过程。

根据上图所示,一个class类首先要经过CustomClassloader(自定义类加载器),查询其缓存中是否已经将该class加载,如果有,则将其返回,没有,则向上检查,此时到了APP(AppClassLoader,同样检查其缓存是否已加载,没有,则继续向上,Extension加载器同样如此,一直检查到BootStrap加载器,当Bootstrap加载器同样没有加载该calss时,开始自顶向下进行实际查找和加载。首先判断该类是否该由Bootstrap加载,不是,则向下,一直到Custom加载器,如果没有找到,则抛异常(ClassNotFound)。

主要是为了安全
假设自定义了一个Java.lang.String,覆盖sun的String,同时自定义一个String的类加载器,将自定义的这个String加载到内存,接下来将整个自定义部分打包成一个类库,交给客户使用 ,此时客户输入密码将会变得非常不安全。
但是采用双亲委派就不会有这个问题,自低向上检查,一直到Bootstap,发现String类已经被Bootstrap加载,其他加载器便不能再次加载这个类,从而保证了安全。

类加载过程

JVM----类加载和初始化_第3张图片

类加载器范围

JVM----类加载和初始化_第4张图片
这些加载范围是由launcher的源码决定
JVM----类加载和初始化_第5张图片
JVM----类加载和初始化_第6张图片

JVM----类加载和初始化_第7张图片
查看每个目录下都有哪些jar包

public class T003_ClassLoaderScope {
    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()));
    }
}

输出结果

C:\Program Files\Java\jdk1.8.0_51\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_51\jre\classes
--------------------
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
--------------------
C:\Program Files\Java\jdk1.8.0_51\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\rt.jar
G:\SoftWare\Java\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar

ClassLoader类加载器

package com.cyc.jvm.c2_classloader;

/**
 * classloader加载器
 */
public class T002_ClassLoaderLevel {
    public static void main(String[] args) {
        //String是由 bootStrapClassLoader加载的
        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());
        //自定义类的classLoader的类加载器为AppClassLoader
        System.out.println(T002_ClassLoaderLevel.class.getClassLoader());
        //extClassLoader的类加载器是BootStrapClassLoader加载的
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
        System.out.println(T002_ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());

        //一个自定义classLoader的默认父classLoader是AppClassLoader
        System.out.println(new T006_CYCClassLoader().getParent());
        //ClassLoader的systemClassLoader也是AppClassLoader
        System.out.println(ClassLoader.getSystemClassLoader());
    }
}

自定义类加载器

准备阶段

  1. 一个需要解析的的class文件
package com.cyc.jvm;

public class Hello {
    public void m() {
        System.out.println("Hello JVM!");
    }
}

  1. 去项目目录下找到Hello的class文件, 带上根目录复制到D盘的test文件夹下

JVM----类加载和初始化_第8张图片

  1. 自定义类加载器

public class T006_CYCClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("D:/test/", name.replace(".", "/").concat(".class"));
        try {
            FileInputStream fis = new FileInputStream(f);
            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 (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new T006_CYCClassLoader();
        Class clazz = l.loadClass("com.cyc.jvm.Hello");
        Class clazz1 = l.loadClass("com.cyc.jvm.Hello");
        //由于使用的是同一个类加载器,加载出来的是同一个class对象, 所以这里会输出true
        System.out.println(clazz == clazz1);

        Hello h = (Hello)clazz.newInstance();
        h.m();
        //自定义类加载器的类加载器是AppClassLoader
        System.out.println(l.getClass().getClassLoader());
        //他的父加载器同样也是AppClassLoader,但是注意, 他们之间不是继承关系。这些类加载器继承的都是ClassLoader
        System.out.println(l.getParent());
        System.out.println(getSystemClassLoader());
    }
}

lazyloading

JVM----类加载和初始化_第9张图片

public class T008_LazyLoading { //严格讲应该叫lazy initialzing,因为java虚拟机规范并没有严格规定什么时候必须loading,但严格规定了什么时候initialzing
    public static void main(String[] args) throws Exception {
        P p;
        X x = new X();
        System.out.println(P.i);
        System.out.println(P.j);
        Class.forName("com.cyc.jvm.c2_classloader.T008_LazyLoading$P");

    }

    public static class P {
        final static int i = 8;
        static int j = 9;
        static {
            System.out.println("P");
        }
    }

    public static class X extends P {
        static {
            System.out.println("X");
        }
    }
}

混合模式

JVM----类加载和初始化_第10张图片
测试

package com.cyc.jvm.c2_classloader;

public class T009_WayToRun {
    public static void main(String[] args) {
        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();
        System.out.println(end - start);
    }

    public static void m() {
        for(long i=0; i<10_0000L; i++) {
            long j = i%3;
        }
    }
}

首先是混合模式,这也是默认的运行模式
JVM----类加载和初始化_第11张图片
混合模式运行时间

JVM----类加载和初始化_第12张图片
解释模式
JVM----类加载和初始化_第13张图片
查看运行时间(时间过于漫长)

编译模式
JVM----类加载和初始化_第14张图片

查看运行时间
JVM----类加载和初始化_第15张图片

初始化(initializing)

package com.cyc.jvm.c2_classloader;

public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    //这个顺序,由于new T(),会进行T的初始化, 给T赋值为null,T中的成员编程int型的count赋值为0,
    //然后调用T的构造方法, 执行count++, 此时count为1,接来下开始执行 public static int count = 2;在这里给count赋值为2
    public static T t = new T(); // null
    public static int count = 2; //0

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}
class T {

    //首先归对象T进行初始化, 此时对象为null, 对象内的变量count赋值为默认值0,然后在initial阶段给count赋指定值2
    //接着调用T的构造方法, 执行count++, count变为3
    public static int count = 2; //2->3
    public static T t = new T(); // null->对象

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}

new对象的过程其实也是分为两步, new 出来T , 先给里面的成员变量赋默认值,new出来T,申请完内存之后,开始调用构造方法,才给成员变量赋初始值。

扩展

结合单例模式解析初始化过程

public class Singleton06 {

    //volatile关键字禁止指令重排
    private static volatile Singleton06 INSTANCE;


    public void c() {
        System.out.println("C");
    }

    /**
     * 构造方法为私有,只能在当前类中new,外部类无法new出来
     */
    private Singleton06() {
    }

    public static Singleton06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Singleton06.class) {
                if (INSTANCE == null) {
                    try {
                        //这里让进入此代码块的线程睡一毫秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(Singleton06.getInstance().hashCode())
            ).start();
        }
    }

}

为什么要加volatile?

为了防止指令重排。这里涉及到类的加载过程。

首先,第一个线程进来了, 加上锁之后,进入到INSTANCE = new Singleton06();代码,在初始化进行到一半的时候,也就是在preparation阶段,已经给Singleton06申请完内存,里面的成员变量已经赋过默认值,比如0,此时INSTANCE 已经指向这个分配的内存, 已经不再是null,此时另外一个线程进来了,由于此时INSTANCE 已经进行了半初始化状态,所以在if (INSTANCE == null)为false,此时另一个线程会拿到这个INSTANCE中的成员变量进行操作, 这样显然是不满足要求的。

想要解析这个问题, 需要查看其字节码文件

例如下面这个测试类T, 使用idea插件查看其字节码文件

在这里插入图片描述

在0 new #2 之后,已经申请过内存。

4 invokespecial #3 这个给类中的静态变量赋初始值

在调用完4之后,才会把这块内存赋值给t,但是由于指令可能会重排的原因, 如果先执行的是7 astore_1, 相当于先把这个地址扔到内存中, 然后在进行的T初始化, 这种情况下,在双重检查懒汉式单例中,就会出现有别的线程读取到半初始化的单例。

相关问题

如何打破双亲委派机制

如果只是重写findClass方法, 是无法打破双亲委派机制的, 示例如下

package com.cyc.jvm.c2_classloader;

public class T011_ClassReloading1 {
    public static void main(String[] args) throws Exception {
        T006_CYCClassLoader cycClassLoader = new T006_CYCClassLoader();
        Class clazz = cycClassLoader.loadClass("com.cyc.jvm.Hello");

        cycClassLoader = null;
        System.out.println(clazz.hashCode());

        cycClassLoader = null;

        cycClassLoader = new T006_CYCClassLoader();
        Class clazz1 = cycClassLoader.loadClass("com.cyc.jvm.Hello");
        System.out.println(clazz1.hashCode());

        System.out.println(clazz == clazz1);
    }
}

输出结果

JVM----类加载和初始化_第16张图片

可以看到两者class的hashcode值相同, 所以, 依然是同一个class对象。

显然这里需要重写loadclass方法才行

package com.cyc.jvm.c2_classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        //重写loadClass方法, 每次都去加载新的class , 而不是去类加载器缓存池中去找该类是否已加载
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            File f = new File("D:/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 (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.cyc.jvm.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("com.cyc.jvm.Hello");

        System.out.println(clazz == clazzNew);
    }
}

查看输出结果

JVM----类加载和初始化_第17张图片


第一次被加载的类, 在类空间里,当它的classLoader被干掉之后, 由于没有任何引用指向它了, 所以会被gc回收。

  • bootstrap加载器为什么返回的是null?

    因为它是由c++编写的,java中并没有与之对应的class

  • class的加载过程用到了哪些设计模式?

    classloader的load过程用到了设计模式中的模板方法模式,因为所有方法都已经写好了,如果要自定义classloader,自己只需要重写findclass方法就可以了。

Tomcat为什么要重写类加载器?

无法实现隔离性:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的。部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM, web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离

无法实现热替换:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。

打破双亲委派机制(参照JVM中的内容)OSGI是基于Java语言的动态模块化规范,类加载器之间是网状结构,更加灵活,但是也更复杂,JNDI服务,使用线程上线文类加载器,父类加载器去使用子类加载器

JVM----类加载和初始化_第18张图片

  1. tomcat自己定义的类加载器:

    CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和各个webapp访问

    CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的class,即对webapp不可见

    SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见

    WebappClassLoader:webapp私有的类加载器,只对当前webapp可见

  2. 每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个JspClassLoader,所以这两个类加载器有多个实例

  3. 工作原理:

    a. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用

    b. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离

    c. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系

    d. 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

  4. tomcat目录结构,与上面的类加载器对应

    /common/*

    /server/*

    /shared/*

    /WEB-INF/*

  5. 默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader的实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成了一个lib目录。所以在我们的服务器里看不到common、shared、server目录。

总结

  1. 加载过程
    1. Loading

      1. 双亲委派,主要出于安全来考虑

      2. LazyLoading 五种情况

        1. –new getstatic putstatic invokestatic指令,访问final变量除外

          –java.lang.reflect对类进行反射调用时

          –初始化子类的时候,父类首先初始化

          –虚拟机启动时,被执行的主类必须初始化

          –动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

      3. ClassLoader的源码

        1. findInCache -> parent.loadClass -> findClass()
      4. 自定义类加载器

        1. extends ClassLoader
        2. overwrite findClass() -> defineClass(byte[] -> Class clazz)
        3. 加密
        4. 如何打破双亲委派
          1. 用super(parent)指定
          2. 双亲委派的打破
            1. 如何打破:重写loadClass()
            2. 何时打破过?
              1. JDK1.2之前,自定义ClassLoader都必须重写loadClass()

              2. ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定

              3. 热启动,热部署(会把整个classLoader都干掉,把class重新load一遍)

                1. osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)

                  每一个webApplication,都有自己的一个classLoader, 每个classLoader中可以有自己的类。

      5. 混合执行 编译执行 解释执行

        1. 检测热点代码:-XX:CompileThreshold = 10000
    2. Linking

      1. Verification
        1. 验证文件是否符合JVM规定
      2. Preparation
        1. 静态成员变量赋默认值
      3. Resolution
        1. 将类、方法、属性等符号引用解析为直接引用
          常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
    3. Initializing

      1. 调用类初始化代码 ,给静态成员变量赋初始值

你可能感兴趣的:(JVM,jvm,java)