jvm详解(一)—— jvm结构与运行时数据区

4 篇文章 1 订阅
2 篇文章 0 订阅

jvm结构与运行时数据区

1、jvm的基础认识

  ​ 定义: jvm是java程序的运行环境的一部分,也是最重要的一部分,它与操作系统直接交互,而操作系统能够操作计算机硬件,所以jvm能间接的操作计算机硬件。java代码通过编译器将其编译成字节码文件,jvm通过加载字节码文件,然后进行运行内存的申请与字节码文件中指令的解释,达到java程序运行的目的。

  特性:

  1. 跨平台。通过安装不懂平台的jvm虚拟机来实现跨平台性。

  2. 垃圾回收

  组成:

  1. 类加载器子系统
  2. 运行时数据区
  3. 执行引擎

2、jvm的位置关系

在这里插入图片描述


  jdk是有java的编译工具和jre组成,而jre是由一些基础类库和jvm组成。一个java程序先通过jdk中的编译工具将java代码编译成字节码文件,然后jre再结合一些基础类库,通过jvm加载到内存中进行程序的执行。

3、jvm的体系结构

在这里插入图片描述

  jvm主要由三部分组成:

  1. .类加载器
  2. 运行时内存区
  3. 执行引擎

  所以一个java程序的运行过程如下:一个java程序首先通过java编译工具将java程序编译成二进制字节码文件,也就是.class文件,然后jvm就开始产生作用了。jvm通过它的类加载器,将.class文件加载入内存中,通过执行引擎来解释.class文件中的指令,指令和数据在内存中按运行时内存区的结构进行放置,如果出现通过java实现不了的方法,就通过调用本地方法来实现对应的功能。一个java的程序最终就是通过类加载器、运行时内存区、执行引擎来进行实现运行。

4、jvm运行时数据区

4.1 程序计数器(PC Register)

  程序计数器是一块比较小的内存区域,它的作用是线程运行过程中的字节码指令的行号指示器,它记录着字节码指令下一个执行语句的地址。字节码解释器工作是就是通过改变程序计数器的值来获取到下一个执行指令的,在java程序中的分支、循环、函数调用、异常、线程恢复等都要依赖程序计数器来实现。每一个线程都会分配一个程序计数器,所以程序计数器是线程所私有的。

  程序计数器由三个特性:

	 1. 当程序是在执行java方法时,程序计数器中的值为下一条执行指令的虚拟地址。
	 2. 当程序执行的时Native方法时,程序计数器中的值为空。
	 3. 程序计数器是唯一不会发生OutMemoryError情况的区域。

  编写一个.java文件程序,然后通过javac将其编译成.class文件,生成对应的字节码文件,再通过javap -c **.class文件进行反编译,就会生成对应的字节码指令文件。如下图就是对应的java代码和对应的编译的字节码指令文件,java中的代码和对应的指令都是一一对应的,而程序计数器所做的就是去保存字节码指令文件中下一个需要执行的指令行号。

  • 在这里插入图片描述
    在这里插入图片描述

4.2 JVM虚拟机栈(JVM Stacks)

  在java程序的运行的时候,每创建一个新的线程时,jvm都会为该线程分配一个JVM虚拟机栈,该栈能够维护线程的运行状态。虚拟机栈是由一个个的栈帧所组成,而一个栈帧就代表着java程序中的一个方法,所以jvm在运行程序的时候,每要调用一个函数方法时,就会把该方法包装成一个栈帧,压栈进入到虚拟机栈中,当该方法执行完成后就把该方法所对应的栈帧进行出栈。一个栈帧由四部分组成:方法变量表、操作数栈、动态链接、方法出口。

  特点:

  1. 是线程私有的,生命周期与线程一致。
  2. 虚拟机栈不存在垃圾回收GC问题,但也存在内存溢出问题OOM。

  注意: jvm对于虚拟机栈的大小可以提供两种选择方式,分别是固定栈的大小和可动态扩展栈的大小。根据不懂得两种方式jvm会抛出不懂得异常:StackOverflowError和OutMeroyError。虚拟机栈的容量大小可以通过-Xss参数进行设置。

  • 当虚拟机栈的大小方式为固定值时,那虚拟机栈的容量大小在线程创建时进行独立选定,如果线程申请栈的容量大于java虚拟机所允许的最大容量时,jvm就会抛出StackOverflowError异常。

  • 当虚拟机的大小为可动态扩展大小时,扩展内存容量尝试申请动态扩展时没有足够的内存或者创建线程时没有足够的内存分配给虚拟机栈,则jvm就会抛出OutMeroyError异常。

​ ​   ​  下面来分别介绍虚拟机栈中的栈帧的四个组成部分的作用。

