【JVM篇】【JVM的组成】

作者: 蚂蚁的心脏 | 来源:发表于2021-10-18 15:16 被阅读0次

    【JAVA】【JVM篇】【JVM的组成】

    来自二线的码农笔记,用自己的理解总结知识点,互相学习

    1. JVM概念

    JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

    2. JVM作用

    引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言的可移植性正是建立在Java虚拟机的基础上(不同系统安装不同版本JVM)。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

    3. JVM(主要)组成部分

    1. 程序计数器 : 记录当前线程所执行到的字节码行数
    2. 虚拟机栈:存放方法运行时候的栈帧
    3. java堆:存储对象实例,好比new出来的都在堆里
    4. 本地方法栈: JVM调用本地方法,提供Native服务
    5. 方法区: 存储运行时候常量池、已被虚拟机下载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    image.png

    4. JVM(主要)组成部分详解

    1.程序计数器

    PC寄存器(程序计数器)用来存储指向下一条指令的地址,也即将要执行的指令代码。CPU来回切换线程后能够更好的知道接下里该执行哪条指令, 由执行引擎读取下一条指令。

    image.png
    代码展示
    public class PCRegisterTest {
        public static void main(String[] args) {
              int i = 10;
              int j = 20;
              int k = i + j;
         }
      }
    

    然后将代码进行编译成字节码文件,我们再次查看 ,发现在字节码的左边有一个行号标识,它其实就是指令地址,用于指向当前执行到哪里。

    0: bipush        10
    2: istore_1
    3: bipush        20
    5: istore_2
    6: iload_1
    7: iload_2
    8: iadd
    9: istore_3
    10: return
    
    

    通过程序计数器我们就可以清楚的知道程序执行到哪一步了.

    那程序计数器存储字节码指令地址到底有什么用呢?

    因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
    JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。

    程序计数器为什么被设定为私有的?

    我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法。自然是为每一个线程都分配一个程序计数器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况

    2. 虚拟机栈

    虚拟机栈的栈元素是栈帧,当有一个方法被调用时,代表这个方法的栈帧入栈;
    当这个方法返回时,其栈帧出栈。因此,虚拟机栈中栈帧的入栈顺序就是方法调用顺序。虚拟机栈的主要作用反应程序调用方法的顺序【先进后出原则】

    何为栈帧?

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。每个方法被调用会生成一个新的栈帧进栈。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    image.png
    白话文谈谈先进后出原则

    学过数据结构的小伙伴们应该都知道栈的特点。那我们该怎么去理解呢?
    拿APP页面举个例子, 比如说你打开美团【APP首页】(A),这个时候我想打开【外卖页】(B)【点个外卖】(C)。这样的话就是A调用B,B调用C。那我想关闭外卖页,我想找个娱乐场所放松一下心情,那么我们就必须先返回外卖页,再返回到APP首页,再去找场所。这样就是先关闭C再关闭B。C是最后进去的却被先关闭了。 这样去理解先进后出原则。【建议大家百度一些先进后出案例】

    每个栈帧的存储
    1. 局部变量表: 局部变量表也被称之为局部变量数组或本地变量表, 主要用于存储方法参数和定义在方法体内的局部变量。局部变量表的大小问题是在编译时期就被确定。
    2. 操作数栈: 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression stack)。操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
    3. *动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。由于篇幅有限这里不再继续讨论解析与分派的过程,这里只需要知道静态解析与动态连接的区别就好。
    4. 方法的返回值:执行引擎遇到任意一个方法返回的字节码指令:传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。如果存在异常,导致方法退出,是不会给上一级返回值的。无论哪一种方式退出都会返回方法的被调用的位置

    3. 虚拟机堆

    JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
    年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
    堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
    非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
    转载文章

    JAVA堆内存管理是影响性能主要因素之一。
    堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的。
    先看下JAVA堆内存是如何划分的,如图:


    image.png

    在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

    元空间有注意有两个参数:
    MetaspaceSize :初始化元空间大小,控制发生GC阈值
    MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

    为什么移除永久代?

    移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
    有了元空间就不再会出现永久代OOM问题了!

    分代概念(每个区的作用)

    新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
    老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
    Minor GC : 清理年轻代
    Major GC : 清理老年代
    Full GC : 清理整个堆空间,包括年轻代和永久代
    所有GC都会停止应用所有线程。

    为什么分代(年轻代和老年代)?

    将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

    为什么会堆内存溢出?

    在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。
    OOM(Out of Memory)异常常见有以下几个原因:
    1)老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
    2)永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
    3)代码bug,占用内存无法及时回收。

    4.本地方法栈

    对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。通俗的说就是程序在执行的过程中会使用到一些非Java语言实现的方法(例如:调用本地打印机的方法),这个时候就会用到本地方法栈。

    当一个线程调用JAVA方法和本地方法时的栈图如下


    image.png

    当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

    5.方法区

    方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    方法区与堆一样,是各个线程共享的内存区域。方法区在 JVM 启动的时候被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的。空间大小可选择固定或者可扩展。
    方法区的大小决定了系统可以保存多少个类,如果系统定义了太多类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError(PermGen space)或者 java.lang.OutOfMemoryError (Metaspace)

    image.png
    运行时常量池

    是方法区的一部分。存储:数量值、字符串值、类引用、字段引用、方法引用。
    常量池表示 Class 文件的一部分,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池在加载类和接口道虚拟机后,就会常见对应的运行时常量池。
    JVM 为每个已加载的类型 (类或接口) 都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
    运行时常量池中包含多种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。运行时常量池,相当于 Class 文件常量池的另一重要特征:具备动态性(String.intern())
    当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则 JVM 会抛 OutOfMemoryError 异常。

    部分概念比较模糊,可自行百度

    相关文章

      网友评论

        本文标题:【JVM篇】【JVM的组成】

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