JVM-计算机基础与Class文件加载机制(类加载时机/双亲委派模型/类加载过程/JIT即时编译器)

版权声明:中华人民共和国持有版权 https://blog.csdn.net/Fly_Fly_Zhang/article/details/89646601

计算机基本概念:

计算机存储元件:

  • 寄存器:
    中央处理器CPU的一部分,是计算机中读写速度最快的存储元件,但是容量很少。
  • 内存:
    属于独立的一个部件,是和CPU沟通的桥梁,用于存放CPU中的运算数据以及与外部存储器交换的数据。虽然现在内存的读写速度已经很快了,但是由于寄存器是在CPU上,所以对于内存的读写速度和对寄存器的读写速度还是有几个数量级的差距。 但是对于内存的读写I/O是很难消除的,寄存器的数量有限,不可能通过寄存器来完成所有的运算任务。

内核空间和用户空间:

连接内存和寄存器的是地址总线,地址总线的宽度影响了物理地址的索引范围,因为总线宽度决定了处理器一次可以从寄存器或内存中获取多少bit,同时也决定了处理器最大可以寻址的地址空间。比如32位CPU的系统,可寻址范围为 0x00000000-0xffffffff 即2^32 个内存位置,每个内存位置1个字节,即32位CPU系统可以有4GB的内存空间。不过应用程序是不可以完全使用这些地址空间的,因为这些地址空间被划分为了内核空间和用户空间,程序只能使用用户空间的内存。内核空间 主要是指操作系统运行时所使用的用于 系统调度,虚拟内存使用或者链接硬件资源的程序逻辑 。区分内核空间和用户空间的目的主要是从系统稳定性角度考虑的, windows 32 操作系统默认内核空间和用户空间的比例是1:1 ,即2G内核空间,2G内存空间, 32位linux系统中默认比例则是1:3 ,即1G内核空间,3G内存空间。

字长:

CPU的主要技术指标之一,指 CPU一次并行处理二进制的位数(bit) 。通常称处理字长为8位数据的CPU为8位CPU, 32CPU就是在同一时间内处理字长为32位的二进制数据。 虽然现在CPU大多是64位的,但是依然以32位字长运行。

class文件加载机制:

java虚拟机示意图:

在这里插入图片描述

类加载时机:

java虚拟机规范严格规定了5种情况必须立即对类进行“初始化” (class文件加载到JVM中)

主动初始化的6种方式:

  • 创建对象实例: new对象的时候,会进行类的初始化,前提是这个了没有被初始化;
  • 调用类的静态属性或者为静态属性赋值:
  • 调用类的静态方法:
  • 通过class文件反射创建对象:
  • 初始化一个类的子类:使用子类的时候会先初始化父类:
  • java虚拟机启动时被标记为启动类的类: 如main方法所在的类;

由上可以得出: java类的加载是动态的,它并不会一次性将所有类全部加载后在运行,而是保证程序运行的基础类(基类)完全被加载到JVM中,至于其他类,则在需要的时候才进行加载。 这样做的目的是节省内存开销

不会初始化的情况:
  • 在同一个类加载器下面只能初始化一次,如果初始化过一次,那就不需要在进行初始话。
  • 在编译期间就能确定下来的静态变量(编译常量) ,不会对类进行初始化。比如final修饰的静态变量。

类的实例的初始化步骤:

没有父类的情况:
  • 类的静态属性;
  • 类的静态代码块;
  • 类是非静态属性;
  • 类的实例代码块;
  • 构造方法;
有父类情况:
  • 父类静态属性;
  • 父类静态代码块;
  • 子类静态属性;
  • 子类静态代码块;
  • 父类实例属性;
  • 父类实例代码块;
  • 父类构造函数;
  • 子类实例属性;
  • 子类实例代码块;
  • 子类构造函数;

在多次类实例化中,类静态属性和方法只会实例化一次,也就是执行一次

如何将类加载到JVM:

class文件是通过类加载器 装载到jvm的。

