jvm类加载子系统之类的初始化

2021/7/11 23:16:00

本文主要是介绍jvm类加载子系统之类的初始化,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 前言
    • 一、具体描述
    • 二、的线程安全问题
      • 1.描述
      • 2.举例
    • 三、主动使用与被动使用
      • 主动使用
      • 被动使用


前言

语雀地址:https://www.yuque.com/yangxiaofei-vquku/wmp1zm/dmz2gd

一、具体描述

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题说明类可以被顺利装载到系统中,此时jvm才会执行类中的字节码(即到了初始化阶段才真正开始执行类中定义的java程序代码)。

  • 初始化阶段就是执行类初始化方法()的过程
  • 此方法不需要程序员定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句按照从上到下的顺序合并得到的。
  • 构造方法中的指令按照赋值动作和静态代码块在源文件中出现顺序排列执行
  • 不同与类的构造方法,类的构造器方法在虚拟机的角度下是()
  • 如果该类具备父类,JVM保证子类的执行前父类的已经执行完毕
  • 虚拟机必须保证一个类的在多线程被同步加锁,不会多次执行

javac编译器并不是所有的类编译后都会产生方法,下面几种类在编译后字节码文件中就不会包含方法。

  • 一个类中没有声明任何类变量,也没有静态代码块时
  • 一个类中声明了类变量但是没有显示赋初始值也没有通过静态代码块赋初始值时
  • 一个类中仅包含static final修饰的基本数据类型,且这些变量的初始化赋值语句采用的是编译时常量表达式即在常量池中有字面量引用时

在这里插入图片描述

二、的线程安全问题

1.描述

对于方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境下的安全性。虚拟机会保证一个类的方法在多线程环境下被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行完方法完毕。正是因为方法是带锁且线程安全的,如果一个类中的方法中存在很多耗时较长的操作,就可能造成多个线程阻塞,引发死锁。方法的锁一个隐式锁不同于直接用synchronized关键字标记的方法,这种死锁是很难被发现的,因为看起来它们并没有可用的锁信息。
如果之前线程成功的加载了该类则等待在队列中的线程就没有机会再执行方法了,当使用这个类是虚拟机会直接返回已经初始化好的类信息。

下面通过一段代码,看下方法和显式synchronized标记的方法在字节码层面的不同。

package jvm.memory.clinit;

public class ClinitDemo {
        static String aa="123";
        static int bb=12;

        static {
            aa="234";
        }

        static synchronized void  locakDemo(){
            System.out.println("这个方法加了同步锁");
        }
}

显式synchronized标记的locakDemo
在这里插入图片描述

ClinitDemo的方法
在这里插入图片描述

可以看到的访问标志上没有锁标记,属于隐性锁实现,又jvm控制,所以通常的死锁排查工具不易发现导致的死锁问题。

2.举例

代码如下,staticA的中初始化staticB,staticB的中初始化staticA,启动线程1初始化staticA,线程2初始化staticB

package jvm.memory.clinit;

public class ClinitLockDemo {

    public static void main(String[] args) {
        StaticDeadLockMain staticDeadLockMain1=new StaticDeadLockMain("A");
        staticDeadLockMain1.setName("线程1");
        StaticDeadLockMain staticDeadLockMain2 = new StaticDeadLockMain("B");
        staticDeadLockMain2.setName("线程2");
        staticDeadLockMain1.start();
        //注释
        staticDeadLockMain2.start();
    }
}

class staticA{
    static {
        try {
            System.out.println("线程"+Thread.currentThread().getName()+"正在初始化staticA");
            Thread.sleep(1000);
            System.out.println("线程"+Thread.currentThread().getName()+"》staticA的<clinit>方法正在执行,需要初始化staticB");
            // 在staticA的<clinit>方法里显示加载并初始化staticB
            Class.forName("jvm.memory.clinit.staticB",true,ClinitLockDemo.class.getClassLoader());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("staticA加载完毕");
    }
}

class staticB{
    static {
        try {
            System.out.println("线程"+Thread.currentThread().getName()+"正在初始化staticB");
            Thread.sleep(1000);
            System.out.println("线程"+Thread.currentThread().getName()+"》staticB的<clinit>方法正在执行,需要初始化staticA");
            // 在staticB的<clinit>方法里显示加载并初始化staticA
            Class.forName("jvm.memory.clinit.staticA",true,ClinitLockDemo.class.getClassLoader());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("staticB加载完毕");
    }
}


class StaticDeadLockMain extends Thread{
    private String flag;

  public StaticDeadLockMain(String flag){
    this.flag=flag;
  }

    @Override
    public void run() {
        try {
            Class.forName("jvm.memory.clinit.static"+flag,true,ClinitLockDemo.class.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

打印结果如下,此时程序并未结束,处于死锁中。

线程线程2正在初始化staticB线程线程1正在初始化staticA
线程线程2》staticB的<clinit>方法正在执行,需要初始化staticA
线程线程1》staticA的<clinit>方法正在执行,需要初始化staticB

当注释掉第12行代码staticDeadLockMain2.start();后再此执行打印结果如下,此时程序结束

线程线程1正在初始化staticA
线程线程1》staticA的<clinit>方法正在执行,需要初始化staticB
线程线程1正在初始化staticB
线程线程1》staticB的<clinit>方法正在执行,需要初始化staticA
staticB加载完毕
staticA加载完毕

三、主动使用与被动使用

主动使用

Class只有在必须首次使用时才会初始化,Java虚拟机规定一个类或接口在初次使用之前必须要进行初始化,这里的“使用”指的是首次使用,而初始化操作之前的加载、验证、准备阶段都已经完成了。主动使用的情况如下:

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
  2. 调用类的静态方法时(使用子类调用父类中的静态方法,子类不会初始化,此类必须为该方法的定义类),即使用了字节码invokestatic指令
  3. 当使用类或者接口的静态字段时(使用子类调用父类中的静态字段,子类不会初始化,此类必须为该字段的声明类)final修饰的且初始值为字面量表达式的除外
  4. 当使用java.lang.reflect包中的方法反射类的方法时,比如Class.forName(“com.yxf.Demo”)
  5. 当初始化子类时,如果发现其父类还未初始化,则需要出发其父类的初始化
  6. 当虚拟机启动时,用户指定要执行main方法定义的类
  7. 当初次调用MethodHandle实例时,初始化还MethodHandle指向的方法所在的类,涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类(通过C语言Native方法调用java方法时会用到,典型存在于JNI扩展开发的情景)

针对5.补充说明:
当java虚拟机初始化一个类时,要求它的所有父类都已经初始化,但是这条规则并不适用于接口。

  • 在初始化一个类时,不会先初始化它所实现的接口
  • 在初始化一个接口是,并不会先初始化它的父接口
    因此一个父接口不会因为他的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

针对6.补充说明:
JVM启动的时候加载一个启动类(定义main方法的类),这个类调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需要的类的加载、链接和初始化。

被动使用

除了以上的情况属于主动使用,其他情况均数据被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会初始化。

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化,当通过子类引用父类的静态变量时不会导致子类的初始化。
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用static final修饰的基本数据类型,且这些变量的初始化赋值语句采用的是编译时常量表达式即在常量池中有字面量引用时,声明此常量的类或接口不会被初始化
  4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
  5. 使用Class.forName(“demo.yxf.Demo”,false,classLoader);反射加载获取一个类时,如果第二个参数是false,不会造成对类的主动使用,不会导致类的初始化


这篇关于jvm类加载子系统之类的初始化的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程