跟着锋哥学Java

深入浅出JVM(一)之JVM的体系结构概述

1.JVM的体系结构概述

Jvm的体系结构由类装载器子系统,方法区,java栈,本地方法栈,堆,程序计数器,执行引擎,本地方法接口以及本地方法库等组成

1.1JVM的位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互.

1.2类装载器(ClassLoader)

   1. 负责加载class文件,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,ClassLoader只负责class文件的加载

   至于它是否可以运行,则由ExecutionEngine(执行引擎)决定 ,class文件在文件开头有特定的文件标示(32字节用16进制表示是"cafebabe")

  2.类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。

  3.类装载器(ClassLoader)详情请看:深入浅出JVM(二)之类加载器 章节

 

 

 

 

 

 

1.3堆内存(Heap)

   1. 堆内存是java内存中的一种,它的作用是用于存储java中的实例对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放,类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行

   2.类装载器(ClassLoader)详情请看:深入浅出JVM(三)之堆内存(Heap)章节

   

 1.3方法区

   1.供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。

   2.在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)

   3.对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(ParmanentGen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走

1.3.1.设置方法区大小与 OOM

-XX:MetaspaceSize:设置元空间内存大小,比如-XX:MetaspaceSize=100m
-XX:MaxMetaspaceSize:设置元空间最大内存,比如-XX:MaxMetaspaceSize=100m

 1.3.2 方法区内部结构包含一下信息

    1. 类信息:类的完整有效名称(包名.类型),类的直接父类的完整有效名,类的修饰符(public,abstract,final 等),类实现的接口的有效列表以及字段信息

    2.运行时常量池,静态变量,即时编译器编译后的代码缓存

    3.方法信息、类加载器的引用、对应class实例的引用

    4.实际而言,方法区(MethodArea)和堆一样,是各个线程共享的内存区域,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

    5.实例变量存在堆内存中,和方法区无关

    6.当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

1.3.3运行时常量池

  1.运行时常量池是方法区的一部分

   2.常量池表是 class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区中的运行时常量池中

   3.在加载类和接口到虚拟机后就会创建对应的运行时常量池

   4.JVM 为每个已加载的类型都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的

   5.运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用,此时不再是常量池中的符号地址了,这里替换为真实地址

   6.运行时常量池类似于传统编程语言中的符号表,但它所包含的数据却比符号表要更加丰富一些

   7.运行时常量池会抛出 OOM 异常

1.3.4 方法区在 jdk6、jdk7、jdk8 

   1.jdk6:有永久代,静态变量存放在永久代上

    2.jdk7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中

   3.jdk8:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中

1.4 JVM栈 (Java Virtual Machine Stacks)

 1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

 2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

 3. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分

   4.虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态,换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建.随着线程停止而消失。

  5.每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出

  

 1.4.1栈帧

   1.栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程;

也就是说 要执行一个方法,将该方法的栈帧压入栈顶,方法执行完成其栈帧出栈

  2.最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。

  3.栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间,栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。

  4.每个方法在执行的同时都会创建一个栈帧,每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

  5.在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的,并写入到方法表的code属性中。

   6.操作数栈:以压栈和出栈的方式存储操作数的

 

 1.4.2局部变量表

1.局部变量表也被称之为局部变量数据组或本地变量表 ;方法中定义的局部变量以及方法的参数存放在这张表中

2.主要用户存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress 类型

3.由于局部变量表示建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

4.局部变量表所需要的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximun local variables数据项中,在方法运行期间时不会改变局部变量表的大小的

5.方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多(递归),对于一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占更多的栈空间,导致其嵌套调用次数就会减少

6.局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈的销毁,局部变量表也会随之销毁

 1.4.3变量槽(Variable Slot)

     1.局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。

  2.对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写

     3.复用:为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用

    4.优点:节省栈帧空间,缺点:影响到系统的垃圾收集行为。

   5.大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存

1.4.4动态连接

 1.每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

 2.在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析

 3.另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接

 1.4.5法出口

  1.当一个方法开始执行后,只有2种方式可以退出这个方法 :

    方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。

    异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。

  2.无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

  3.一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

 1.5本地方法栈(Native Method Stack)

   Native本地方法栈主要是调底层的C语言的函数库。

   2.本地方法本质上是依赖于虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法。

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

 4.如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。

1.6程序计数器

1.每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码。

2.用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

3.它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

4.简单的讲,程序计数器就是记录了程序内部的运行流程和跳转顺序。例如:就是排版值日表的顺序

5.如果正在执行的是Native方法,则这个计数器为空。

 

 2.堆的指向

   2.1栈指向堆  

     1.如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象

          

 

 

 

  

 

 

 

 

  2.2 方法区指向堆

       方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。 例如:private static Object obj=new Object();

        

 

 

 

 

 

 

 

 

 

    2.3堆指向方法区

     方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?

      

 

posted on 2022-03-22 20:31  跟着锋哥学Java  阅读(214)  评论(0编辑  收藏  举报

导航