深刻理解JVM虚拟机

  • JVM平台上还能够运行其余语言,运行的是Class字节码。只要能翻译成Class的语言就OK了。挺强大的。
  • JVM厂商不少
  • 垃圾收集器、收集算法
  • JVM检测工具

 

关于类的加载:java

  •  Java代码中,类型(interface, class,enum等,有些是在运行时候生成的,好比动态代理)的加载、链接与初始化过程都是在程序运行期间完成的。不涉及到对象的概念。同时也是个Runtime阶段。
  •  提供了更大的灵活性,增长了更多的可能性。提供了一些扩展,灵活扩展。

    

Java虚拟机与程序的生命周期:linux

  在以下几种状况下,Java虚拟机将会结束生命周期:程序员

  1. 执行了System.exit()方法
  2. 程序正常执行结束
  3. 程序执行过程遇到了异常或者错误异常终止了
  4. 操做系统出现错误致使Java虚拟机进行终止

 

类的加载、链接与初始化:算法

加载:查找并加载类的二进制数据数据库

链接: api

  • 验证: 确保被加载类的正确性。Class有格式的。
  • 准备:为类的静态变量分配内存,并将其初始化为默认值  
  • 注:
    1.类的静态变量或类的静态方法,一般能够看作全局的,由类去直接调用。此时仍是个类的概念,不存在对象。
    2.关于默认值问题:
    class Test{
    public static int a = 1;
    }
    中间过程: Test类加载到内存的过程当中,会给a分配一个内存。而后将a初始化为默认值0(整型变量)

  • 解析: 把类中的符号引用转为直接引用。符号的引用也是间接的引用方式。

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

  • class Test{
       public static int a = 1;
      }
    此时的a才真正成为1了
    

      

类的使用与卸载安全

 使用: 类的方法变量使用等服务器

 卸载: class字节码文件,加载到内存里面。造成了本身的数据结构,驻留在内存里面。能够销毁掉。卸载到了就不能进行new 对象了。网络

 

整体流程:

 

 

 

Java程序对类的使用方式分为两种:

  1. 主动使用
  2. 被动使用

 

全部的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才初始化他们。即初始化只会执行一次。

 

主动使用,七种(非精确划分,大致划分):

  1. 建立类的实例。
  2. 访问某个类或接口的静态变量,或者对静态变量赋值。 字节码层面上,使用的助记符:get static、  put static
  3. 调用类的静态方法。 invoke static
  4. 反射(如Class.forName("com.test.t1"))
  5. 初始化一个类的子类
    好比:
    
     class Parent{}
     class Child extends Parent{}
    
    初始化Child时候,先去初始化Parent 
  6. Java虚拟机启动时被代表为敌情类的类(Java Test)
    Java虚拟机启动时候,被标明为启动的类,即为有main方法的类,也会主动使用 
  7. JDK1.7开始提供动态语言支持:
    注:
    1.java.lang.invoke.MethodHandle实例的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic句柄对应的类没有初始化,则初始化
    2.1.7开始提供了对动态语言的支持。特别的JVM平台上经过脚本引擎调用JS代码(动态语言)。  

:助记符了解便可

 

除了以上七种状况,其余使用Java类的方式都被看作是对类的被动使用,都不会致使类的初始化

 

类的加载:

 类的加载指的是将类 .class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,而后在内存中建立一个java.lang.Class对象(规范并说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中,JVM没有规范这个)用来封装类在方法区内的数据结构

 引伸:一个类无论生成了多少实例,全部的实例对应只有一份Class对象。 Class对象是面镜子,能反映到方法区中的Class文件的内容、结构等各类信息。

加载.class文件的方式:

  1. 从本地系统中直接加载
  2. 经过网络下载
  3. 从zip、jar等贵方文件中加载
  4. 从转悠数据库中提取
  5. 将Java源文件动态编译为.class文件

 

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str1);
//        System.out.println(MyChild1.str2);
    }
}

class MyParent1{

    //静态成员变量
    public static String str1 = "str1";
    // 静态代码块(程序加载初始化时候去执行)
    static {
        System.out.println("MyParent1 -----> static block running");
    }
}
class MyChild1 extends MyParent1{
    //静态成员变量
    public static String str2 = "str2";
    static {
        System.out.println("MyChild1 -----> static block running");
    }
}

  

 

 

 

str1 子类调用了继承到的父类的str1,子类的静态代码块没有执行。str1是父类中定义的。MyParent1的主动使用,可是没有主动使用MyChild1. 总结:看定义的!

 

 

str2 能够执行,同时初始化子类时候,父类会主动使用。全部的父类都会被初始化

 

MyTest1是一个启动类,主动使用。先加载之。 

 

总结: 

  1. 对于静态字段来讲,只有直接定义了该字段的类才会被初始化。
  2. 当一个类在初始化时候,要求其父类所有已经初始化完毕。每一个父类最多只能初始化一次! 

 

引伸: -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来。能够看到类的加载状况。

           打印: 虚拟机在当前启动状况下所加载的类的信息。

 

总结设置方式:

全部JVM参数都是: -XX: 开头  

  相似于Boolean类型的开关:

        -XX:+<option> 表示开启option选项

        -XX: - <option>  表示关闭option选项

  赋值:   

     -XX:<option>=<value>, 表示将option选项的值设置为value  

 

 

 关于常量:

public class MyTest2 {

    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

class MyParent2{

    // final修饰成为常量
    public static final String str = "hello world";

    static {
        System.out.println("MyParent2 ----> run");
    }

}

 

 

 

 

在编译阶段这个常量被存入到 调用这个常量的方法所在的类的常量池中

本例中:

   “hello world”是一个常量,会放置到MyTest2类的常量池中。

   这里指的时将常量存放到了MyTest2的常量池汇总,以后MyTest2与MyParent2就没有任何关系了

   甚至,极端一些。咱们能够将MyParent3的class文件删除。(编译完毕后,把class字节码删除

 

总结:

  •  常量编译阶段会存入到调用这个常量的方法所在的类的常量池中。
  •  本质上,调用类并无直接引用到定义常量的类,所以并不会触发定义常量类的初始化。

 

引伸反编译: javap -c  类的全路径名字

 

助记符引伸:

  • ldc表示将int,float 或 String类型的常量值从常量池中推送至栈顶。
  • bipush表示将单字节(-128 ~ 127)的常量值推送至栈顶  
  • sipush表示将一个短整型常量值(-32768 ~ 32767)推送至栈顶
  • iconst_1 表示将int类型的1推送至栈顶 (iconst_1 ~ iconst_5)

助记符是在rt.jar中相关类去实现的。

 

 

若是常量的值,在编译器不能肯定下来呢?

public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}

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

 

 

此时放在MyTest3类的常量池中没有意义的。

 

总结:  

  当一个常量值并不是编译期间能够肯定的,那么其值就不会被放到调用类的常量池中。这时在程序运行时,会致使主动使用这个常量所在的类,显然会致使这个类被初始化。

 

new对象实例状况:

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4 myParent4 = new MyParent4();
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

对这个类的主动使用。

若是屡次new,只会初始化一次。首次主动使用。

 

数组状况:

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[1];
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

不在七种状况范围内。不会初始化!

不是MyParent4的实例!

到底建立的什么实例?getClass!,数组的实例究竟是个啥玩意儿?

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[1];
        //看看是啥
        Class<? extends MyParent4[]> aClass = myParent4s.getClass();
        System.out.println(aClass);
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

Java虚拟机在运行期,建立出来的类型。是个数组类型。有点相似动态代理

 

数组类型也是比较特殊的。[Lxxxx

二维数组也是同样的特殊

 

看下父类型:

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[1];
        //看看是啥
        System.out.println(myParent4s.getClass().getSuperclass());
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

父类型实际上是Object

 

 

 

总结:

 对于数组实例来讲,其类型是由JVM在运行期动态生成的

 动态生成的类型,其父类就是Object

 对于数组来讲,JavaDoc常常将构成数组的元素为Component,实际上就是将数组下降一个维度后的类型。

 

看下原生类型的数组:

public class MyTest4 {
    public static void main(String[] args) {
       int[] ints = new int[3];
        System.out.println(ints.getClass());
        System.out.println(ints.getClass().getSuperclass());
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

 

 

 

助记符:

  anewarray: 表示建立一个引用类型的(好比类、接口、数组)数组,并将其引用值压如栈顶。

  newarray: 表示建立一个指定的原始类型(如:int,float,char等)的数组,并将其引用值压入栈顶。

 

以上所总结的是类与类之间的关系,包括继承的。下面接口的特色:

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}

interface MyParent5 {
    public static int a = 5;
}

interface MyChild5 extends MyParent5 {
    public static int b = 6;
}

  接口是没有静态代码块的。能够经过手动删除class文件来证实之。

 

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}

interface MyParent5 {
    public static int a = 5;
}

interface MyChild5 extends MyParent5 {
    // 只有在运行时候才会赋值,会放到MyTest5的常量池里面。若是Class删除了,运行时候就会报错!
    public static int b = new Random().nextInt(2);
}

 

  结论:

  • 当一个接口在初始化时候,并不要求其父类接口都完成了初始化。
  • 只有在真正使用到父类接口的时候(如引用接口中定义的常量时),才会初始化。
  • 类,必定要先初始化父类。

 

public class MyTest6 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        System.out.println("counter"+ instance.counter1);
        System.out.println("counter"+ instance.counter2);
    }
}
class Singleton{
    public static int counter1;
    public static int counter2 = 0;

   private static Singleton singleton = new Singleton();

   private Singleton(){
       counter1++;
       counter2++;
   }
   public static Singleton getInstance(){
       return singleton;
   }
}

 

 

分析: 先赋值: 默认的0 和 给定的0,而后构造方法进行++操做。 

 

若是更改位置:

public class MyTest6 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        System.out.println("counter1-->"+ instance.counter1);
        System.out.println("counter2-->"+ instance.counter2);
    }
}
class Singleton{
    public static int counter1;
   private static Singleton singleton = new Singleton();

   private Singleton(){
       counter1++;
       counter2++;
       System.out.println(counter1);
       System.out.println(counter2);
   }
    public static int counter2 = 0;

    public static Singleton getInstance(){
       return singleton;
   }
}

 

按照从上到下的顺序进行初始化。

类主动使用时候,先准备,给类的静态变量赋初始值。

此时: 

   counter1 初始值 0 

   singleton 初始值 null

   counter2 初始值 0

接着调用静态方法 getInstance时候,赋初始值。

sigleton 会指向一个实例,而后执行私有构造方法。

而后执行到 public static int counter2 = 0时候,显示赋值0了。

 

总结:

 先准备

 再初始化: 根据类里面代码的顺序去执行的.真正的赋值(准备为其提供初始值,要不谈不上作++操做)

 

 

 

 

 画个图:

 

 

 关于类的实例化:

  为对象分配内存,即为new对象,在堆上面。

   为实例变量赋默认值、为实例变量赋正确的初始值都跟静态变量似的了。赋予默认值以后,再去赋予开发者指定的值。

 

类的加载:

  •   类的加载的最终产品是位于内充中的Class对象
  •   Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口

Class是反射的入口。像一面镜子同样。

 

有两种类型的类加载器:

  1.Java虚拟机自带的加载器

  • 根类加载器(BootStrap)
  • 扩展类加载器(Extension)
  • 系统(应用)类加载器(System)

2.用户自定义的类加载器

  • java.lang.ClassLoader的子类
  • 用户能够定制类的加载方式

 

类的加载: 

 类加载器并不须要等到某个类被“首次主动使用”时候再加载它

 注:

  •   JVM规范容许类加载器在预料某个类将要被使用时就预先加载它。若是在预先加载的过程当中遇到了.class文件确实或者存在错误,类加载器必须在程序首次主动使用该类时候才报告错误(LinkageaError错误)
  •   若是这个类一直没有被程序主动使用,那么类加载器就不会报告错误

 

类的验证:

  类被加载后,就进入链接阶段。链接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时的环境中去。

  

类的验证的内容:

  •   类文件的结构检查
  •   语义检查
  •   字节码验证
  •   二进制兼容性的验证

在准备阶段:

  

 

 

 

 初始化阶段:

 

 

 

 类的初始化步骤:

  •  假如这个类尚未被加载和链接,那就先进行加载和链接
  •  假如类存在直接父类,而且这个父类尚未被初始化,那就先初始直接父类
  •  假如类中存在初始化语句,那就依次执行这些初始化语句 

 

 

只有当程序访问的静态变量或静态方法确实在当前类或当前接口定义时,才能够认为是对类或接口的主动使用。

调用ClassLoader类的loadClass方法加载一个类,并非对类的主动使用,不会致使类的初始化。

 

 

 

 

除了以上虚拟机自带的加载器外,用户还能够定制本身的类加载器。Java提供了抽象类java.lang.ClassLoader,全部用户自定义的类加载器都应该继承ClassLoader类

 

 

引伸看下这个例子:

 public class MyTest {
    public static void main(String[] args) {
        System.out.println(MyChild.b);
    }
}

interface MyParent{
  public static int a = 5;
}
interface MyChild extends MyParent{
 public static final int b = 8;
}

分析:

MyTest类有main函数。会主动使用,先去加载。

接口和类实际上是不一样的,以下:

加载层面: 

若是是类的话,MyChild确定会被加载。若是是接口的话,不会被加载。

若是把b 修改成 Random(运行期才知道的值)。会将Parend 和 Child都加载. 很重要的一点是变量是编译器的仍是运行期才能肯定的

若是 parent和child都是final,test用到的常量会放入本身的常量池中,则不会对parent和child进行加载了。

若是把接口换作class,则存在加载,不加载的话必须是final的!

 

总结出了final关键字的区别小结:

  •   final修饰的变量,决定当前类是否加载。(static修饰的,不会这样)
  •   implement 实现的接口,不会加载

final修饰后,哪一个类去主动调用就将这个常量放入到本身类的常量池里面。

 

Remember:

 block 优先 构造函数执行,每次都执行。

 

证实初始化一个类时候,不会初始化他的接口:

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}