java默认有三种类加载器:

在这里插入图片描述

类加载器分类:
从JVM角度来讲,只存在以下两种不同的类加载器:
  • Bootstrap ClassLoader(启动类加载器): 这个类加载器用C++实现,是虚拟机自身的一部分;
  • 其它所有类加载器:这些类由java实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader ;
从开发人员角度:
  • Bootstrap ClassLoader(启动类加载器): 负责加载$JAVA_HOME中jre/bin/rt.jar里所有的class文件(由C++实现)或者被Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar ; 名字不符合即使放在lib目录下也不会被加载)类库加载到虚拟机内存中;
    其不是ClassLoader子类;
    启动类加载器无法被java程序直接引用,用于在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器时,直接直用null即可;
  • Extension ClassLoader(扩展类加载器): 负责加载java平台中扩展功能的一些jar包,包括 $JAVA_HOME中 jre/bin/ext/*.jar 或者-Djava.ext.dirs指定目录下的jar包。 开发者可以直接使用扩展类加载器;
  • Application ClassLoader(应用程序类加载器): 负责加载classpath中指定的jar包以及目录中的class; 这个类加载器是由AppClassLoader(sun.misc.launcher$AppClassLoader)实现的,由于这个类是ClassLoader中getSystemClassLoader()方法的返回值,因此一般称为系统类加载器 它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义的类加载器,一般情况下这个就是程序中默认的类加载器;
双亲委派模型:

在这里插入图片描述

工作步骤:
  • 当app加载到一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给Ext去完成;
  • Exe加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给boot来尝试加载;
  • 如果boot加载失败(指定路径中未查找到该class),会使用Ext来尝试加载;
  • 如果Ext也加载失败,则会使用App来加载;
  • 如果App加载失败则抛出异常(ClassNotFoundException)
    简单来说:就是一个类加载器收到了类加载请求,他不会尝试自己去加载这个类,而是把请求委托给父类加载器去完成,依次向上;
双亲委派模型的好处:
  • 防止内存中出现多份同样的字节码(安全性角度),也就是说防止用于编写的类动态替换java的一些核心类;
  • 避免类的重复加载: 因为JVM判定两个类是否是同一个类,不仅仅根据类名是否相同进行判断,还需要判断加载该类的是不是同一个加载器;
  • 使得java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使基础类得到统一;
    例如:java.lang.Object 存放在rt.jar中,如果另外编写一个java.lang.Object类并放到ClassPath中程序是可以编译通过的,但是不能运行.由于双亲委派模型的存在,所以rt.jar的Object比ClassPath中的Object优先级更高.所以只会加载rt.jar中的Object;
  • 特别说明: 类加载器在成功加载到某个类之后,会把得到的类实例缓存起来(class对象) 下次再请求加载该类时,类加载器会直接使用缓存的类的实例,而不是尝试加载;
ClassLoader类方法:
  • loadclass: 负责以双亲委派的方式去加载类;
  • findclass:根据类的包路径找到class文件;
  • defineclass:负责从字节码中加载class对象,然后class对象通过反射机制生成对象;
    findclass次数<=loadclass次数;
如何实现一个类加载器:

自定义实现类加载器是以不破坏双亲委派模型为前提的;
因此只能重写findclass;

  • 重写loadclass会破坏双亲委派模型;
  • defineclass只有包权限,只能源码调用;
import java.io.*;

class MyClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //TODO Auto-generated method stub
        String Dir="D:";//用户自定义路径进行拼接
        String classPath=Dir+File.separatorChar+name.replace('.',File.separatorChar)+".class";
        File file= new File(classPath);
        byte[] buff=null;
        InputStream in;
        try{
            in=new FileInputStream(file);
            buff=new byte[in.available()];
            in.read(buff)
            in.close();
            Class<?> c=defineClass(name,buff,0,buff.length);
            return c;
        }catch (FileNotFoundException e){
            //TODO Auto-generated catch block
            e.printStackTrace();
        }catch(IOException e) {
            //TODO Auto-generted catch block
            e.printStackTrace();
        }
        return super.findClass(classPath);
    }
}

类的生命周期:

在这里插入图片描述

扫描二维码关注公众号,回复: 6144428 查看本文章
类加载过程:

包含了加载,验证,准备,解析和初始化这5个阶段;

1,加载:查找并加载类的二进制数据,在java的方法区创建一个java.lang.Class类的对象。

加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构 转化为方法区的运行时存储结构
  • 在内存中生成一个代表这个类的class对象,作为方法区这个类的各种数据访问入口。
  • 其中二进制字节流可以从以下方式中获取:
  • 从ZIP包中读取,称为JAR,EAR,WAR格式的基础。
  • 从网络中获取,最典型的应用就是Applet.
  • 运行时计算生成,例如动态代理技术,在java.lang.reflect.Proxy使用ProxyGenerator.generateProxyClass的代理类的二进制字节流。
  • 由其他文件生成,例如由JSP文件生成。
2,验证:文件格式,元数据,字节码,符号引用验证。

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3,准备:为静态变量分配内存,并将其初始化为默认值。

类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
初始值一般为默认值,下面静态类变量value被初始化为0而不是123:

public static int value=123; 

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为0;

public static final int value=123;

实例变量不会在这个阶段分配内存,它将会在对象实例化时随对象一起分配在堆中。
注意: 实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只能进行一次,实例化可以进行多次。

4,解析:把类中符号引用转换为直接引用

将常量池的符号引用替换为直接引用的过程。(符号变成直接地址)
其中解析过程在某些情况下可以在初始化阶段之后这是为了支持java的动态绑定

5,初始化:为类得静态变量赋予正确得初始值;

初始化阶段才真正开始执行类中定义的java程序代码。初始化阶段即JVM执行类构造器 < clinit >()方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据代码的去初始化变量和其它资源。

< clinit >() 方法具有以下特点:
  • 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值不能访问。
public class Test{
   
     {
         i=0;   //变量赋值操作可以正常编译通过;
         System.out.print(i); //这句会提示 : 非法向前引用
     }
      static int i=0;
}

  • 与类的构造函数(或者说实例构造器)不同,不需要显示的调用父类的构造器 。虚拟机会自动保证在子类的< clinit >()方法之心之前,父类的< clinit >() 方法已经执行结束。因此虚拟机中第一个执行的< clinit >() 方法的类肯定是Object类;
  • 由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
  • < clinit >()方法对于类或者接口不是必须的,如果一个类不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成< clinit >()方法
  • 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成< clinit >()方法,但是接口与类不一样的是,执行接口< clinit >()方法不需要执行父接口的< clinit >()方法。只有当父接口定义的变量使用时,父接口才会初始化。另外,接口实现的类在初始化时也一样不会执行接口的< clinit >()方法。
  • 虚拟机会保证一个类的< clinit >()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的< clinit >()方法,其它线程都会被阻塞等待,直到活动线程执行< clinit >()方法完毕,如果在一个类的< clinit >()方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

JIT即时编译器:对于热点代码JVM将字节码重新编译优化成机器码;非热点代码JVM使用解析器逐条解析。

一般我们可能会想:JVM加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行 解析器解析;但是这样速度太慢。
JVM是这样实现的:

  • 就是把这些java字节码重新编译优化 生成机器码,让CPU直接执行。这样效率更高。
  • 编译也是要花费时间的,我们一般对热点代码 做编译,非热点代码直接解析就好了。
什么是热点代码:
  • 多次调用的方法;
  • 多次执行的循环体;
使用热点探测来检测是否为热点代码:
  • 采样;
  • 计数器;
HotSpot使用的是 计数器的方式 它为每个方法准备了两类计数器:
  • 方法调用计数器:
  • 回边计数器:
  • 在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值,就会触发JIT编译
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Fly_Fly_Zhang/article/details/89646601