java虚拟机--内存区域

一、运行时数据区域

一个基本的JVM运行时内存模型如下所示

二、程序计数器 

1.概念

程序计数器( Program Counter Register )是一块较小的内存空间,它主要是用于记录字节码的执行到的位置。在Java 虚拟机的概念模型里 ,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。

2.程序计数器的特点

(1)线程隔离性,每个线程工作时都有属于自己的独立计数器。
(2)执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址
(3)执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。(4)程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。

(5)程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

三、java虚拟机栈

1.概念

与程序计数器一样, Java 虚拟机栈( Java Virtual Machine Stack )也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的线程内存模型:每个方法被执行的时候, Java 虚拟机都会同步创建一个栈帧 Stack Frame )用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

2.栈帧栈帧组成

(1)局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址),总而言之就是基本数据结构,和局部变量的引用存在栈中的局部变量表中

局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、int、float等。对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。

为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的, 也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕), 那这个变量对应的Slot就可以交给其他变量使用。可以节省栈帧空间。 但是会影响到系统的垃圾收集行为如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。

(2)操作数栈

用于储存字节码指令

(3)动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。这里简单提一下动态连接的概念,后面在详细讲解.

(4)方法出口

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

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

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

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

   一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。

  而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

3.可能发生的异常:

1.OutOfMemoryError:当栈扩展时无法申请到足够的内存会抛出
2. StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度会抛出

四、本地方法栈

本地方法栈( Native Method Stacks )与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机
栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地( Native ) 方法服务。

五、Java堆

1.概念

对于 Java 应用程序来说, Java 堆( Java Heap )是虚拟机所管理的内存中最大的一块。 Java 堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,不过 随着 Java 语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换 [2] 优化手段已经导致一些微妙 的变化悄然发生,所以说Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。

2.堆内存的分配

(1)回收内存的角度

Java 堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作 “GC堆”  从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名
(2) 从分配内存的角度
如果从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区
Thread Local Allocation Buffer TLAB ),以提升对象分配时的效率。
不过无论从什么角度,无论如 何划分,都不会改变Java 堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

3.异常

Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩 展来实现的(通过参数-Xmx -Xms 设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

六、方法区

1.概念

方法区( Method Area )与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载
的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《 Java 虚拟机规范》中把 方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“ 非堆 Non-Heap ),目的是与 Java 堆区分开来

2.运行时常量池

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

3.存放的内容

(1)类型信息

类的完整名称(比如,java.long.String)
类的直接父类的完整名称
类的直接实现接口的有序列表(因为一个类直接实现的接口可能不止一个,因此放到一个有序表中)
类的修饰符 

(2)类型的常量池 (即运行时常量池)

每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;

字面值:就是像string, 基本数据类型,以及它们的包装类的值,以及final修饰的变量,简单说就是在编译期间,就可以确定下来的值;

符号引用:不同于我们常说的引用,它们是对类型,域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用;

存在这里面的数据,类似于保存在数组中,外部根据索引来获得它们 

(3)字段信息

  • 声明的顺序
  • 修饰符
  • 类型
  • 名字

(4)方法信息

  • 声明的顺序
  • 修饰符
  • 返回值类型
  • 名字
  • 参数列表(有序保存)
  • 异常表(方法抛出的异常)
  • 方法字节码(native、abstract方法除外,)
  • 操作数栈和局部变量表大小

(5)类变量(即static变量) 

非final类变量

在java虚拟机使用一个类之前,它必须在方法区中为每个非final类变量分配空间。非final类变量存储在定义它的类中;

final类变量(不存储在这里)

由于final的不可改变性,因此,final类变量的值在编译期间,就被确定了,因此被保存在类的常量池里面,然后在加载类的时候,复制进方法区的运行时常量池里面 ;final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用;

(6)对类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

7)对Class类的引用

jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

(8)方法表

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构,也就是方法的代码逻辑

七、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

JDK 1.4 中新加入了 NIO New Input/Output )类,引入了一种基于通道( Channel )与缓冲区
Buffer )的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java 堆和 Native 堆中来回复制数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值