interface MyParent5 {
    public static Thread thread = new Thread(){
        {
            System.out.println("MyParent5 Thread ==========");
        }
    };
}

interface MyChild5 extends MyParent5 {
    public static int b = 6;
}
class C{
    {
        System.out.println("hello c{block}");
    }
    public C(){
        System.out.println("hello c(construct)");
    }
}

 

 

 若是将父子的interface 改为class 则会初始化父类

 当一个类被初始化时候,他所实现的类是不会被初始化的。

 

继续看下面例子:

public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}


interface MyGrandPa{
    public static Thread thread = new Thread(){
        {
            System.out.println("MyGrandPa Thread ==========");
        }
    };
}


interface MyParent5 extends MyGrandPa{
    public static Thread thread = new Thread(){
        {
            System.out.println("MyParent5 Thread ==========");
        }
    };
}

interface MyChild5 extends MyParent5 {
    public static int b = 6;
}
class C{
    {
        System.out.println("hello c{block}");
    }
    public C(){
        System.out.println("hello c(construct)");
    }
}

 

 

 

总结:

  1.  先看是不是finanl修饰,是的话,就不用加载别的类。前提是编译器的。
  2. 再看interface否。

 

 

类加载器的双亲委派机制:

  在双亲委派机制中,各个加载器按照父子关系造成了树形结构,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器。

 

 

 

 

 

 

 

若是有一个类加载器可以成功加载Test类,那么这个类加载器被称为定义类加载器,全部可以成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始化类加载器。(了解便可)

public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("java.lang.String");
        System.out.println(clazz.getClassLoader());

        Class<?> mClazz = Class.forName("com.jvm.t1.M");
        System.out.println(mClazz.getClassLoader());

    }
}
//位于工程的classPath目录地址下
class M{

}

 

 

 

 

 

以下例子:

package com.jvm.t1;

public class MyTest9 {
    static {
        System.out.println("MyTest9 static block");
    }

    public static void main(String[] args) {
        System.out.println(Child.b);
    }
}

class Parent{
    static int a = 3;
    static {
        System.out.println("parent static block");
    }
}

class Child extends Parent{
    static int b = 4;
    static {
        System.out.println("chile static block");
    }
}

 

 便于查看加载过程清晰:

 

输出结果:

 

 

 

 

看下面的例子:

public class MyTest10 {
 static {
     System.out.println("MyTest10 static block");
 }

    public static void main(String[] args) {
     //声明类型的使用,并非主动使用
        Parent2 parent2;
        System.out.println("-------");
        parent2 = new Parent2();
        System.out.println("---------");
        System.out.println(parent2.a);
        System.out.println("---------");
        System.out.println(Child2.b);

    }
}
class Parent2{
  static int a = 3;
  static {
      System.out.println("Parent2 static block");
  }
}

class Child2 extends Parent2{
    static int b = 4;
    static {
        System.out.println("Child2 static block");
    }
}

 

 

 使用child时候,parent已经被初始化了,只会初始化一次。

总结:

初始化一次就OK了。 

 

看下面例子:

class Parent3{
    static int a = 3;
    static {
        System.out.println("Parent3 static block");
   }
   static void doSomeThing(){
        System.out.println("do something");
    }
}
class Child3 extends Parent3{
    static {
        System.out.println("Child3 static block");
    }
}

public class MyTest11 {
    public static void main(String[] args) {
        //访问父类的。调用父类的Parent的(主动使用)
        System.out.println(Child3.a);
        //访问的父类的。调用父类的Parent的(主动使用)
        Child3.doSomeThing();
    }
}

 

 

 

总结: 

  • 虽然名字是Child3 可是没有对其主动使用。
  • 若是使用子类去访问父类定义的变量、方法,本质上都表示对于父类的主动使用!

 

 

 

 

看下面例子:

class CL{
    static {
        System.out.println("static block class CL");
    }
}

public class MyTest12 {
    public static void main(String[] args) throws ClassNotFoundException {
        //系统类加载器(应用类加载器)
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //指定加载的类
        //这个不会致使类的初始
        Class<?> clazz = classLoader.loadClass("com.jvm.t1.CL");
        System.out.println(clazz);
        System.out.println("-------");
        //类的初始化,反射致使类的初始化
        clazz = Class.forName("com.jvm.t1.CL");
        System.out.println(clazz);
    }
}

 

 

 

总结:

  • 调用classLoader.loadClass 不是对类的主动使用,不会致使初始化
  • 反射是对类的主动使用




关于双亲委派机制:
 

public class MyTest13 {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        while (null !=  systemClassLoader){
            systemClassLoader = systemClassLoader.getParent();
            System.out.println(systemClassLoader);
        }

    }
}

 

 

 结论:

 在HotSpot中,BootStrap ClassLoader使用null表示的.(启动类加载器)

  

看下面例子:

public class MyTest14 {
    public static void main(String[] args) {
        //获取上下文的类加载器。线程建立者提供的。(有默认值的)
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(contextClassLoader);

    }
}

 

 

类型是APPClassLoader,加载应用的类加载器(系统类加载器)。

 

看下面的例子:

public class MyTest14 {
    public static void main(String[] args) throws IOException {
        //获取上下文的类加载器。线程建立者提供的。(有默认值的)
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        //存在磁盘上的字节码(磁盘上的目录)
        String resourceName = "com/jvm/t1/MyTest13.class";
        //给定名字的全部资源(图片、音频等)
        Enumeration<URL> resources = contextClassLoader.getResources(resourceName);
       while (resources.hasMoreElements()){
           URL url = resources.nextElement();
           System.out.println(url);
       }

    }
}

 

 

 

 

获取ClassLoader的途径:

 

 

 咱们本身定义的类,APPClassLoader:

public class MyTest14 {
    public static void main(String[] args) throws IOException {
        Class<MyTest14> myTest14Class = MyTest14.class;
        System.out.println(myTest14Class.getClassLoader());
    }
}

 

 

  

 

public class MyTest14 {
    public static void main(String[] args) throws IOException {

        Class<String> stringClass = String.class;
        System.out.println(stringClass.getClassLoader());
    }
}

 

 

 String 这个类位于rt.jar

 

用户自定义的类加载器都直接或间接的从ClassLoader类继承下来。

数组类的Class对象并非由类加载器建立的,运行时因为Java虚拟机自动建立的。只有数组如此

 

public class MyTest15 {
    public static void main(String[] args) {
        String[] strings = new String[2];
        System.out.println(strings.getClass().getClassLoader());
        System.out.println("--------------");
        MyTest15[] myTest15s = new MyTest15[12];
        System.out.println(myTest15s.getClass().getClassLoader());
        System.out.println("--------------");
        int[] ins = new int[2];
        System.out.println(ins.getClass().getClassLoader());
    }
}

 

 

 

 

总结:

 

 

  • 根据里面的每一个元素的类型定义的!String、MyTest15。 
  • 虽然获取到了数组的类加载器,可是数组对应的Class对象并非ClassLoader加载的,是JVM动态建立的。
  • 原生类型,没有加载器。

 

 本身定义类加载器,看下面例子:

 

public class MyTest16 extends ClassLoader {
    private String classLoaderName = "";
    private String fileExtension = ".class";