4.2.1 方法变量表

  方法变量表中所存储的就是在一个方法中的所定义得局部变量和函数参数变量,如果是八种基本数据类型,则在该表中所保存的就是对应局部变量的真实值,如果是引用类型的变量,则所保存的是对应对象的引用,也就是保存了对象的地址。方法变量表的最大容量在对java文件进行编译成class文件时就已经进行确定了。

  方法变量表中的容量是以变量槽为最小单位,每一个变量槽的大小为32b,对于数据类型byte、short、int、float、char、bool、引用类型可以用一个变量槽就可以存储,而对于double、long类型则需要两个变量槽来进行高位对齐方式存储。

4.2.2 操作数栈

  操作数栈是虚拟机所利用的一个工作区,在虚拟机执行指令的过程中,将所需要的数据通过压栈方式压入到操作数栈中,通过入栈、出栈来对相应的数据进行运算,然后再把最终的结果压入操作数栈中,等待下一条指令对最终结果的操作。总的来说操作数栈就是虚拟机用来对数据进行运算的一个中转站。

4.2.3 动态链接

  每个栈帧都会包含一个指向运行时常量池中该帧所属方法的引用。持有这个方法是为了支持在方法的调用中进行动态的链接。Class 文件的常量池中有大量的符号引用,也就是一些字符串,这些字符串代表着一些具体的类、方法、返回类型等,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态链接。动态链接其实就是在一个方法的运行过程当中,可能会需要调用一些还没有加载到内存中的类对象或者方法,所以在这运行过程中,通过去运行常量池中查询需要加载的类对象和方法是那些,最后再通过JIT将这些类对象和放法加载进内存中。

4.2.4 方法出口

  保存了方法的返回地址,即存放调用该方法的PC寄存器中的值,用于恢复调用者的方法变量表、操作数栈的信息。本质上方法出口就是将当前栈帧从虚拟机栈中进行出栈的过程,并同时恢复调用者的方法变量表、操作数栈并同时设置PC寄存器中的值,让调用者继续执行下一条指令。

  方法的返回方式有两种,一种是正常退出,另一种的发生异常,无论是以何种方式介绍该方法,程序都会返回到调用该方法的被调用位置。当正常退出该方法时,返回的就是调用者的程序计数器中的值,也就是下一条执行指令的地址;如果是发生异常,则由异常表来确定返回地址,栈帧中不保存异常的任何信息。

4.3 本地方法栈(Native Method Stacks)

  本地方法栈和虚拟机栈一样,都是将方法通过栈帧的方式进行方法的调用,但区别就是它们作用的方法类型不一样,虚拟机栈服务的是java方法,而本地方法栈服务的是native方法。所以本地方法栈也会抛出 StackOverflowError 异常和 OutOfMemoryError 异常。

  本地方法不是由java来进行实现的,而是通过c语言来进行实现。

4.4 堆(Heap)

4.4.1 堆的定义

  堆是存储java引用对象实例的内存区域,只要在java中的对象是通过new关键字所产生,则都是储存在堆中。当产生一个新的对象实例时,将此对象存储在堆中,而在jvm虚拟机栈中则保留着该对象的引用地址。由于jvm虚拟机栈、程序计数器、本地方法栈都是线程所私有的,所以这些资源随着线程的关闭而释放,而堆则需要垃圾回收算法来进行释放,所以堆是垃圾回收的主要场所。

  堆的特点:

 1. 堆是所有线程共享的,所以存在线程安全问题。
 2. 堆具有垃圾回收机制。
4.4.2 堆的组成

  堆由两部分组成,分别是新生代和老年代,而新生代又分为伊甸园区(Eden)、幸存0区、幸存1区。

​ 注意:其实在逻辑上,堆是分为三个区域,分别是新生代、老年代、方法区,但是在物理上方法区和新生代与老年代分开的。而对于方法区的实现,在jdk1.8以前是通过永久代的方式来实现;而在jdk1.8以后,则是通过元空间的方式来进行实现。元空间与永久代唯一的区别就是元空间并不在jvm中,而是才是本地内存的方式。

在这里插入图片描述
  新生代中的内存大小比列大概为Eden:from:to = 8:1:1,通过参数-Xms可以设置新生代和老年代的初始内存大小,-Xmx设置他们的最大内存容量;对于jdk1.8以前的永久区,可以用-XX:PermSize设置永久区的初始大小,-XX:MaxPermSize设置最大容量。对于jdk1.8以后的元空间区,分别用 -XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间区的内存大小。

在这里插入图片描述

