美文网首页
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-类生命周期

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