jvm课程学习一

课堂PPT

jvm课程学习一_第1张图片jvm课程学习一_第2张图片jvm课程学习一_第3张图片jvm课程学习一_第4张图片jvm课程学习一_第5张图片jvm课程学习一_第6张图片jvm课程学习一_第7张图片
jvm课程学习一_第8张图片
jvm课程学习一_第9张图片jvm课程学习一_第10张图片
jvm课程学习一_第11张图片spi的原因:

实现由spi内置于jdk核心包,由启动类加载器进行加载,但是实现类却由应用类加载器加载,在程序的运行中通过thread.currentThead.getContextClassloader 改变类的加载器

违背了双亲模型 因为双亲模型中子类由某个类加载,则父类也由它加载
jvm课程学习一_第12张图片jvm课程学习一_第13张图片jvm课程学习一_第14张图片jvm课程学习一_第15张图片jvm课程学习一_第16张图片jvm课程学习一_第17张图片jvm课程学习一_第18张图片jvm课程学习一_第19张图片jvm课程学习一_第20张图片

类加载

  • 在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
  • 提供了更大的灵活性,增加了更多的可能性
    1)此处类型代表class对象
    2)在runtime期间完成加载、连接和初始化
    3)加载:class文件加载到内存中

类加载器深入剖析

  • java虚拟机与程序的生命周期
  • 在如下几种情况下,java虚拟机将结束生命周期
    1)执行了system.exit()方法
    2)程序正常执行结束
    3)程序在执行的过程中遇到了异常或错误而异常终止(抛异常不处理抛到main方法导致jvm退出)
    4)由于操作系统出现错误而导致java虚拟机进程终止

类的加载、连接和初始化

  • 加载:查找并加载类的二进制数据到内存

  • 连接:
    1)验证:确保被加载的类符合jvm的规范
    2)准备:为类的静态变量分配内存,并将其初始化为默认值(int置为0,Boolean置为false)
    3)解析:将类中的符号引用转化为直接引用

  • 初始化:为类的静态变量赋予正确的初始化值

public class Test {
    /*
    在准备阶段a为默认值0,
    在初始化阶段初始化为1
    * */    
    public static int a=1;
    
}

符号引用:字符串,能根据这个字符串定位到制定数据,比如java/lang/StringBuilder
直接引用:指针
方法区:jvm中定义的一个概念,用于存储类信息,常量池,静态变量等信息。永久代是hotspot中的方法区的实现。

类的使用与卸载

jvm课程学习一_第21张图片

public class Test {
    /*
    初始化后仍旧为0
    * */
    public static int a;

}

类的加载、连接与初始化

  • java程序对类的使用方式分为两种
    1)主动使用
    2)被动使用

  • 所有的java虚拟机实现必须在每个类或者接口被java程序“首次主动使用”时才初始化它们
    1)首次
    2)主动使用

主动使用(七种)

  • 创建类的实例(new对象)
  • 访问某个类或接口的静态变量,或者对该静态变量赋值(拥有静态变量的类,比如静态变量在父类声明,但是使用子类去访问,不会触发子类的初始化,只会触发父类的初始化)
  • 调用类的静态方法(拥有静态方法的类,同上)
  • 反射(如Class.forName(“com.test.Test”))
  • 初始化一个类的子类,要是父类没有初始化此时会触发父类的初始化
  • java虚拟机启动时被标明启动类的类(java Test)
  • JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化则初始化

除了以上七种情况,其他使用java类的方式被看做是对类的被动使用,都不会导致类的初始化

虽然不初始化但是可能会加载

类的加载

  • 类的加载是指将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后再内存中创建一个java.lang.Class对象(规范并未说明Class对象放在哪里,hotspot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构

  • 加载.class文件的方式
    1)从本地文件系统中直接加载
    2)通过网路下载.class文件
    3)从zip,jar等归档文件中加载.class文件
    4)从专用数据库中提取.class文件
    5)将java源文件动态编译为class文件(动态代理)

public class Test1 {

