美文网首页
JVM-类生命周期

JVM-类生命周期

作者: 麦大大吃不胖 | 来源:发表于2020-12-02 10:05 被阅读0次

by shihang.mai

0. 生命周期概述

  1. 我们编写的源代码(.java文件),需要经过javac,将从java文件变为class文件。这个阶段叫编译期,属前段编译。
  2. class文件,经过类初始化过程,然后new 类被我们程序中使用。这个阶段叫运行期,涉及后端编译。
  3. 类卸载

1. 编译期

源代码.java会经过词法分析、语法分析、语义分析与中间代码生成,生成.class文件

1.1 编译期优化

  • 如没定义构造方法,自动添加默认无参构造
  • 自动拆装箱
  • 泛型擦除
  • 可变参数。参数:String...args->String [] args
  • lombok,在设置了相关注解后lombok会在编译期生成源代码中没有的方法等

2. 运行期

  1. jvm一般都有解析器和编译器。
  2. 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
  3. 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地机器码之后,可以获得更高的执行效率。即使用JIT技术。将热点代码变为机器码,把将.class文件翻译成机器指令

哪些是热点代码?

  • 被多次调用的方法(JIT)
  • 被多次执行的循环体 OSR

如何找到热点方法?

  • 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是热点方法

  • 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。

HotSpot 虚拟机采用的是第二种:基于计数器的热点探测。因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter),这两个计数器都有一个确定的阈值,当计数器超过阈值就会触发 JIT 编译

方法调用计数器


方法调用计数器

方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器值就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰期

回边计数器


回边计数器

回边计数器没有计算热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程