    public MyTest16(String classLoaderName) {
        super(); // 将系统类加载器当作该类加载器的父类加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);  //显示指定该类的加载器的父类加载器
        this.classLoaderName = classLoaderName;
    }


    private byte[] loadClassData(String name) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        try {
            //注意win和linux
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            is = new FileInputStream(new File(name + this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch ;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            // 字节数组输出流转换成字节数组
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
                baos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);
        //返回Class对象
        return this.defineClass(className, data, 0 , data.length);
    }

    public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //内部底层的api已经被咱们重写了
        Class<?> clazz = classLoader.loadClass("com.jvm.t1.MyTest15");
        Object object = clazz.newInstance();
        System.out.println(object);
    }

    @Override
    public String toString() {
        return "[" + this.classLoaderName + "]";
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16 loader1 = new MyTest16("loader1");
        test(loader1);
    }
}

 

 

 

 

其实此时咱们定义的 findClass是没有被调用的!觉得双亲委派机制,让父类去加载了!

 

看下面例子:

public class MyTest16 extends ClassLoader {
    private String classLoaderName = "";
    private String fileExtension = ".class";
    private String path;

    public MyTest16(String classLoaderName) {
        super(); // 将系统类加载器当作该类加载器的父类加载器
        this.classLoaderName = classLoaderName;
    }

    public void setPath(String path){
        this.path = path;
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);  //显示指定该类的加载器的父类加载器
        this.classLoaderName = classLoaderName;
    }


    private byte[] loadClassData(String className) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        className.replace(",","/");


        try {
            //注意win和linux
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            //指定磁盘全路径
            is = new FileInputStream(this.path + new File(className + this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch ;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            // 字节数组输出流转换成字节数组
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
                baos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {

        System.out.println("findClass invoked:" + className);
        System.out.println("class loader name" + this.classLoaderName);

        byte[] data = this.loadClassData(className);
        //返回Class对象
        return this.defineClass(className, data, 0 , data.length);
    }

    @Override
    public String toString() {
        return "[" + this.classLoaderName + "]";
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        // 建立自定义类加载器 名字“loader1” 父类加载器是系统类加载器
        MyTest16 loader1 = new MyTest16("loader1");
        //此路径为classPath,故 findClass方法不会被调用执行! 若是换个路径,不是classPath就会去执行了!
        loader1.setPath("D:\\eclipse_pj\\dianshang\\jvmTest\\out\\production\\jvmTest\\");
        Class<?> clazz = loader1.loadClass("com.jvm.t1.MyTest15");
        System.out.println("class:"+ clazz.hashCode());
        Object object = clazz.newInstance();
        System.out.println(object);

    }
}

 

 委托给父类,父类去classPath目录下面找,找到了加载之。

 

关于命名空间:

  • 每一个类加载器都有本身的命名空间,命名空间由该加载器及全部父加载器所加载的类组成
  • 同一个命名 空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不一样的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类 

 

关于类的卸载:

  •  当MySample类被加载、链接和初始化后,他的声明周期就开始了。当表明MySample类的Class对象再也不被引用,即不可触及时,Class对象就会结束声明周期,MySample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
  •  一个类什么时候结束生命周期,取决于表明他的Class对象什么时候结束生命周期。
  • 由用户自定义的类加载器所加载的类是能够被卸载的。  

 

 

 

  加载  <----> 卸载

 

 

 看下面的例子:

public class MySample {
    MySample(){
        System.out.println("MySample is loaded by"+ this.getClass().getClassLoader());
        MyCat myCat = new MyCat();

    }
}
public class MyCat {
    public MyCat() {
        System.out.println("MyCat is loaded by" + this.getClass().getClassLoader());
    }
}
public class MyTest17 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16 loader1 = new MyTest16("loader1");
        //要加载的类
        Class<?> clazz = loader1.loadClass("com.jvm.t1.MySample");
        System.out.println("clazz"+ clazz.hashCode());

        //若是注释掉改行,那么并不会实例化MySample对象,即MySample构造方法不会被调用
        // 所以不会实例化MyCat对象,即没有对MyCat进行主动使用,这里就不会加载MyCat class
        Object object = clazz.newInstance();// new instance 没有任何参数。调用无参构造方法

    }
}

 

 

 关于命名空间的说明: 

  1.   子加载器加载的类,可以访问父加载器加载的类。  
  2.   父加载器加载的类,不能访问子加载器加载的类。

 

public class Test3 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyTest16 loader1 = new MyTest16("loader1`");
        MyTest16 loader2 = new MyTest16("loader2`");
        
        loader1.setPath("/User/test/");
        loader1.setPath("/User/test/");

        //加载相同的类。(都委托为appClassLoader了)
        Class<?> clazz1 = loader1.loadClass("com.jvm.test.Test");
        //加载过了
        Class<?> clazz2 = loader2.loadClass("com.jvm.test.Test");

        // 都是app加载的,双亲委派
        System.out.println(clazz1 == clazz2);

        Object o1 = clazz1.newInstance();
        Object o2 = clazz2.newInstance();

        Method setMyPerson = clazz1.getMethod("setMyPerson", Object.class);
        //执行o1的方法,参数是o2
        setMyPerson.invoke(o1, o2);

    }
}

 

状况1.若是 class字节码在classPath,返回 true。 执行成功。(读者自行考虑,提示双亲委派)

状况2.若是 class字节码只在:"/User/test/" 。返回false。执行报错。

       缘由

  •    命名空间。 两个loader不存在父子关系,是平行的。在jvm中存在两个命名空间。
  •    不一样命名空间的类不可见,引用不到就报错。(子加载器的命名空间包含全部父加载器的命名空间,子可看到父类加载的全部类。)

 

 

双亲委派的好处:

  1.    能够确保Java核心库的类型安全,全部的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中。
  2.    若是这个加载过程是由Java应用本身的类加载器所完成的,那么极可能就会在JVM汇总存在多个版本的java.lang.Object类。并且这些类之间是不兼容,相互不可见的(命名空间)。
  3.    借助双亲委派机制,java核心类库中的类的加载工做都是启动类加载器统一完成的。确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。Java核心类库不会被自定义的替代。启动类去加载之。
  4.    不一样的类加载器能够为相同名称(binary name)的类建立额外的命名空间,相同名称的类能够并存在Java虚拟机中,只须要不用的类加载器(包括没有父子关系、不一样类加载器)来加载他们便可。不一样类加载器所加载的类是不兼容的。这就至关于在Java虚拟机内部建立了一个又一个相互隔离的       Java类空间,这类技术在不少框架中都获得了实际应用。

 

知识总结: 

  1. 关于扩展类加载器:须要作成jar包,再放到指定目录下。
  2. 在运行期,一个Java类是由该类的彻底限定名(binary name, 二进制名)和用于加载该类的定义列类加载器(defing loader)所共同决定的。若是一样名字(即相同的彻底限定名)的类是由两个不一样的加载器所加载,那么这些类就是不一样的。即使 .class文件的字节码彻底同样,而且从相同的位置加载亦如此。
  3. 在Oracle的hotSopt实现中,系统属性sun.boot.class.path若是修改错了,则运行会报错,提示: Error occurred during Initialization of VM
  4. 内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其余的Java平台类,当JVM启动时候,一块特殊的机器码会运行,他会扩展类加载器与系统类加载器,这块特殊的机器码叫作启动类加载器(BootStrap)。
  5. 启动类加载器并非Java类,而其余的加载器都是Java类。启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。
  6. 全部类加载器(除启动类加载器)都被实现为Java类。不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程可以顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。
  7. 启动类加载器还会负责加载供JRE正常运行所须要的基本组件,这包括java.util与java.lang包中的类等等。

 

简单看下:

public class test4 {
    public static void main(String[] args) {
        System.out.println(ClassLoader.class.getClassLoader());
        //扩展类
        System.out.println(Launcher.class.getClassLoader());
    }
}

 

 

 

能够本身作系统类加载器。略。须要控制台指令显示指定

经过改变属性,提示:

System.getProperty("java.system.class.loader")

 

 

引伸:

 getSystemClassLoader()

  •   返回用于委托的系统类加载器,
  •   建立的ClassLoader默认的爸爸(也是用于启动应用的类加载器)。
  •   建立类加载器,而后设置为调用这个方法的线程的上下文类加载器。(Contex Class Loader)。应用框架,服务器大量使用的!
  •   默认的系统类加载器,与此类实现相关的实例。 
  •   java.system.class.loader所指定的类,是被默认的系统类加载器加载。必需要定义public的构造方法,传递自定义类加载器的爸爸。

 

OpenJDK是JDK开源版本。

 

解析Class.forName:

 其实:Class.forName("Foo") 等价于 Class.forName("Foo",true, this.getClass().getClassLoader() ) 

 

关于线程上下文的类加载器: Thread.currentThread().setContextClassLoader(sys)

做用就是改变双亲委派模型在某些场景下不适用的状况。

看下面例子:

public class MyTest24 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Thread.class.getClassLoader());  // 路径位置致使的
    }
}

 

 

 

当前类加载器(Current ClassLoader)

每一个类都会使用本身的类加载器(即加载自身的类加载器)去加载其它类(指的是所依赖的类):

  若是ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY还没有被加载)

  

线程上下文类加载器:

  •  线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader  与 setContextClassLoader(ClassLoader cl) 分别用来获取和设置上下文类加载器
  • 若是没有经过setContextClassLoader进行设置的话。线程将继承其父线程的上下文类加载器。
  • Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码能够经过该类加载器来加载类与资源。

 线程上下文类加载器的重要性:

 应用场景: 

 SPI(Service Provider Interface)

 父ClassLoader可使用当前线程Thread.currentThread().getContexClassLoader() 所指定的ClassLoader加载的类,这就改变了父ClassLoader不能使用子ClassLoader或是其余没有直接父子关系的ClassLoader加载的类的状况。

 线程上下文类加载器就是当前线程的Current ClassLoader

在双亲委派模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。可是对于SPI来讲,有些接口是Java类核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不一样的jar包(厂商提供。

Java的启动类加载器是不会加  载其余来源你的Jar包 ,这样的传统的双亲委派模型就没法知足SPI的要求。而经过给当前线程设置上下文类加载器,就能够由设置的上下文类加载器来实现对于接口实现类的加载。

 总结:接口是启动类加载器加载的, 实现类应用类加载器加载,经过给当前的线程设置上下文类加载器,实现对于接口实现类的加载,打破了双亲委派模型如今。(框架开发,底层开发会用到)

(JDK中没有对于JDBC的任何实现,除了传统的接口以外,具体实现都是由厂商趋势线的,好比MySQL。)

 

 

 

看下面代码:

public class MyTest25 implements Runnable {

    private Thread thread;

    public MyTest25(){
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        // 获取到上下文类加载器
        ClassLoader classLoader = this.thread.getContextClassLoader();
        this.thread.setContextClassLoader(classLoader);
        System.out.println("Class:"+classLoader.getClass());
        System.out.println("Class:"+classLoader.getParent().getClass());
    }

    public static void main(String[] args) {
        MyTest25 myTest25 = new MyTest25();
    }
}

 

没有设置,因此线程将继承父线程的上下文类加载器。

 

 

线程上下文类加载器的通常使用模式(获取 - 使用 - 还原)

注意:若是一个类由A加载器加载,那么这个类的依赖也是由相同的类加载器加载的(若是该依赖以前没有被加载过的话)

ContextClassLoader的做用就是为了破坏Java的类加载委托机制  

当高层提供了统一的接口让底层去实现,同时又要在高层加载(或者实例化)低层的类时候,就必需要经过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类

 

看下面例子:

public class MyTest26 {

    public static void main(String[] args) {

        //设置下
       // Thread.currentThread().setContextClassLoader(MyTest26.class.getClassLoader());

        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = loader.iterator();

        while (iterator.hasNext()){
            Driver driver = iterator.next();
            System.out.println("driver" + driver.getClass() + ", loader" + driver.getClass().getClassLoader() );
        }

        System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
    }
}

 

 

关于字节码:

 

对于能编译成class字节码的代码,class的规范,合法性保证好了就OK了。

对于Idea编译器,是很是熟悉class字节码了,能够为所欲为的反编译。

对于java代码:

public class MyTest1 {

    private int a = 1;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

idea看字节码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.jvm.t1.t2;

public class MyTest1 {
    private int a = 1;

    public MyTest1() {
    }

    public int getA() {
        return this.a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

经过反编译指令:

 

 看到三个方法:其中一个是默认的构造方法。

 

详细查看字节码信息:输入

javap -c com.jvm.t1.t2.MyTest1

Compiled from "MyTest1.java"
public class com.jvm.t1.t2.MyTest1 {
//构造方法
public com.jvm.t1.t2.MyTest1();
//下面都是助记符 Code:
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return public int getA(); Code: 0: aload_0 1: getfield #2 // Field a:I 4: ireturn public void setA(int); Code: 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return }

 

看下面指令:

javap -verbose com.jvm.t1.t2.MyTest1

Classfile /D:/eclipse_pj/dianshang/jvmTest/out/production/jvmTest/com/jvm/t1/t2/MyTest1.class
  Last modified 2019-10-20; size 473 bytes
  MD5 checksum c5b1387c6f6c79b14c1b6a5438da3b29
  Compiled from "MyTest1.java"
public class com.jvm.t1.t2.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

// 常量池: 占据至关大的比重 Constant pool: #
1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/jvm/t1/t2/MyTest1.a:I #3 = Class #22 // com/jvm/t1/t2/MyTest1 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/jvm/t1/t2/MyTest1; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest1.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/jvm/t1/t2/MyTest1 #23 = Utf8 java/lang/Object
//方法的描述 {
public com.jvm.t1.t2.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 5: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/jvm/t1/t2/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jvm/t1/t2/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 12: 0 line 13: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/jvm/t1/t2/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java"

使用如上的这个命令分析字节码时候,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。

备注:

 魔数: 全部的.class字节码文件的前4个字节都是魔数,魔数值为固定值: 0xCAFEBABE。

 魔数以后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major  version(主版本号)。

 

常量池(constant pool): 紧接着主板号以后就是常量池入口。一个Java类中定义的不少信息都是由常量池来维护和描述的。常量池在整个字节码文件中占的比重最大,里面的信息会被不少地方引用到。至关于把常量集中在一个地方,其余地方用到时候去引用之。经过Index找到常量池中特定的常量。能够将常量池看作是class文件的资源仓库。好比:Java类总定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用量。

注意:常量池!里面存放的不必定都是常量。也有变量信息。

  • 字面量如文本字符串,Java中声明为final 的常量值等,而符号引用,好比说类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。

常量池的整体结构: Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同组成。常量池数量紧跟在主版本后面,占据2个字节;常量池数组则紧跟在常量池数量以后。常量池数组和通常的数组不一样的是,常量池数组中不一样的元素的类型,结构都是不一样的,长度固然也就不一样;可是每一种元素的数都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时候,会根据这个u1类型来获取元素的具体类型。值得注意的是:常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不使用)。目的是知足某些常量池索引值的数据在特定状况下须要表达 【不引用任何一个常量池】的含义。根本缘由在于,索引为0也是一个常量(保留常量)。只不过它不位于常量表中,这个常量就对应null值。因此常量池的索引从1而非从0开始。

以下,从1开始:

 

常量池中数据类型:

 

 

 

 在JVM规范中,每一个变量/字段都有描述信息,描述信息主要的做用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和表明无返回值的void类型都

用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积。对于基本数据类型,JVM都只使用一个大写字母来表示,以下所示:

B ---> byte   C --> char  D ---> doube F ---> float  I --> int  J --long S  --> short  Z --> boolean  V --> void

L --->对象类型 ,如: L java/lang/String

 

对于数组类型来讲,每个维度使用一个前置的 [ 来表示,如 int[ ]  被记录为 [I , String[][] 被记录为[[ Ljava/lang/String

用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组以内,如方法:

get getName (int id, String name)描述为:

 

常量池里面存储的各类 index  和 信息

 

Java字节码总体结构:

 

 

 

 

 

 完整Java字节码接口例子:

 

 

 

Access_Flag访问标志

访问标志信息包括该Class文件是类仍是接口,是否被定义成public,是不是abstract,若是是类,是否被声明称final。

 

 

 

 

 字段表集合:

  字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量(静态变量)以及实例变量(非静态变量),可是不包括方法内部声明的局部变量。

 

 

  

 

 

一个field_info包含的信息:

 

 

 

  方法表:

methods_count: u2

 

 

 

前三个字段和field_info同样

 

 

 方法中每一个属性都是一个attribute_info结构

 

 

 

JVM预约义了部分attribute,可是编译器本身也能够实现本身的attribute写入class文件里,供运行使用

不一样的attribute经过attribute_name_index来区分

 

 

 Code结构

Code attribute的做用是保存该方法的结构,如所对应的字节码

 

 

  • attribute_length 表示attribute所包含的字节数,不包含attribute_name_index 和 attribute_length字段
  • max_stack 表示这个方法运行的任什么时候刻所能达到的操做数栈的最大深度
  • max_locals表示方法执行期间建立的局部变量的数目,包含用来表示传入的参数的局部变量 
  • code_length表示该方法所包含的字节码的字节数以及具体的指令码
  • 具体字节码便是该方法被调用时,虚拟机所执行的字节码
  • exception_table,这里存放的是处理异常的信息
  • 每一个exception_table表项由start_pc, end_pc, handler_pc, catch_type 组成
  • start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc, 不包含end_pc)的指令抛出的异常会由这个表项来处理。
  • handeler_pc 表示处理异常的代码的开始处,catch_type 表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理全部异常。
  • 方法中的每一个属性都是一个attribute_info结构

 code attribute的做用是保存该方法的结构,如所对应的字节码

 

 

 

 推荐你们使用:  jclasslib 阅读字节码信息

 

 Java中,每个方法都是能够访问this(表示对当前对象的引用),

 字节码角度,若是方法自己是个非静态(实例)的,this能够做为方法的第一个方法,能够隐式的传递进来。会使得每一个实例方法均可以访问this。至少会有个局部变量,这个局部变量就是this。

 

对于某各种Test,中的静态方法 使用了synchronized 关键字,至关于给这个Test对应的Class对象加锁了。

 

关于this关键字: 

 

Java编译器在编译时候,把对this方法的访问,转变成了对普通参数的访问。在Java中,每个非静态实例的方法的局部变量中,至少会存在一个指向当前对象的局部变量。即:

对于Java类中的每个实例方法(非static方法),其中在编译后所生成的字节码当中,方法参数的数量总会比源代码汇总方法的参数多一个(this),它位于方法的第一个参数位置处;这样咱们就能够在Java实例方法中使用this访问当前对象的属性以及其余方法。这个操做是在编译期间完成的,由javac编译器,在编译时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。因此,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。

 

 

关于异常处理:

 

 Code结构:

 attribute_length表示attribute锁包含的字节数,不包含attribute_name_index和attribute_length字段

 max_stack表示这个方法运行的任什么时候刻所能达到的操做数栈的最大深度

 max_locals表示方法执行期间所建立的局部变量的数目,包含用来表示传入的参数的局部变量

 code_lenght表示该方法所含的字节码的字节数以及具体的指令码

 具体字节码便是该方法被调用时,虚拟机所执行的字节码

 exception_table, 这里存放的是处理异常的消息

 每一个exception_tabel 表项由start_pc, end_pc , handler_pc ,catch_type 组成

 start_pc 和 end_pc 表示在code 数组中的从start_pc都end_pc处(包含start_pc, 不包含end_pc)的指令抛出的异常会由这个表项来处理

 handler_pc表示处理异常的代码的开始处。catch_type 表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理全部的异常。

 

Java字节码对于异常的处理方式:

 1.  统一采用异常表的方式来对异常进行处理

 2. 老版本中,并非使用遗产表的方式来对异常进行处理的,而是采用特定的指令方式(了解)

 3. 当异常处理存在finally语句块时,现代化的JVM采起的方式将finally语句块的字节码拼接到每个catch块后面,换句话说,程序存在多少个catch块,就会在每个catch块后面重复多少个finally语句块的字节码。

 

栈帧,是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。

栈帧, 自己是一种数据结构,封装了风阀的局部变量表,动态连接信息,方法的返回地址操做数栈等信息。

Java中,对于不一样的类之间的关系,编译期间,地址关系其实是不知道的。何时知道?

  1. 类加载时候

  2. 真正调用时候,才知道目标方法地址。

基于以上两点,引伸出了符号引用和直接引用。

有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫作静态解析;另一些符号引用则是在每次运行期转为直接引用,这种转换叫作动态连接,这体现为Java的多态性

好比父类因用户指向子类实现。

   Aninaml a = new Cat();
    a.run();
    a = new  Fish();
    a.run
    

编译时候,a都是Animal.  字节码角度,都是Animal

运行时候,每次运行期,都会进行一次直接引用的转换。

 

 

JVM 方法调用的字节码指令:

 1. invokeinterface:调用接口中的方法,其实是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法(一个接口,n个实现类)。

 2. invokestatic: 调用静态方法

 3.invokespecial: 调用本身的私有方法,构造方法(<init>) 以及父类的方法

 4. invokevirtual: 调用虚方法,运行期动态查找的过程。

 5. invokedynamic: 动态调用方法。

 

静态解析的四种状况:

  1. 静态方法

  2.父类方法

  3. 构造方法

 4. 私有方法(公有方法能够被重写或者复写,多态的可能。私有方法在加载时候就可以被肯定了)

以上四种称之为: 非虚方法。他们是在类加载阶段就能够将符号引用转换为直接引用的。

public class MyTest5 {
     public void test(GrandPa grandPa){
         System.out.println("grandPa");
     }

    public void test(Father father){
        System.out.println("father");
    }

    public void test(Son son){
        System.out.println("son");
    }

    public static void main(String[] args) {
         //都是GrandPal类型的
        GrandPa father = new Father();
        GrandPa son = new Son();

        MyTest5 myTest5 = new MyTest5();

        myTest5.test(father);
        myTest5.test(son);
    }



}

class GrandPa{

}
class Father extends  GrandPa{

}
class Son extends Father{

 

以上代码 ,  father的静态类型是Grandpa,而father的实际类型(真正指向的类型)是Father  

 

变量自己的静态类型是不会被改变的, GrandPa father

结论:

 变量的静态类型是不会发生变化的,而变量的实际类型是能够发生变化的(多态的一种体现)。实际类型是在运行期方可肯定。

 

以上,方法的重载,参数类型不同。方法重载是一种纯粹的静态行为。

因此,当使用myTest5调用方法的时候, 是根据类型进行匹配。寻找类型是 GrandPa的。编译器就能够彻底肯定的。

 

public class MyTest6 {
    public static void main(String[] args) {
        Fruit apple = new Apple();
        Fruit orange = new Orange();

        apple.test();
        orange.test();

        apple = new Orange();
        apple.test();
    }
}

class Fruit{
    public void test(){
        System.out.println("fruit");
    }
}
class Apple extends Fruit{
    @Override
    public void test() {
        System.out.println("apple");
    }
}
class Orange extends Fruit{
    @Override
    public void test() {
        System.out.println("orange");
    }
}

 

 

引伸:

Java中,new起到了三个做用:

 1. 在堆上开辟空间

 2. 执行构造方法

 3. 将构造方法执行后返回的堆上的此引用值返回

 

方法的动态分派:

 方法的动态分派涉及到一个重要概念:方法接收者

 invokevirtual字节码指令的多态查找流程

 方法重载和方法重写,咱们能够获得这个方法重载是静态的,是编译器行为,方法重写是动态的,是运行期行为。

 

public class MyTest7 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Dog dog = new Dog();
        animal.test("hello");
        dog.test(new Date( ));
    }
}

class Animal{
    public void test(String str){
        System.out.println("animal str");
    }
    public void test(Date date){
        System.out.println("animal date");
    }
}

class Dog extends Animal{

    @Override
    public void test(String  str) {
        System.out.println("dog str");
    }

    @Override
    public void test(Date date) {
        System.out.println("dog date");
    }
}

 

 

 

针对于方法调用动态分派的过程,虚拟机会在类的方法区创建一个虚方法表的数据结构(virtual method table,简称 vtable)

 

现代JVM在执行Java代码的时候,一般会将解释执行与编译执行两者结合起来执行。

所谓解释执行:经过解释器读取字节码,遇到相应的指令就去执行该指令

所谓编译执行:经过及时编译器(Just In Time, JIT)将字节码转为本地机器码来执行,现代JVM会根据代码热点来生成相应的本地机器码。

 

基于栈的指令集合基于寄存器的指令集之间的关系:

 1. JVM执行指令时所采起的的方式是基于栈的指令集

 2. 基于栈的指令集的主要操做: 入栈、出栈

 3. 基于栈的指令集的优点在于他能够在不一样平台间一直,而基于寄存器的指令集是与硬件架构密切关联的,没法作到可移植。

 4.  基于栈的指令集的缺点: 完成相同的操做,执行数量一般要比基于寄存器的指令集数量多 。基于栈的指令集是在内存中操做的,而基于寄存器的指令集是直接由CPU执行的,它是在高速缓冲区中进行的,速度要快不少。虽然虚拟机能够采用一些优化手段,但整体      来讲,基于栈的指令集的执行速度要慢一些。

 

注意:

栈 配合 局部变量表使用,局部变量表的0位置是this 

 

对应动态代理,主要有一个类(proxy)和一个接口(InvocationHandler)去搞定。

接口:

public interface Subject {
    void request();
}

实现类:

public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("reslsubjct");
    }
}

代理类:

/**
 * 动态代理文件
 */
public class DynamicSubject implements InvocationHandler {

    private Object sub;

    public  DynamicSubject(Object obj){
        this.sub = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before calling"+ method);

        method.invoke(this.sub, args);

        System.out.println("after calling"+ method);

        return null;

    }
}

测试:

public class Client {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        DynamicSubject dynamicSubject = new DynamicSubject(realSubject);
        Class<?> clazz = realSubject.getClass();
        //获取 Class对象是为了,动态代理须要类加载器。
        Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), dynamicSubject);
        subject.request();
        System.out.println(subject.getClass());
    }
}

程序运行期动态生成的:

 

首先建立代理类,而后建立代理类的实例对象。 

 

 

对象分为两部份内容: 

1, 对象自己拥有的那些数据(位于堆)

2, 对象所属的类型(元数据信息,MetaData) 全部实例对应一个Class对象。位于方法区(存储的一部分对象的类型数据信息)

 

方案一:

对象引用的是一个指向对象实例的指针,另一个指针指向方法区中的类型数据

方案二:(HotSpot的方案)

对象引用的是对象自己,和一个指向方法区汇总的类型数据指针  (对象实例数据、方法区)

 

 两种方案的差异L

堆发生垃圾回收频率很高,对于垃圾回收算法来讲,有几种会涉及到对象移动(压缩):为了保证区域连续的地方增大,移动之

 

 

方案一:对象一旦移动了,指针值会发生变化!随着每次垃圾回收会变化。

方案二:指针不会随之变化。

 

 JVM内存划分:

虚拟机栈

程序计数器

本地方法栈:主要用于处理本地方法

堆: JVM管理的最大一块内存空间

线程共享的区域,主要存储元信息。从JDK1.8开始,完全废弃永久代。使用元空间(meta space)

运行时常量池(方法区的一部分): 方法区的一部份内容。编译后的字节码的符号引用等等。加载完后,放入到方法区的运行时常量池。

直接内存: Direct Memory。 与Java NIO密切相关,JVM经过堆上的DirectByteBuffer来直接操做内存。

 

现代几乎全部的垃圾收集器都是采用的分代收集算法,因此堆空间也基于这一点进行了相应的划分。

 

Java对象的建立:

 new

 反射

 克隆

 反序列化

 

new关键字建立对象的3个步骤:

 1, 在堆内存中建立出对象的实例

 2, 为对象成员变量赋初始值(指的是,实例变量,区别静态变量)

 3, 将对象的引用返回。 

 

虚拟机干的活儿: 检查指令的参数new指令建立一个对象,指令参数是否是能在常量池中定位成一个类的符号引用。查看这个类是否是已经加载、连接、初始化了。

指针碰撞: 前提是堆中的空间经过一个指针进行分割,一侧是已经被占用的空间,另外一侧是未被占用的空间。

空闲列表:(前提是堆内存空间中已被使用与未被使用的空间交织在一块儿的。这时,虚拟机就须要经过一个列表来记录那些空间是能够用的,哪些空间是已被使用的,接下来找出能够容纳下新建立对象的且未被使用的空间,在此空间存放该对象,同时还要修改列表的记录)

 

一个对象包含三部分布局:

  1.对象的头, 

  2.实例数据(class中定义的成员变量) 

  3.对齐填充

 

永久代属于与堆链接的一个空间,对于永久代处理是比较麻烦的。

元空间,使用的操做系统的本地内存。能够不连续的。元空间里还有元空间虚拟机,管理元空间的内存的分配和回收状况。 初始大小21M,随着对于内存占用,会进行垃圾回收,甚至内存扩展,能够扩展到内存大小的最大值。

存放一个类的元数据信息,在框架中,用到运行期动态生成类的手段。动态建立出来的类,元信息放在元空间。

 

元空间参数: -XX:MaxMetaspaceSize=200M

在Java虚拟机(如下简称JVM)中,类包含其对应的元数据,好比类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已肯定的符号引用和虚方法表。

在过去(当自定义类加载器使用不广泛的时候,几乎不动态搭理),类几乎是“静态的”而且不多被卸载和回收,所以类也能够被当作“永久的”。另外因为类做为JVM实现的一部分,它们不禁程序来建立,由于它们也被认为是“非堆”的内存。

在JDK8以前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫作永久代的区域。永久代一段连续的内存空间,咱们在JVM启动以前能够经过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。可是有一个明显的问题,因为咱们能够经过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。

备注:在JDK7以前的HotSpot虚拟机中,归入字符串常量池的字符串被存储在永久代中,所以致使了一系列的性能问题和内存溢出错误。想要了解这些永久代移除这些字符串的信息,请访问这里查看。

随着Java8的到来,咱们再也见不到永久代了。可是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域就是咱们要提到的元空间。

这项改动是颇有必要的,由于对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。而且为永久代设置空间大小也是很难肯定的,由于这其中有不少影响因素,好比类的总数,常量池的大小和方法数量等。

同时,HotSpot虚拟机的每种类型的垃圾回收器都须要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不只实现了对元空间的无缝管理,还能够简化Full GC以及对之后的并发隔离类元数据等方面进行优化。

 

  

移除永久代的影响

因为类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。所以,咱们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户能够为元空间设置一个可用空间最大值,若是不进行设置,JVM 会自动根据类的元数据大小动态增长元空间的容量。

注意:永久代的移除并不表明自定义的类加载器泄露问题就解决了。所以,你还必须监控你的内存消耗状况,由于一旦发生泄漏,会占用你的大量本地内存,而且还可能致使交换区交换更加糟糕。

元空间内存管理

元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据咱们须要不一样的垃圾回收器进行处理,如今只须要执行元空间虚拟机的 C++ 代码便可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,于是不会被回收掉。

咱们从行文到如今提到的元空间稍微有点不严谨。准确的来讲,每个类加载器的存储区域都称做一个元空间,全部的元空间合在一块儿就是咱们一直说的元空间。当一个类加载器被垃圾回收器标记为再也不存活,其对应的元空间会被回收。在元空间的回收过程当中没有重定位和压缩等操做。可是元空间内的元数据会进行扫描来肯定 Java 引用。

元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个全局的空闲组块列表。当一个类加载器须要组块时,它就会从这个全局的组块列表中获取并维持一个本身的组块列表。当一个类加载器再也不存活,那么其持有的组块将会被释放,并返回给全局组块列表。类加载器持有的组块又会被分红多个块,每个块存储一个单元的元信息。组块中的块是线性分配(指针碰撞分配形式)。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式链接,一旦某个虚拟内存映射区域清空,这部份内存就会返回给操做系统。

Java永久代去哪儿了

上图展现的是虚拟内存映射区域如何进行元组块的分配。类加载器 1 和 3 代表使用了反射或者为匿名类加载器,他们使用了特定大小组块。 而类加载器 2 和 4 根据其内部条目的数量使用小型或者中型的组块。

 参考:https://www.infoq.cn/article/Java-PERMGEN-Removed

 

命令:jstat -gc 进程号  打印元空间信息

           jmap -clstats PID  打印类加载器数据

          jcmd PID GC.class_stats  诊断命令

 

jcmd 是从jdk1.7开始增长的命令

 1. jcmd pid VM.flag:查看JVM启动参数

 2. jcmd pid help: 列出当前运行的Java进行能够执行的操做

 3. jcmd pid help JFR.dump: 查看具体命令的选项

 4. jcmd pid PerfCounter.print: 查看JVM性能相关参数

 5. jcmd pid VM.uptime:查看JVM的启动时长

 6. jcmd pid GC.class_histogram 查看系统中类的统计信息

 7. jcmd pid Thread.print: 查看线程堆栈信息

 8. jcmd pid GC.heap_dump filename: 导出heap dump文件,导出的文件能够经过jvisualvm查看

 9. jcmd pid VM.system_properties: 查看JVM的属性信息

10. jcmd pid VM.version: 查看目标JVM进程的版本信息

11. jcmd pid VM.command_line:查看JVM启动的命令行参数信息

 

jstack: 能够查看或是导出Java应用程序中栈线程的堆栈信息

jmc: java  Mission Control

 

  

补充:

 针对于犯法调用动态分派的过程,虚拟机会在类的方法区创建一个虚方法表的数据结构(virtual method table, vtable)

 针对于invokeinterface指令来讲,迅疾会创建一个叫接口方法表的数据结构(interface method table, itable) 

 

JVM运行时数据区:

 

 

 

 

程序计数器

本地方法栈

Java虚拟机栈(JVM Stack)

  •     Java虚拟机栈描述的是Java方法的执行模型: 每一个方法执行的时候都会建立一个帧(Frame)栈用于存放局部变量表,操做栈,动态连接,方法出口等信息。一个方法的执行过程,就是这个方法对于帧栈的入栈出栈过程。
  •    线程隔离

堆 

  •   堆里存放的是对象的实例
  •   是Java虚拟机管理内存中最大的一块
  •   GC主要的工做区域,为了高效的GC,会把堆细分红更多的子区域
  •  线程共享

方法区:

  •   存方法每一个Class的结构信息,包括常量池,字段描述,方法描述
  •  GC的非主要工做区域

  

看下面例子:

  

  public void method(){
        Object obj = new Object();
    }

 

生成了两部份内存区域:

 1.obj这个引用变量,由于是方法内的变量,放到JVM Stack里面

 2. 真正Object class的实例对象,放到Heap里面

 

上述的new语句一共消耗12个byte。JVM规定引用占4个byte(JVM Stack),而空对象是8个byte(在Heap)

方法结束后,对应Stack中的变量立刻回收,可是Heap中的对象要等GC来回收

相关文章
相关标签/搜索