重识JVM(一):运行时数据区域

记得上一次看jvm相关的知识还是在大四的时候,两年过去了,记忆已经逐渐模糊。现在来重识一下jvm,希望温故而知新,一些重要的知识点我会在博客上把学习的知识记录下来。

想要了解jvm,就一定需要了解java虚拟机是如何使用内存的,要不在出现内存泄漏和内存溢出等问题时,我们是没法准确地排查出错误的,下面就让我一起来重新认识一下java虚拟机在执行java程序的过程中的运行时数据区。

参照上图,我们来进一步了解一下运行时数据区中各个区域的作用和详细情况。

一.程序计数器

1.从最简单的程序计数器说起,程序计数器就是当前线程执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选去下一跳需要执行的字节码指令, 分支, 循环, 跳转, 异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

2.每条线程都需要有一个独立的程序计数器, 各条线程之间的计数器互不影响, 独立存储, 我们称这类内存区域为"线程私有内存"。

3.如果线程正在执行的是一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法, 这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

二.java虚拟机栈

1.与程序计数器一样, Java虚拟机栈也是线程私有的, 它的生命周期与线程相同

2.每个方法被执行的时候都会同时创建一个栈帧用于存储 局部变量表, 操作栈, 动态链接, 方法出口等信息.。每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

3.局部变量表存放了编译期可知的各种基本数据类型(Boolean, byte , char, short, int, float , long , double), 对象引用(reference类型, 它不等同于对象本身, 根据不同的虚拟机实现, 他可能是一个指向对象起始地址的引用指针, 也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

4.如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常; 如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态拓展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当拓展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

三.本地方法栈

本地方法栈和虚拟机栈发挥的作用十分相似。同样是线程私有,它们之间的区别不过是虚拟机栈为Java 方法服务,而本地方法栈为虚拟机使用到的Native 方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError 异常和OutOfMemoryError 异常。

四.堆

1.Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存。 这一点在Java虚拟机规范中描述的是: 所有的对象实例以及数组都要在堆上分配, 但是随着 JIT编译器的发展与逃逸分析技术(通过分析若一个对象没有逃逸出一个方法,那么该对象在栈上分配空间,该对象随着栈的销毁而销毁)的逐渐成熟, 栈上分配, 标量替换优化技术(将部分字段使用标量存储)将会导致一些微妙的变化发生, 所有的对象都分配在堆上也逐渐变得不是那么"绝对"了

2.Java 堆是垃圾收集器管理的主要区域, 因此很多时候也被称做"GC堆"(Garbage Collected Heap), 如果从内存回收的角度看, 由于现在收集器基本都是采用的分代收集算法, 所以Java堆中还可以细分为: 新生代和老年代; 新生代中有Eden空间, From Survivor空间, To Survivor空间。堆空间内存分配(默认情况下)老年代 : 三分之二的堆空间,新生代代 : 三分之一的堆空间,eden区: 8/10 的新生代空间,From Survivor : 1/10 的新生代空间,To Survivor: 1/10 的新生代空间。

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

五.方法区

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

2.在JDK1.7以前HotSpot虚拟机使用永久代来实现方法区。

3.在JDK1.7中 存储在永久代的部分数据就已经转移到Java Heap或者Native memory。譬如符号引用(Symbols)转移到了native memory,原本存放在永久代的字符常量池移出。但永久代仍存在于JDK 1.7中,并没有完全移除。

4.JDK1.8中进行了较大改动,在Java8 中,永久代被删除,方法区的HotSpot 的实现为Metaspace 元数据区,不放在虚
拟机中而放在本地内存中,存储类的元信息;而将类的静态变量(放在Class 对象中)和运行时常量池放在中。

六.运行时常量池

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

七.直接内存

1.在JDK1.4 中新加入的NIO类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O 形式,他可以使用Native 函数直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场所显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

2.直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现。 显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制。 服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常。

参考资料:《深入裂解java虚拟机》以及jdk1.8相关信息。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章