JVM系列教程:1、类的加载

JVM系列教程:1、类的加载,第1张

JVM系列教程:1、类的加载

JVM深入理解-1、类的加载
  • 一、JVM(Java Virtual Machine)介绍
  • 二、HotSpot虚拟机
  • 三、类的加载
    • 3.1、java虚拟机与程序的生命周期
    • 3.2、类加载
      • 3.2.1、加载
      • 3.2.2、连接
      • 3.2.3、初始化
      • 3.2.4、使用
      • 3.2.5、卸载
    • 3.3、类的两种使用方式
      • 3.3.1、java当中对类的使用方式可以分为两种
      • 3.3.2、主动使用(7种,非常重要,牢记)
      • 3.3.3、主动使用被动使用之间的关联关系
      • 3.3.4、总结
    • 3.4、类的加载
      • 3.4.1、加载class文件的方式
      • 3.4.2、jvm参数介绍
      • 3.4.3、类加载机制示例

一、JVM(Java Virtual Machine)介绍

JVM即java虚拟机,是一个令人望而却步的领域,因为它很博大精深,涉及到的内容与知识点非常之多,虽然java开发者每天都再使用jvm但对其有深入研究的人很少,基于jvm的动态或静态语言生态圈已经非常繁荣了,对jvm运行机制有一定的理解不但可以提升我们的竞争力,还可以让我们再面对问题的时候沉着应对,加速问题的解决速度,同时还能提升我们的自信心让我们更加游刃有余。

二、HotSpot虚拟机

HotSpot是sun公司开发的虚拟机,但不仅仅只有sun有,其他公司也有,比如ibm就提供了自己的jvm实现,包括linux下的openjdk它是一个虚拟机的开源实现。虚拟机是有规范的,只要你参照规范把它实现出来那么他就是一个可用的虚拟机。本文所讲内容均基于HotSpot虚拟机。

三、类的加载 3.1、java虚拟机与程序的生命周期

在如下情况下java虚拟机将结束生命周期

  • 执行了System.exit();方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或者错误而异常终止。
  • 由于 *** 作系统出现错误而导致java虚拟经济进程终止

我们能控制的有前三种,第四种我们无法控制了解即可。

3.2、类加载

在java代码中,类型的加载,连接与初始化过程都是在程序运行期间完成的。

  • 加载:将class字节码从磁盘加载到内存
  • 连接:建立类之间的关系以及字节码校验
  • 初始化:类型静态变量的赋值是在初始化阶段完成的

这么做的目的是为了提高灵活性,和更多的可能性。

3.2.1、加载

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

3.2.2、连接
  • 验证:确保被加载的类的正确性(确保字节码正确,不被恶意的篡改)
  • 准备:为类的静态变量分配内存,并将其初始化为默认值
    比如一个类中private static int a = 1;,并不是直接赋值1,而是先分配内从赋值为默认值而1是在后面的某个阶段(初始化阶段)再赋值为1
  • 解析:吧类中的符号引用转换为直接引用
    1、符号引用:一种间接的引用方式
    2、直接引用:通过指针的方式直指向目标对象内存位置
3.2.3、初始化

在连接阶段之后进行,为类的静态变量赋予正确的初始值。

其实还有使用和卸载这两个阶段,因为比较简单没什么可说的,简单介绍。

3.2.4、使用

顾名思义就是程序员使用它.

3.2.5、卸载

类不光可以加载到内存当中还可以卸载。

3.3、类的两种使用方式 3.3.1、java当中对类的使用方式可以分为两种
  • 主动使用
  • 被动使用

所有java虚拟机实现必须在每个类或接口被java程序 首次主动使用 时才初始化他们。

3.3.2、主动使用(7种,非常重要,牢记)
  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射初始化一个子类
  • java虚拟机启动时被标明为启动类的类
  • jdk1.7开始提供的动态语言支持:Java.lang.invoke.MethodHandle 实例的解析结果REF_getStatic,REF_putStatic,REF_invockStatic句柄对应的类没有初始化则初始化

