1. 概述
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙里面的人却想出来。
与C、C++开发人员在内存管理领域既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任所不同的是,对Java程序员来说,我们把对内存控制的权利交给了JVM。在JVM的帮助下,不容易出现内存泄漏和内存溢出的问题,但是相应的,一旦出现内存泄漏和内存溢出的问题,如果不了解JVM的内存管理机制,那么将很难定位和解决问题。
2. Java虚拟机运行时内存模型
JVM在执行Java程序的时候会把它管理的内存划分为若干个不同的数据区域,如图: jvm运行时数据区.png2.1 程序计数器
- 定义:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
字节码解释器工作原理:通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 特点:
- 程序计数器是线程私有的,每条线程都有一个程序计数器,各条线程之间计数器互不影响,独立存储。
- 如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址
- 如果线程正在执行的是一个Native方法,计数器值则为空。
- 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.2 Java虚拟机栈
- 定义:即我们平时所说的栈内存,是Java方法执行的内存模型
- 每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本数据类型、对象引用类型和returnAddress类型,一个局部变量空间是32位长度。long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。
- 特点:
- 也是线程私有的,生命周期与线程相同
- Java虚拟机规范中,对这个区域规定了两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果虚拟机栈可以动态扩展,但扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2.3 本地方法栈
- 与虚拟机栈类似,虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。
- 本地方法栈也是线程私有的,也会抛出StackOverflowError和OutOfMemoryError异常。
2.4 Java堆
- 用于存放几乎所有的对象实例和数组。
- 是Java虚拟机所管理的内存中最大的一块。
- 是被所有线程共享的一块内存区域,在虚拟机启动时创建。
- 是垃圾收集器管理的主要区域,也被称作“GC堆”。
- 可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
- 实现时,可以实现成固定大小的,也可以是可扩展的,主流的都是按照可扩展实现的。
- 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出OutOfMemoryError。
2.5 方法区
- 是线程共享的内存区域
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 和Java堆一样不需要连续的内存、可以选择固定大小或者可扩展。除此以外还可以选择不实现垃圾收集。
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
2.6 运行时常量池
- 方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 相对于Class文件常量池的另外一个重要特征是具备动态性,也就是说并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
3. HotSpot虚拟机对象探秘
3.1 对象的创建
- 类加载检查:检查这个new指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
为新生对象分配内存,根据Java堆中内存是否是绝对规整的可以分成两种:
- 是绝对规整的:所有用过的内存都放到一边,空闲的内存放在另一边,中间放着一个指针做为分界点的指示器。当分配内存的时候就是把指针向空闲空间那边挪动一段与对象大小相等的距离。这种分配方式被称为“指针碰撞”。
- 不是绝对规整的:虚拟机需要维护一个列表,记录哪些内存块可用。在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。这种分配方式被称为“空闲列表”。
Java堆是否规整是由所采用的垃圾收集器是否带有压缩整理功能决定的。所以使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,采用的是空闲列表。
如何解决线程安全问题
- 对分配内存空间的动作进行同步处理
- 把内存分配的动作按照线程划分在不同的空间之中进行。
- 设置对象头:对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
- 把对象进行初始化
3.2 对象的内存布局
对象在内存中存储的布局可以分为3块区域:
-
对象头(Header):包括两部分信息:
- Mark Word:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针:用来确定这个对象是哪个类的实例。
- 对齐填充:占位符的作用。
3.3 对象的访问定位
-
访问方式:
-
使用句柄访问:在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的具体地址信息。
通过句柄访问对象.jpeg -
使用直接指针访问:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
通过直接指针访问对象.jpg
-
使用句柄访问:在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的具体地址信息。
-
好处
- 使用句柄访问:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
- 使用直接指针访问:速度快,节省了一次指针定位的时间开销。
网友评论