您的位置:首页 > 编程语言 > Java开发

java虚拟机简介,JVM运行时数据区域(jvm内存模型)

2020-07-14 06:26 525 查看

java虚拟机 JVM(java virtual machine

  • 直接内存(非虚拟机运行数据区)
  • 什么是JVM(简单介绍一下JVM)

    虚拟机指的是抽象化的计算机,通过运行在实际计算机上来仿真模拟各种计算功能,而java虚拟机也是如此,它有自己完善的硬体结构,如处理器、堆栈、寄存器等,还有相应的指令集系统。java作为跨平台语言之所以实现了一次编写,到处执行的,其中离不开java虚拟机的帮助。因为java虚拟机它屏蔽了底层操作系统相关的信息,程序只需要生成.class文件的字节码,放在java虚拟机上运行即可,至于如何和底层平台沟通合作,是java虚拟机的事情。


    在java程序开发阶段,我们编写的是以java为后缀的源代码,当使用源码编译器后,这些文件会生成对应的class文件,class后缀的文件是二进制表示的字节码。运行java程序时,实际上是操作系统执行了一个java虚拟机进程,当它启动时,会根据需要将初始类(class后缀的字节码)加载进内存,对这个类进行初始化和动态链接。虚拟机执行引擎从某个类的main方法开始执行,解释class字节码指令,将这些指令翻译成本机硬件可以识别的指令,最后在CPU中运行。

    jvm运行时数据区域

    虚拟机的执行,必须加载所需的class字节码文件,并且执行文件中的字节码指令。它做这些操作,肯定需要内存空间,打个比方,人消化食物,也是需要先把东西存入胃中。虚拟机也需要内存空间来存放运行过程中的数据。java虚拟机在执行java程序过程中,会把它所管理的内存划分为若干个不同的数据区域。这些数据区域各自有各自的用途,java虚拟机规范规定,java虚拟机所管理的内存包括以下几个运行时数据区域。

    程序计数器(Program Counter Register)

    程序计数器是一个比较小的内存空间,用来记录当前线程所执行的字节码的行号指示器。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器就是通过改变程序计数器的值来选取下一条要被执行的字节码指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,因为程序计数器记录了当前线程执行的位置,当线程切换回来的时候就可以让线程恢复到原来正确的执行位置。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。因为每个线程都需要一个专门记录自己执行位置的程序计数器,所以程序计数器是线程私有的内存。(线程私有即各条线程之间的这一块内存是独立村存储,互不影响的)

    虚拟机栈(VM Stack)

    java虚拟机栈依然是线程私有的,生命周期和线程相同,它是描述JAVA方法运行过程中的内存模型:每个方法被执行的时候,java虚拟机会同步创建一个栈帧用于存储方法运行时候需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等。当方法执行结束时,这个方法对应的栈帧就会从虚拟机栈中出栈。

    这个内存区域会出现两种异常状况:如果线程请求的栈深度大于事先指定的栈的最大深度,将抛出StackOverflowError异常;如果栈扩展无法申请到足够的内存会抛出OutOfMemoryError异常。

    局部变量表中存放了虚拟机八种基本数据类型,(boolean、byte、short、int、long、float、double、char)和对象引用(reference类型,它不是对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    操作数栈的大小也是在编译期就完成分配的,操作栈中每一个元素都可以是包括long和double在内的任意数据类型。32位数据类型所占栈容量为1,64位数据类型所占的栈容量为2。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区,主要用于局部变量的运算和传递。

    动态链接:class文件中存在着大量的符号引用,字节码中的方法调用指令就是以常量池里指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用使用的时候转化为直接引用,这种转化称为静态解析。而另一部分将在每一次运行期间都转化为直接引用,这部分称为动态链接。

    方法返回地址:方法只有两种方式退出,第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这种退出方法成为正常调用,当方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能保存的就是这个计数器值。而另一种退出方式是执行过程中遇到了异常,并且这个异常并没有在方法体中处理,找不到异常对应的处理器导致了方法退出,这种退出方法称为异常调用完成,返回地址要通过异常处理器表来确定,栈帧中一般不保存这部分信息。

    本地方法栈(Native Method Stack)

    本地方法栈结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法(指的是通过其他语言实现的与处理器相关的方法)的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError。

    Java堆(Heap)

    java堆是虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例和数组都在堆上分配,字符串常量池在java 7 后也移到了堆中。

    java堆是垃圾收集器的管理的主要区域,从内存回收的角度看,因为现在的收集器都是采用了分代收集理论设计的,所java堆可以细分为:新生代,老年代,新生代又可分为Eden空间,From Survivor空间,To Survivor空间。如果从内存分配的角度看,线程共享的java堆中可以分出多个线程私有的分配缓冲区,(Thread Local Allocation Buffer,TLAB)。无论如何划分,存储的内容都是对象实例和数组。将他们细分只是为了更好的回收和分配内存。

    Java堆上可以物理上不连续, 只要逻辑上是连续的即可(当空间中不连续的空间越来越多,可能会发生明明剩余空间大于需要申请的空间却申请失败), 。 当前主流的虚拟机都是按照可拓展来实现的( 可设置-Xms 初始化堆, -Xmx 最大堆空间), 如果在堆中没有内存完成实例分配, 并且堆也无法在拓展时, 将会抛出OutOfMemoryError异常。

    新生代(年轻代):新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区。Eden空间不足时会把存活的对象转移到Survivor(实际上使用的是垃圾回收算法中的标记-复制算法,具体怎么转移,需要了解回收算法的执行过程)。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。

    老年代:存放经过多次垃圾回收仍然存活的对象。

    永久代:实际上是为了将垃圾分代设计扩展到方法区,而取的称呼,本质上可以理解成使用永久代来实现方法区。存放的其实就是方法区中存放的数据。
    在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

    方法区(Method Area)

    方法区也是线程中共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap非堆, 目的是与Java Heap 区分开来。
    如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
    在JDK1.7以前HotSpot虚拟机使用永久代来实现方法区,永久代的大小在启动JVM时可以设置一个固定值(-XX:MaxPermSize),不可变;
    在JDK1.7中 原本存放在永久代的字符常量池移到堆中。但永久代并没有完全移除。
    JDK1.8中进行了较大改动

    1.移除了永久代(PermGen),替换为元空间(Metaspace);
    2.永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
    3.永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
    4.永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

    运行时常量池

    运行时常量池(Runtime Constant Pool) 是方法区的一部分(虚拟机规范)。 Class文件中除了有类的版本, 字段,方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中。
    运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, Java语言并不要求常量一定只能在编译期产生, 也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 这种特性被开发人员利用的比较多的便是String类的intern() 方法。

    直接内存(非虚拟机运行数据区)

    在JDK1.4 中新加入的NIO 类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O 形式,他可以使用Native 函数直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场所显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。
    直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现。 显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制。 服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常。

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: