深入理解JVM虚拟机

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

 

关于类的加载:

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

    

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

  在如下几种情况下,Java虚拟机将会结束生命周期:

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

 

类的加载、连接与初始化:

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

连接: 

  • 验证: 确保被加载类的正确性。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 对象了。

 

总体流程:

深入理解JVM虚拟机_第1张图片

 

 

 

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 可以执行,同时初始化子类时候,父类会主动使用。所有的父类都会被初始化

深入理解JVM虚拟机_第2张图片

 

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

 

总结: 

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

 

引申: -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来。可以看到类的加载情况。

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

 

总结设置方式:

所有JVM参数都是: -XX: 开头  

  类似于Boolean类型的开关:

        -XX:+

        -XX: -

  赋值:   

     -XX:

 

 

 关于常量:

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];
        //看看是啥
        Classextends MyParent4[]> aClass = myParent4s.getClass();
        System.out.println(aClass);
    }
}
class MyParent4{
    static {
        System.out.println("MyParent4 --> run");
    }
}

深入理解JVM虚拟机_第3张图片

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");
    }
}

深入理解JVM虚拟机_第4张图片

 

 

 

助记符:

  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;
   }
}

深入理解JVM虚拟机_第5张图片

 

 

分析: 先赋值: 默认的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了。

 

总结:

 先准备

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

 

 深入理解JVM虚拟机_第6张图片

 

 

 画个图:

深入理解JVM虚拟机_第7张图片

 

 

 关于类的实例化:

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

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

 

类的加载:

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

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

 

有两种类型的类加载器:

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

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

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

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

 

类的加载: 

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

 注:

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

 

类的验证:

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

  

类的验证的内容:

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

在准备阶段:

  深入理解JVM虚拟机_第8张图片

 

 深入理解JVM虚拟机_第9张图片

 

 初始化阶段:

深入理解JVM虚拟机_第10张图片

 

 深入理解JVM虚拟机_第11张图片

 

 类的初始化步骤:

  •  假如这个类还没有被加载和连接,那就先进行加载和连接
  •  假如类存在直接父类,并且这个父类还没有被初始化,那就先初始直接父类
  •  假如类中存在初始化语句,那就依次执行这些初始化语句 

深入理解JVM虚拟机_第12张图片

 

 

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

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

深入理解JVM虚拟机_第13张图片

 

 深入理解JVM虚拟机_第14张图片

 

 

除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类

 深入理解JVM虚拟机_第15张图片

 

你可能感兴趣的:(深入理解JVM虚拟机)