除了上面所列举的七种情况之外其他使用java类的方式都被看做是对类的被动引用,都不会导致类的初始化。

3.3.3、主动使用被动使用之间的关联关系

示例1:

public  class MyTest {
    public static void main(String[] args) {
        System.err.println(MyChild.str);
    }
}
 class MyParent1 {
    public static String str = "hello world";
    static {
        System.err.println("MyParent1 STATIC block print");
    }
}
class MyChild extends MyParent1 {

    static {
        System.err.println("MyChild static block print");
    }
}

输出结果为:

MyParent1 STATIC block print
hello world

System.err.println(MyChild.str); 这行代码称作对MyParent1的主动使用,谁定义的静态变量就代表谁主动使用有没有感到意外,接下来我再修改下代码:
示例2:

public  class MyTest {
    public static void main(String[] args) {
        System.err.println(MyChild.str2);
    }
}
 class MyParent1 {
    public static String str = "hello world";
    static {
        System.err.println("MyParent1 STATIC block print");
    }
}
class MyChild extends MyParent1 {
    public static String str2 = "welcome";
    static {
        System.err.println("MyChild static block print");
    }
}

此时打印结果为:

MyParent1 STATIC block print
MyChild static block print
welcome

str2是在子类中定义的,符合 初始化一个子类 其父类也会初始化,即一个子类被初始化的时候要求它的所有父类也要初始化,所以MyParent1必须先行初始化。

3.3.4、总结

1、对于静态字段来说只有直接定义了该字段的类才会被初始化
2、当一个类初始化时要求它的父类都全部初始化完毕,且每个类只会初始化一次Object肯定是第一个被初始化的
3、当执行System.err.println(MyChild.str);时子类没有初始化,那子类有没有被加载呢?可以通过-XX:+TraceClassLoading用于追踪类的加载并打印结果。从打印结果中可以看到加载了MyChild的,由于打印内容太多就不贴出来了,感兴趣的可以自己打印。

3.4、类的加载 3.4.1、加载class文件的方式
  • 从本地系统中直接加载
  • 通过网络下载class文件
  • 从zip、jar等归档文件中加载class文件
  • 从传统数据库中提取class文件
  • 将java源文件动态的编译为class文件
3.4.2、jvm参数介绍

在jvm参数当中一共有以下三种
-XX:+ 加号表示开启选项
-XX:- 减号表示关闭选项
-XX:=:表示将option的值设置为value
在idea中添加vm option参数,–XX:+TraceClassLoading用于打印类加载信息

3.4.3、类加载机制示例

这些示例很重要最好每一个自己敲一遍才能深入体会

示例1:

public class MyTest2 {
    public static void main(String[] args) {
        System.err.println(MyParent2.str);
    }
}
class MyParent2 {
    public static String str = "hello world";
    static {
        System.err.println("MyParent2 static block");
    }
}

输出结果为:

hello world
MyParent2 static block

System.err.println(MyParent2.str);是对MyParent2的主动使用,会导致MyParent2加载所以静态代码块会执行。
我们对变量str加上final关键字:
示例2:

public class MyTest2 {
    public static void main(String[] args) {
        System.err.println(MyParent2.str);
    }
}
class MyParent2 {
    public static final String str = "hello world";
    static {
        System.err.println("MyParent2 static block");
    }
}

加final与不加的区别挺大,为什么呢?原因是在编译阶段这个常量就会被存入到调用这个常量的那个方法所在类的常量池中。对于例子中hello
world会被放到MyTest2类的常量池中,本质上调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。将字符串存到MyTest2的常量池中之后二者就没有任何关系了,甚至我们可以将MyParent2的class文件删除都可以正常运行。

反编译MyTest2