    /*
    *  System.out.println(MyChild.str1);只会触发父类的初始化,子类不会初始化
    * System.out.println(MyChild.str);父类没有初始化会触发父类的初始化
    * -XX:+TraceClassLoading
    * System.out.println(MyChild.str1);可以触发子类的加载
    * -XX:+
    public static void main(String[] args) {
//        System.out.println(MyChild.str);
        System.out.println(MyChild.str1);
    }

}

class MyChild extends MyParent{
    public static String str = "hello";
    static {
        System.out.println("MyChild");
    }
}

class MyParent{
    public static String str1 = "parent";
    static {
        System.out.println("MyParent");
    }
}
public class MyTest2 {

    /*
    *  public static final String str="hello";常量在编译器会被保存在调用这个常量的类的(MyTest2)常量池中,此时可以将
    * 本质上,调用并不会调用直接引用的定义常量的类,所以不会触发初始化。
    * 之后MyParent2和MyTest2没有任何关系,删除编译后的MyParent2.class文件,程序仍旧可以正常运行
    * ldc表示将int,float或者string类型的常量值从常量池中取出推送至栈顶
    * bipush表示将单字节(-128~127)内的常量值推送至栈顶
    * sipush表示将短整型常量值(-32768~32767)
    * iconst_1表示将int类型的1推送至栈顶(iconst_1~iconst_5)
    * 这些助记符可以在jdk中找到相应的定义
    * */
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

class MyParent2 {

    public static final String str="hello";

    static {
        System.out.println("MyParent2");
    }
}
javap jdk自带的反编译工具
G:\code\jvm\out\production\jvm\com\jvm\study>javap -c MyTest2.class
Compiled from "MyTest2.java"
public class com.jvm.study.MyTest2 {
  public com.jvm.study.MyTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String hello
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

源码如下:
public class MyTest2 {

    /*
    *  public static final String str="hello";常量在编译器会被保存在调用这个常量的类的(MyTest2)常量池中,此时可以将
    * 本质上,调用并不会调用直接引用的定义常量的类,所以不会触发初始化。
    * 之后MyParent2和MyTest2没有任何关系,删除编译后的MyParent2.class文件,程序仍旧可以正常运行
    * */
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

public class MyTest3 {

    /*
    * 当一个常量的值并非在编译期间可以确定的,那么其值就不会被找到并放到常量池中
    * 这是在程序运行时,会导致主动使用这个常量所在的类,显然会导致这类的初始化
    * */
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }

}

class MyParent3{
    static final String str= UUID.randomUUID().toString();
    static {
        System.out.println("MyParent3");
    }
}

public class MyTest4 {

    /*
    对于数组来书,其类型实jvm在运行期间动态生成的,表示为class [Lcom.jvm.study.MyParent4
    这种形式,动态生成的类型,其夫类型就是object

    对于数组来说,javaDoc经常将构成数组的元素为component,实际上就是将数组降低一个维度后的类型

    此时不会触发MyParent4的初始化

    助记符:
    anewarray:表示创建一个引用类型的(如类,接口,数组)数组,并将其压入栈顶
    newarray:表示创建一个原始的(如int,char,short,boolean,byte等)数组,并将其压入栈顶
     */

    public static void main(String[] args) {
//        MyParent4 myParent4 = new MyParent4();
        MyParent4[] myParent4s = new MyParent4[1];
        System.out.println(myParent4s.getClass());

        MyParent4[][] myParent4s1 = new MyParent4[1][1];
        System.out.println(myParent4s1.getClass());
        System.out.println(myParent4s.getClass().getSuperclass());
        System.out.println(myParent4s1.getClass().getSuperclass());

        int[] ints = new int[0];
        System.out.println(ints.getClass());
        System.out.println(ints.getClass().getSuperclass());

        char[] chars = new char[0];
        System.out.println(chars.getClass());
        System.out.println(chars.getClass().getSuperclass());

        short[] shorts = new short[0];
        System.out.println(shorts.getClass());
        System.out.println(shorts.getClass().getSuperclass());

        byte[] bytes = new byte[0];
        System.out.println(bytes.getClass());
        System.out.println(bytes.getClass().getSuperclass());
    }

}
class MyParent4{

