JVM主要组成部分及其作用

目录

1、JVM主要组成部分及其作用

 2、类加载器

 2.1、类加载过程

2.1.1、加载

2.1.2、验证

2.1.3、准备

2.1.4、解析

2.1.5、初始化

2.1.6、类卸载

2.2、类加载器作用

2.3、类加载器加载规则

2.4、自定义类加载器

2.5、双亲委派模型

2.5、双亲委派模型好处

2.6、打破双亲委派模型

3、执行引擎

4、运行时数据区域

4.1、方法区

4.1.1、简介

4.1.2、运行时常量池

4.2、虚拟机栈

 5、本地方法栈

6、堆

7、程序计数器


1、JVM主要组成部分及其作用

  • Class loader(类加载器):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区中的方法区
  • Execution engine(执行引擎):执行引擎也叫解释器,负责解释命令,交由操作系统执行
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存,我们所有所写的程序都被加载到这里,之后才开始运行。

        首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能

 2、类加载器

        类从被加载到虚拟机内存到开始卸载出内存为止,生命周期可以简单概括为7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,前三个阶段可以统称为连接(Linking)。

 2.1、类加载过程

        类加载过程描述的是类的生命周期从加载到初始化的阶段。

2.1.1、加载

  • 通过全类名获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  • 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
    加载这一步主要是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由双亲委派模型 决定。Java中的每个类都有一个引用指向它的ClassLoader。数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

加载阶段与连接阶段是交叉进行的,加载尚未结束,连接阶段可能就开始了

2.1.2、验证

  1. 文件格式验证(Class 文件格式检查,比如版本号是否在当前虚拟机的处理范围之内)

    基于类的二进制字节流进行,目的是保证输入的字节流能正确地解析并存储在方法区内。

    而其他三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流。

  2. 元数据验证(字节码语义检查,比如这个类是否有父类,是否继承了final类等)

  3. 字节码验证(程序语义检查)

  4. 符号引用验证(类的正确性检查,比如该类使用的其他类是否存在,字段是否存在等)

    发生在类加载过程中的解析阶段,具体点说就是JVM将符号引用转化为直接引用的时候。

    用来确保解析阶段能够正常执行。如果无法通过符号引用验证,JVM会抛出异常。

2.1.3、准备

        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 此时分配的变量仅包括静态/类变量(static 修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  2. JDK7之前,类变量使用的内存都在方法区(永久代)中分配,JDK7之后,HotSpot使用元空间来代替方法区,而字符串常量池和静态变量移动到了堆中。那么类变量就随着Class对象一起存放在Java堆中。

  3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。

2.1.4、解析

        解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

  • 直接引用:直接引用是可以直接指向目标的指针。
    举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量

2.1.5、初始化

        初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  • 当遇到 new、getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  • 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("...")newInstance() 等等。如果类没初始化,需要触发其初始化。
  • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  • 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

2.1.6、类卸载

卸载类需要满足如下要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

2.2、类加载器作用

        类加载器的主要作用就是加载Java类字节码(.class文件)到JVM中(在内存中生成CLass对象代表该类)。类加载器实现了类加载过程中的加载这一步。

        每个 Java 类都有一个引用指向加载它的 ClassLoader

2.3、类加载器加载规则

        大部分类在具体用到才会去加载,这样对内存更为友好。对于已经加载的类,会被存放在ClassLoader中,在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

JVM 中内置了三个重要的 ClassLoader

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar 、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
    • rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*
  • ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader 抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过 BootstrapClassLoader 加载的。

2.4、自定义类加载器

        除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

ClassLoader 类有两个关键的方法:

  • protected Class loadClass(String name, boolean resolve)加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类
  • protected Class findClass(String name)根据类的二进制名称来查找类,默认实现是空方法

建议 ClassLoader的子类重写 findClass(String name)方法而不是loadClass(String name, boolean resolve) 方法。

也就是,如果不想打破双亲委派模型,使用findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。否则重写loadClass()方法。

2.5、双亲委派模型

双亲委派模型用来判断我们使用哪个类加载器来加载类。反过来说,就是类加载器ClassLoader使用委派模型来搜索类和资源。

模型要求顶层的启动类加载器除外,其余的类加载器必须有自己的父类加载器

另外,在查找类或资源之前,搜索类和资源的任务会委托给父类加载器

类加载器之间的父子关系一般不是以继承的关系来实现的,而是通过组合关系,来复用父加载器的代码。

  • 因为组合优于继承,多用组合少用继承。

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    //--》这里进入递归调用
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

结合上面的源码,简单总结一下双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)
    • 判断两个Java类是否相同,主要在两个方面:全类名 + 类加载器都一致。
  •  类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。

2.5、双亲委派模型好处

        双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

2.6、打破双亲委派模型

        自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

  • 因为类加载器进行类加载的过程中,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

比如 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。使得 Tomcat 可以加载不同Web应用下相同名的Servlet类。(Tomcat中可以运行多个Web应用程序,并且不会冲突)

3、执行引擎

细节参考链接《JVM之执行引擎

4、运行时数据区域

4.1、方法区

4.1.1、简介

  1. 有时候也称为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC
    在这里进行的GC主要是对方法区里的常量池和对类型的卸载,在java8之前,方法区的实现是永久代,java8之后,永久代被取消,元空间将其取代
  2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后
    的代码
    等数据。
  3. 该区域是被线程共享的。
  4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池
    具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

4.1.2、运行时常量池

        运行时常量池属于方法区的一部分。Class文件的类版本、字段、方法、接口等描述信息外,还有一项信息就是常量池表,用于存放编译期生成的各种字面量符号引用,这部分内用将在类加载后存放在方法区的运行时常量池中。 

        一般来说出来保存Class文件中描述的符号引用外,还会把符号引用翻译出来的直接引用也存储在运行时常量池中

        运行时常量池相对于Class文件常量池的另外一个重要特性就是具备动态性,Java语言不要求常量只有编译器才能产生,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得最多的时String类的intern()方法。

        运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

4.2、虚拟机栈

  • 虚拟机栈:也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
  • 虚拟机栈:是线程私有的,它的生命周期与线程相同。
  • 局部变量表:存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地
    址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
  • 操作数栈:的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
  • 栈帧:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用
  • 如果线程的请求的栈深度大于java虚拟机栈的深度将抛出StackOverflowError异常,如太深的递归容易导致此异常。
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError 

 5、本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。 一个Native Method就是一个java调用非java代码的接口。

6、

ava堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这
里创建,因此该区域经常发生垃圾回收操作。 

7、程序计数器

        内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内
存区域是唯一 一个java虚拟机规范没有规定任何OOM情况的区域。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值