2.1 运行期优化

  1. 公共子表达式消除
    如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替 E 就好了
  2. 数组边界检查消除
    在访问数组时,系统会自动进行上下界的范围检查。但是有N个数组,这样多个判断要消耗很多资源。在循环的时候访问数组,如果编译器只要通过数据流分析就知道循环变量是不是在区间 [0, array.length] 之内,那在整个循环中就可以把数组的上下界检查消除
  3. 方法内联
    调用同样的方法,直接拆去方法的调用
  4. 逃逸分析
    当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
    4.1 栈上分配:如果确定一个对象不会逃逸到方法之外,那么就可以在栈上分配内存.对象所占的内存空间就可以随栈帧出栈而销毁,这样会大大减轻 GC 的压力
    4.2 同步消除:如果逃逸分析能确定一个变量不会逃逸出线程,无法被其它线程访问,那这个变量的读写就不会有多线程竞争的问题,因而变量的同步措施也就可以消除了
    4.3 标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,那程序执行的时候就可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来替代
  5. 锁粗化

    JVM会检测到一连串的操作都对同一个对象加锁,那么会将锁粗化到这一连串操作外 锁粗化.jpg
  6. 锁消除
    Java虚拟机在JIT编译时,通过对运行上下文的扫描,经过逃逸分析(没方法逃逸、没线程逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗


    锁消除.png

3. 类初始化过程

类初始化过程

3.1 class文件结构

可用idea插件BinEd查看.class 16进制码

可用idea插件JclassLib查看.class 详细信息

class文件有以下几个属性

属性 备注
magic class文件标示 CAFE BABE
minor version 小版本(52.0的0)
Major version 大版本(52.0的52)
Constant_pool_count: 常量池个数。常量池编号从1开始
Constant 常量。个数=Constant_pool_count-1
access_flags 标识public final interface
This class 当前class在常量池的位置
Super class 父class在常量池的位置
Interface_count 接口数量
Interfaces 接口
Fields_count 属性数量
fields 属性
Method_count 方法数量
Methods 方法
Attributes_count 附加值数量
Attributes 附加值

3.2 Loading

3.2.1 双亲委派模型

ClassLoader和其加载的类路径

类加载器 加载的class
BootStrap lib/rt.jar charset.jar等核心类,C++实现的
Extension jre/lib/ext/*.jar
App classpath下的class
Custom 自定义加载

Class都是被ClassLoader加载入内存的,ClassLoader间并没有extends关系,ClassLoader(基础的App Extension)都是BootStrap加载的。

自定义的ClassLoader实际就是自己写的一个类,那么肯定是App加载,即自定义的ClassLoader由App加载。

ClassLoader双亲委派模型

双亲委派模型作用:1.主要为了安全 2.次要为了资源,加载过的不加载

例如:现在有一个叫java.lang.String(自己写的),直接用CustomClassLoader加载,打包成一个jar,交给客户,然后客户输入密码,存放在String,将密码偷偷向我邮箱发送。全世界用到我类库的,我都能获取到密码。

3.2.2 破坏双亲委派模型

首先双亲委派模型在ClassLoader.loadClass()中写的,重写loadClass即可破坏双亲委派

  1. JNDI定义了一些标准的接口放在BootStrap加载,例如,jdbc是需要各厂商第三方实现的,必然不是BootStrap加载,但是工程启动时,却父能看见子,根据双亲委派模型可见性,显然破坏了

  2. tomcat作为一个容器,它可以启动多个war,而war中同一个类可以是多个版本

3.2.3 自定义类加载器

查看ClassLoader类中的loadClass(),其中findClass()用到了模版方法设计模式

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

extends ClassLoader,重写findClass方法即可,在findClass方法里面还需要用到defineClass方法。

public class T006_MSBClassLoader extends ClassLoader {
    public T006_MSBClassLoader() {
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace(".", "/").concat(".class"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            boolean var5 = false;

            int b;
            while((b = fis.read()) != 0) {
                baos.write(b);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception var7) {
            var7.printStackTrace();
            return super.findClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new T006_MSBClassLoader();
        Class clazz = l.loadClass("com.maishihang.jvm.Hello");
        Class clazz1 = l.loadClass("com.maishihang.jvm.Hello");
        System.out.println(clazz == clazz1);
        Hello h = (Hello)clazz.newInstance();
        h.m();
        //===>App
        System.out.println(l.getClass().getClassLoader());
        //===>App
        System.out.println(l.getParent());
        System.out.println(getSystemClassLoader());
    }
}
  1. 先利用loadClass进行双亲委派模型
  2. 父加载不了,进入findClass,找class加载
  3. 使用defindClass去返回Class对象

3.3 Linking

  1. verification

    验证文件是否符合JVM规定

  2. preparation

    静态变量赋默认值

  3. resolution

    将类、方法、属性等符号引用解析为直接引用。

    在16进制的文件里面,下面方法用到了#1->常量池1号的地方,这个resolution就是将这个#1符号引用解析为真正内存->Object直接引用

    符号引用 常量池

3.4 Initializing

静态变量赋初始值

public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    public static T t = new T(); // null
    public static int count = 2; //0

    //private int m = 8;

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}
  1. 当调用T.count时,AppClassLoader把T加载到内存
  2. 然后执行verification->preparation,将t=null,count=0,->resolution
  3. Initializing,t=new T(),调用构造方法,count=1
  4. count赋初始值,count=2
public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    public static int count = 2; //0
    public static T t = new T(); // null
    //private int m = 8;

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}
  1. 当调用T.count时,AppClassLoader把T加载到内存
  2. 然后执行verification->preparation,将t=null,count=0,->resolution
  3. count赋初始值,count=2
  4. Initializing,t=new T(),调用构造方法,count++,count=3

4. new 类()

对象中的成员变量赋值也分为两部,new T(),向内存申请空间,m=0,再调用构造方法,m=8

5. 类卸载

类除了以上的初始化过程,当然还有类卸载构成整个类的生命周期。

而当代表Msh类的Class对象不再被引用,即不可即不可触及时,Class对象就会结束生命周期,Msh类在方法区内的数据也会被卸载,从而结束Msh类的生命周期,
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

5.1 Class对象关系

ClassLoader会用一个集合存放自己加载过的Class对象的引用,然后Class对象. getClassLoader()又能获取到加载自己的ClassLoader

  1. ClassLoader与Class对象相向联系
  2. 每个类的实例.getClass()能获取到自己对象的Class对象
Class对象关系图

5.2 类卸载条件

  1. 只有自定义类加载器加载的类才可能被卸载
  2. 该类所有的实例已经被回收
  3. 加载该类的ClassLoder已经被回收
  4. 该类对应的java.lang.Class对象没有任何地方被引用
https://zhuanlan.zhihu.com/p/71566226

相关文章

  • JVM-类生命周期

    by shihang.mai 类生命周期 1. 编译期 从java文件变为class文件 解析和填充符号表 注解处...

  • JVM类加载器与双亲委派模型(JDK8)

    引言 在上文JVM-类加载机制[https://imchenway.com/2021/07/01/JVM-%E7%...

  • JVM-类加载器

    JVM-类加载器 类与类加载器 对于类与类加载器有两种限定: 对于任意一个类,都需要由加载它的类加载器和这个类本身...

  • JAVA-大白话探索JVM-类加载过程(二)

    首先我们知道JVM是什么以及类加载器的作用 不清楚的可以看看JAVA-大白话探索JVM-类加载器(一) 现在我们来...

  • 深入了解JVM-方法区

    今天呢!灯塔君跟大家讲: 深入了解JVM-方法区 当JVM使用类装载器装载某个类时,它首先要定位对应的class文...

  • 深入了解JVM-方法区

    今天呢!灯塔君跟大家讲: 深入了解JVM-方法区 当JVM使用类装载器装载某个类时,它首先要定位对应的class文...

  • JVM-从字节码到运行时(2)

    JVM-从字节码到运行时(2) 基于栈的解释器执行过程 这是例子ByteCodeDemo类中的add(int z)...

  • JVM-类加载机制

    1.类加载机制 1.1什么是类的加载 类的加载是将.class文件中的二进制数据读到内存中,并把它放到运行时数据区...

  • JVM-类加载机制

    一.类加载时机 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证...

  • JVM-类加载机制

    前言 近几个月学习了儒猿技术窝的专栏《从 0 开始带你成为JVM实战高手[https://apppukyptrl1...

网友评论

      本文标题:JVM-类生命周期

      本文链接:https://www.haomeiwen.com/subject/hldoiktx.html