    static {
        System.out.println("MyParent4");
    }
}

此时并不会触发数组类对象的加载

[Loaded com.jvm.study.MyTest4 from file:/G:/code/jvm/out/production/jvm/]
[Loaded sun.launcher.LauncherHelper$FXHelper from C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded com.jvm.study.MyParent4 from file:/G:/code/jvm/out/production/jvm/]
class [Lcom.jvm.study.MyParent4;
class [[Lcom.jvm.study.MyParent4;
class java.lang.Object
class java.lang.Object
class [I
class java.lang.Object
class [C
class java.lang.Object
class [S
class java.lang.Object
class [B
class java.lang.Object
public class MyTest5 implements MyParent5{

    /*
    * 当一个接口在初始化时,并不要求父类也初始化
    * 是由在真正使用父接口时才会初始化
    *
    * 接口中的变量默认添加final
    *
    * */

    static String str = "hello";

    public static void main(String[] args) {
//        System.out.println(MyParent5.parent);
//        System.out.println(MyTest5.str);
        System.out.println(MyChild5.uuid);
//        System.out.println(MyChild5.string);
//        System.out.println(MyChild5.child);
    }

}
interface  MyParent5{
    public static Thread parent=new Thread(){
        {
            System.out.println("MyParent5");
        }
    };
}
interface MyChild5 extends MyParent5{
    String string = "hello child";
    String uuid = UUID.randomUUID().toString();
    public static Thread child=new Thread(){
        {
            System.out.println("MyChild5");
        }
    };
}

public class MyTest7 {

    /*
    * string 为rt包下,有bootstrapclassload进行加载,所以为空
    * C在classpath路径下,有系统类加载器(应用加载器)
    * */
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(C.class.getClassLoader());
    }

}
class C{

}

//输出
null
sun.misc.Launcher$AppClassLoader@18b4aac2

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类构成
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
  • 同一命名空间内的类是相互可见的,非同一命名空间内的类是不可见的
  • 子加载器可以见到父加载器加载的类,父加载器也不能见到子加载器加载的类

类的卸载

  • 当一个类被加载、连接和初始化之后,它的生命周期就开始了。当此类的class对象不再被引用,即不可触及时,class对象就会结束生命周期,类在方法区内的数据也会被卸载
  • 一个类何时结束生命周期,取决于代表它的class对象何时结束生命周期
  • 由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期汇总,始终不会被卸载。java虚拟机本身会始终引用这些加载器,而这些类加载器则会始终引用他们所加载的类的class对象,因此这些class对象是可触及的
  • 有用户自定义的类加载器所加载的类可以被卸载的。(jvisualvm查看当前java进程 -XX:+TraceClassUnloading用于追踪类的卸载情况)

类的加载

  • jvm规范允许类加载器在预料某个类将要被使用是就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载必须在程序首次主动使用该类的才报告错误(LinkageError错误)
  • 如果这个类一直没有被程序主动使用,那么该类加载器就不会报告错误

jvm课程学习一_第22张图片

public class MyTest20 {
	
    public static void main(String[] args) throws Exception {
        CustomerClassLoader customerClassLoader1 = new CustomerClassLoader("classloader");
        CustomerClassLoader customerClassLoader2 = new CustomerClassLoader("classloader");
        customerClassLoader1.setPath("C:\\Users\\xingkong\\Desktop\\jvm");
        customerClassLoader2.setPath("C:\\Users\\xingkong\\Desktop\\jvm");
        Class<?> class1 = customerClassLoader1.loadClass("com.jvm.study.MyPerson");
        Class<?> class2 = customerClassLoader2.loadClass("com.jvm.study.MyPerson");
        System.out.println(class1 == class2);

        Object objec1=class1.newInstance();
        Object objec2=class2.newInstance();
        Method methods = class1.getMethod("setMyPerson", Object.class);
        methods.invoke(objec1, objec2);
    }

}

结果输出为:

false
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.jvm.study.MyTest20.main(MyTest20.java:26)
Caused by: java.lang.ClassCastException: com.jvm.study.MyPerson cannot be cast to com.jvm.study.MyPerson
	at com.jvm.study.MyPerson.setMyPerson(MyPerson.java:20)
	... 5 more

不同的类加载器的命名空间关系

  • 同一个命名空间内的类是相互可见的,子加载器的命名空间包含所有的父加载器的命名空间,因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载加载的类能看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。如果两个加载器之间没有直接或者间接的父子关系,那么它们各自加载的类相互不可见。
  • 可见不代表可以访问,可访问通过修饰符进行控制

类加载器的双亲委托模型的好处

  • 可以确保java核心库的类型安全:所有的java应用都是至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到java虚拟机中,如果这个加载过程是否java应用自己的类加载器所完成的,那么很可能就会在jvm中存在多个版本的java核心类,二期这些类之间换是不兼容的,相互不可见的(正式命名空间在发挥作用)。借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载来完成,从而确保了java应用所使用的都是同一个版本的java核心类库,他们之间是兼容的。
  • 可以确保java核心类库所提供的的类不会被自定义的类所代替
  • 不同的类加载器可以为相同的名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载来加载即可,不同类加载器所加载的类之间是不兼容的,这就相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间,这类技术在很多框架汇总都得到了实际应用。
    jvm课程学习一_第23张图片
  • 内建于jvm中的启动类加载器会加载java.lang.ClassLoader以及其他的java平台类。当jvm启动时,一块特殊的机器码会执行,它会加载扩展类加载器和系统类加载去。这块特殊的机器码叫做启动类加载器(Bootstrap)。启动类加载器并不是java,而其他的加载器都是java类
    。启动类加载是特定于平台的机器指令,它负责开启整个加载过程。所有的类加载(处理启动类加载器)都被实现为java类,不过,总归要有一个组件来加载一个java类加载器。从而让整个加载过程能够顺利运行下去,加载第一个纯java类加载是就是启动类加载的职责。 启动类加载器换负责加载供jre正常运行所需的基本组件,这包括java.util和java.lang包中的类
//	SPI
        Class.forName("com.mysql.jdbc.Drive");
        Connection connection = Driver.getConnection();
        Statement statement = connection.getStatement();
当前类加载器(current class loader)
 每个类都会使用自己的类加载器(即加载自身的类加载)来气加载其他类(指的是所依赖的类),  
 线程上下文类加载(context classloader)    
 线程上下文类加载是从jdk1.2开始引入,类thread中的getContextClassLoader()与
 setContextClassloader()分别用来获取和设置上下文类加载器    
 如果没有通过setContextClassLoader(classLoader c1)运行设置的话,线程将继承其类线程的上下文类加载器,
 java应用运行时的和初始线程上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源

 线程上下文类加载的重要性:
 SPI(services provider  Interface)

 父classloader可以使用当前的线程thread.currentThread(),getContextClassLoader()所指定的classLoader
 加载的类,这就改变了父classLoader不能使用子classloader或是其他没有直接父子类关系的classloader加载的
 类的情况,即改变了双亲委托模型

 线程上下文类加载器就是当前线程的current  classloader,

 在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上进行加载,但是对于spi来说,有些接口
 是java核心类库所提供的,而java核心类库是由启动类加载器来加载的,而这些就接口的实现却来自与不同的jar包,
(厂商提供),java的启动类加载是不回家在其他来源的jar包,这样传统的双亲委托模型就无法满足spi的需求,
 而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载来实现对于接口类型类的加载。
  •    线程上下文类加载器的一般使用模式(获取-使用-还原)
    classLoader c1=Thread.currentThread().getContextClassLoader();
     try{
         Thread.currentThread().setContextClassLoader(targetTest1);
     }finally{
         Thread.currentThread().setContextClassLoader(targetTest1);
     }
    
     myMethod里面则调用了Thread.currentThread().getContextClassLoader(),
     获取当前线程的的上下文类加载器做某些事情,如果一个类由类加载器A加载,那么
     这个类的依赖类也是由相同的类加载加载的(如果该依赖之前没有加载过)
     
     contextClassLoader的作用就是为了破坏java的类加载器委托机制
     
     当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)
     底层的类时,就必须通过线程上下文类加载器来帮助高层classloader找到
     并加载该类。
    

你可能感兴趣的:(java)