4.4.3 堆的垃圾回收策略

  在java程序中,新创建的对象首先会存放在新生代中的Eden区,而当Eden区的内存占满时jvm就会发生一次Minor GC ,对新生代的区域进行一定的垃圾回收,具体的过程是通过Minor GC算法释放掉不需要的对象,然后把存活的对象复制到幸存区,然后再将Eden区清空。对于幸存区中的from 区和to区,to区总是为零对象的区域,而from区是有着上次从Eden中复制过来的一些存活对象的区域,当发生Minor GC时,会释放掉Eden区和from区无用的对象,再把Eden区的存活对象复制到from区,如果from区内存没占满,则清空掉Eden区的内存区域就结束。而当from区内存满了的时候,from区中的对象也会像Eden区中的对象一样,一部分被释放,另一部分被复制到to区,然后同样的清空from区域。也就是说,当发生一次Minor GC时,会通过垃圾回收算法将Eden与from区 中的无用对象释放,而将有用对象复制到to区,再将Eden和from区进行清空,剩下的对象都移动到了to区。且此时,to区就成为了下一次Minor GC的from区,被清空的from区又变成了下一次Minor GC的to区。所以幸存区中的from区和to区是一直在交换的。而对于新生代中的对象,jvm会给他们分配一个年龄计数器,每次在Minor GC中存活一次,它的值就会加1,当它的值达到15时,该对象就会被复制到老年代中。Minor GC的执行时间比较短,而且再执行过程中线程依然在运行。

  当进行Minor GC时,jvm会将存活了16次的对象复制到老年代中,还有一些大对象也会被复制到老年代中。而当老年代的区域装满以后就会发生一次Major GC,对老年代的区域进行垃圾回收操作。进行Major GC所需要耗费的时间较多,而且同时会停掉除了垃圾回收线程以外的所有线程,所以如果平凡的进行Major GC垃圾回收就会使得程序的运行速度较低。jvm调优的目的就是为了去减少Major GC的执行时间和它的执行次数。

​  幸存区存在的意义: 如果没有幸存区作为Eden区和老年代的缓存区域,则当Eden区进行一次Minor GC垃圾回收时就会把存活的对像复制到老年代中,而这些对象很大部分可能存活周期本就不长,再进行几次的Minor GC垃圾回收时就会被清除掉。这样就会造成老年代很快就会填满,使得老年代触发Major GC,而一般情况下老年代的内存空间远大于新生代,这样执行一次Major GC的时间也远大于执行一次Minor GC,使得耗费时间增多了,显然频繁的触发Major GC对于程序的执行速度会造成很大的影响。

  幸存区分为两个区的意义: 主要就是为了更加高效的解决内存碎片化的问题。如果幸存区只有一个区,那当进行Minor GC时就会将Eden区和幸存区的一些对象释放掉,从而幸存区中可能就会产生一些较小且稀疏的内存空间块,当Eden区中的对象复制到幸存区时这些空间块又不足以存放对象,就造成了内存空间的浪费,所以如果只有一个区,内存碎片化的问题会比较严重,造成内存空间的浪费。通过使用两个区,每一次都将幸存的对象复制到零对象的to区,让所有对象按照顺序区占据to区的内存空间,就能很高效的解决内存碎片化的问题。

4.4.4 方法区

  jvm的方法区是存储静态变量、常量和类的信息(构造函数、接口定义、方法)的内存区域。

  JDK8以后,一般方法区就由类型信息、域信息、方法信息、运行时常量区和JIT代码缓存所组成。类型信息存储了加载的类的一些基本信息,包类名、报名、父类、修饰符等;而方法信息则存储了方法的名称、方法参数、返回类型等信息。

  特点:

  1. 和堆一样都是线程共享的。
  2. 方法区的大小决定了系统可以保存多少类,如果系统定义的类较多,超过了方法区的存储容量,就发发生OutOfMerroyError的异常。
  3. 方法区的内存容量同堆一样也可以选择固定大小模式和可以动态扩展模式。
  4. 方法区的生命周期同jvm的生命周期一致,随着jvm的开始运行而创建,随着它的关闭而释放。

  运行时常量池: 运行时常量池是方法区的一部分,是存储类中的字面量和符号引用表的地方。一个Class字节文件中包含了类的基本信息和对应的方法、字段的信息,还包括一个常量池(Constant pool)。当jvm将Class字节文件加载如内存中时,就会把常量池的内容加载到运行时常量区中。所以一个类就对应着一个运行时常量池区域。

  运行时常量池主要分为两大部分:字面量和符号引用,其中字面量就是文本字符串、常量和静态变量等,而符号引用就是包括字段的名称和描述、方法的名称和描述、类的完全限定名等。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值