javap -c org.javabase.jvm.MyTest2
Compiled from "MyTest2.java"
public class org.javabase.jvm.MyTest2 {
  public org.javabase.jvm.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.err:Ljava/io/PrintStream;
       3: ldc           #4                  // String hello world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

可以看到ldc右边的注释已经是hello world了,说明Mytest2在初始化的时候已经放到常量池中,就不用再去MyParent2中去取了。
助记符ldc是什么意思呢?它表示将int、float、或者是String类型的常量值从常量池中推送至栈顶。
再添加一个变量public static final short s = 7;
示例4:

public class MyTest2 {
    public static void main(String[] args) {
        System.err.println(MyParent2.s);
    }
}
class MyParent2 {
    public static final String str = "hello world";
    public static final short s = 7;
    static {
        System.err.println("MyParent2 static block");
    }
}

反编译:

public class org.javabase.jvm.MyTest2 {
  public org.javabase.jvm.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.err:Ljava/io/PrintStream;
       3: bipush        7
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       8: return
}

bipush将-127到128之间的值推送到栈顶

将public static final shorts = 7;改为public static final int s = 777;再反编译:

public class org.javabase.jvm.MyTest2 {
  public org.javabase.jvm.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.err:Ljava/io/PrintStream;
       3: sipush        777
       6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       9: return
}

sipush表示将短整型常量值(-32768到32767)推送至栈顶
定义public static final int s = 1;再反编译

public class org.javabase.jvm.MyTest2 {
  public org.javabase.jvm.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.err:Ljava/io/PrintStream;
       3: iconst_1
       4: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       7: return
}

iconst_1表示将int型的1推送至栈顶,分别将是的值改为12、3、4、5会发现iconst_2、iconst_3、iconst_4、iconst_5直到6以后就没了,可能是因为jvm认为1到5比较常用.0和-1也是同样的效果。
示例5:

public class MyTest3 {
    public static void main(String[] args) {
        System.err.println(MyParent3.str);
    }
}
class MyParent3{
    public static final String str = UUID.randomUUID().toString();
    static {
        System.err.println("MyParent3 static block");
    }
}

与上面示例4的结果不同都是static代码块执行了,区别是在于常量的值在编译期能不能确定。UUID的值在编译期是不知道的只有运行的时候才知道。当一个常量的值并非编译期间可以确定,那么其值就不会被放到调用类的常量池中,这时在程序运行当中会导致主动使用这个常量所在的类
示例6:

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4 myParent4 = new MyParent4();

    }
}
class MyParent4{
    static {
        System.err.println("MyParent4 static block");
    }
}

打印结果为MyParent4 static block,符合new对象是主动使用的规则,

示例7:

public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent5 = new MyParent4[1];

    }
}
class MyParent4{
    static {
        System.err.println("MyParent4 static block");
    }
}

表示MyParent4的数组类型,静态代码块没有打印,通过我们执行System.err.println(myParent5.getClass());打印结果为class [Lorg.javabase.jvm.MyParent4;,其实这个类型是jvm在运行期帮我们生成的

示例8:

public class MyTest5 {
    public static void main(String[] args) {
        System.err.println(MyChild5.b);
    }
}
interface MyParent5{
    public static Thread t = new Thread() {
        {
            System.err.println("MyParent5");
        }
    };
}
class MyChild5 implements MyParent5{
    public static int b = 6;
}

当一个接口初始化时不要求父接口也初始化,只有在正真使用到父接口的时候,比如适使用父接口定义的常量的时候,才会导致父接口被初始化

以下代码的输出结果是什么?

public class MyTest6 {
    public static void main(String[] args) {
        SingleTon instance = SingleTon.getInstance();
        System.err.println(SingleTon.counter1);
        System.err.println(SingleTon.counter2);
    }
}

class SingleTon {
    public static int counter1;

    private static SingleTon singleTon = new SingleTon();
    private SingleTon () {
        counter1 ++;
        counter2 ++;
        System.err.println(counter1);
        System.err.println(counter2);
    }
    public static int counter2 = 0;
    public static SingleTon getInstance() {
        return singleTon;
    }
}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5574888.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存