Java内存区域与对象创建过程
一、java内存区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(JavaSE7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1.程序计数器(线程私有)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
每条线程都有一个独立的程序计数器,各个线程之间互不影响,独立储存。
如果线程正在执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
如果线程正在执行的是一个Native方法,计数器值为空(undefined)。
此内存区域是唯一一个在Java规范中没有规定任何OutOfMemoryError(OOM)情况的区域
2.Java虚拟机栈(线程私有)
虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
通常大家所说的虚拟机堆内存和栈内存中,栈内存一般是指这里的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型、对象引用(不是对象本身,可能是指向对象的指针或代表对象的句柄或其他的与此对象相关的位置,涉及到下文的“对象的访问定位”)和returnAddress类型。
3.本地方法栈(线程私有)
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,区别不过是虚拟机栈为Java方法(字节码)所服务,而本地方法栈则为虚拟机使用的Native方法服务。Java虚拟机规范没有对本地方法栈使用的语言、方式和数据结构有强制规定。因此虚拟机可以自由实现它,甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
4.Java堆(线程共享)
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。唯一的目的就是存放对象实例。所有的对象实例和数组都要在堆上分配内存。
Java堆是垃圾收集器管理的主要区域。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,类似于磁盘空间。在实现时既可以是固定大小,也可以是可扩展的,当前的主流虚拟机都是按照可扩展实现的。如果在堆中没有内存完成实例化分配,并且堆也无法扩展时,会抛出OOM异常。
5.方法区(线程共享)
方法区(Method Area)用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虚拟机规范把方法区描述为了堆的一个逻辑部分,但有个别名Non-Heap。
6.运行时常量池(线程共享)
运行时常量池(Runtime Constant Pool) 是方法区的一部分。
class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中保存。
Java虚拟机对Class文件中的每一部分的格式都有严格的规定。每一个字节用于存储哪种数据都必须符合规范才会被虚拟机认可、装载和执行,但是对于运行常量池,Java虚拟机规范没有做任何细节的要求,不同的虚拟机可以按照自己的需求进行实现。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定要只有编译的时候产生,运行期间也可能将新的常量放入池中,利用的多的便是String类的intern()方法。
在无法再申请到内存是会排除OOM异常。
二、HotSpot虚拟机对象探秘
HotSpot虚拟机在Java堆中对象分配、布局和访问过程的探讨。
1.对象的创建
Java是面向对象的语言,在程序的执行过程中随时都有可能会有对象被创建,从语言层面上讲,创建对象(例如克隆,反序列化)通常仅仅是new关键字。
过程:
- 加载类:当虚拟机遇到一个new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且通过检查这个符号引代表的类是否已经被加载、解析和初始化过。如果没有则执行相应的类加载过程。(此处不讨论这部分的具体内容,详情参考第七章)
- 分配内存:在类加载通过后,虚拟机要为新生对象分配内存,对象所需要的内存在类加载后就可以确定,为对象分配空间就是从Java堆中划分出来。此时常见有两种方法:(1)指针碰撞:若堆中内存是绝对规整的,已经使用的在一边,没有使用的在一边,中间有个分割指针,则只需要把分割指针向没有使用的内存移动所需要的固定大小(2)空闲列表:Java堆内存不是规整的,已使用的未使用的是相互交错的,虚拟机就要维护一个列表,列表保存哪些是已经使用的,哪些是未被使用的,分配的时候只需要从未被使用的内存中分配固定的大小给新对象。
选择哪种方法由堆内存是否规整所决定,而是否规整取决于采用的垃圾收集器是否带有压缩整理功能。
可以很明显的看出,这种分配空间的方法不是线程安全的,可能在对A对象分配时指针还没来得及修改,对象B就采用了原来的指针,解决这个问题有两种方案:
(1)对分配空间的动作进行同步化处理:实际上虚拟机采用CAS配上失败重试的方式保持更新操作的原子性(要么全部做完,要么一点也不做)。
(2)把内存分配的动作按照线程划分在不同的空间上进行,即每个线程在Java堆中预先分配一块内存,成为本地线程分配缓冲TLAB。哪个线程要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。
- 内存空间初始化:内存分配结束后,虚拟机将分配的内存空间都初始化为零值(不包括对象头,分配的内存空间的具体内容下一部分会解释),如果使用TLAB来完成,这一工作可以提前到TLAB分配时进行,这一操作保证了对象的实例字段在Java中不赋初始值就可以直接使用,即零值。
- 对象头设置:接下来,虚拟机要对对象进行必要的设置,例如:是哪个类的实例?如何找到类的元数据信息?哈希码?对象的GC分代年龄等。这些信息存放在对象的对象头(Object Header)中。
- 初始化:从虚拟机视角来看,一个新的对象已经产生,但是从Java程序来看,这才刚刚开始,因为内存的数据内容都是零值,执行new之后,会执行<init>方法,按照程序员的要求来进行初始化。
2.对象的内存布局
分为三部分:
(1)对象头
①存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。32位虚拟机中为32bit,64位虚拟机中为64bit,官方称之为"Mark Word"。但是需要的存储的运行时数据太多,其实已经超过了32bit或者64bit,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的效率Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态来复用自己的存储空间。例如在32位的HotSpot虚拟机中,如果对象处于未被锁的状态下,那么就不储存和锁有关的信息,25bit存储hashcode,4bit存储GC,2bit存储锁标志位、1bit固定为0。
②类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的示例。并不是所有的虚拟机实现都必须通过数据上保留类型指针(和对象的访问定定位有关,下面会进行解释)。如果对象是一个Java数组,那在对象头还必须有一块来记录数组长度的数据,因为从数组的元数据中无法确定数组的大小。
(2)实例数据
对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容,无论是父类继承下来的还是子类中定义的,都要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义的顺序有关。HotSpot虚拟机默认的分配策略为: longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers),从策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个的前提下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认就是true),那么子类之中较窄的变量也可能插入到父类变量的空隙之中。
(3)对齐填充
不是必然存在的,也没有特殊的含义,仅仅是占位符的作用。由于HotSpot VM 的自动内存管理必须要求对象的起始地址必须是8字节的整数倍,换句话说就是对象的内存大小必须是8字节的整数倍,如果不足,则填充到8的整数倍。
3.对象的访问定位
即栈内存中的引用如何访问堆内存中的对象。目前主流的方式有两种:
(1)使用句柄:Java堆中会划分一块区域作为句柄池,栈中存储的就是对象的句柄地址,而句柄中包含了对象实例数据指针(指向堆内存中)和类型数据指针(指向方法区)。
(2)直接指针:栈中存储的直接就是对象地址,而堆中对象的布局要包括对象示例数据和类型数据指针(指向方法区)。
使用句柄的优点: 引用中存储的稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的行为),只会改变句柄的实例数据指针,而引用本身不会改变。
直接指针的优点: 速度快,节省了一次指针定位的时间开销。由于对象访问十分频繁,极少成多之后也是可观的执行成本。HotSpot虚拟机是使用第二种进行对象访问的。
三、实战:OOM异常
原书阐述了产生几种异常的方式和简单代码,具体参考原书内容。
Java内存区域与对象创建过程的更多相关文章
- JVM系列一(Java内存区域和对象创建).
一.JVM 内存区域 堆 - Heap 线程共享,JVM中最大的一块内存,此内存的唯一目的就是存放对象实例,Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆"( ...
- Java内存区域和对象的创建
文章绝大部分内存摘抄自<深入理解Java虚拟机>,结合了小部分个人理解如果有什么错误,还望指出,如果涉及到侵权,联系博主,立马删除,再次感谢<深入理解Java虚拟机>的作者-周 ...
- Java虚拟机2:Java内存区域及对象
几个计算机的概念 为以后写文章考虑,也为巩固自己的知识和一些基本概念,这里要理清楚几个计算机中的概念. 1.计算机存储单位 从小到大依次为位Bit.字节Byte.千字节KB.兆M.千兆GB.TB,相邻 ...
- java基础---类加载和对象创建过程
类中可以存在的成员: class A{ 静态成员变量: 非静态成员变量: 静态函数: 非静态函数: 构造函数 A(..){...} 静态代码块 static{...} 构造代码块 {...} } 类加 ...
- java内存区域和对象的产生
一直被java中内存组成弄的头晕眼花,这里总结下都有哪些,先上图片 程序计数器 小块内存,线程执行字节码的信号指示器,以此获取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复都要依赖他 ...
- Java中对象创建过程
本文介绍的对象创建过程仅限于普通Java对象,不包括数组和Class对象. 1.类加载检查 虚拟机遇到一条new指令时,首先去检查该指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用 ...
- 深入java----java内存区域及对象的创建
看完深入理解jvm之后自己再用图的方式进行一遍梳理,用以加深理解. 第一部分,首先对整体java运行时内存区域有一个整体框架式的了解. 运行时内存区域的划分如上图所示,那么接下里看看一个对象的创建又怎 ...
- Java中类加载过程和对象创建过程
类加载过程: 1, JVM会先去方法区中找有没有相应类的.class存在.如果有,就直接使用:如果没有,则把相关类的.class加载到方法区 2, 在.class加载到方法区时,会分为两部分加载:先加 ...
- 深入理解Java虚拟机(二)——HotSpot对象创建、内存、访问
对象的创建 虚拟机遇到一条字节码new指令时,开始对象创建过程. 首先去检查这个指令的参数是否能在常量池定位到一个类的符号引用: 检查这个符号引用代表的类是否已被加载.解析和初始化,如果没有就必须执行 ...
随机推荐
- JavaScript设计模式_05_发布订阅模式
发布-订阅模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都将得到通知.发布-订阅模式是使用比较广泛的一种模式,尤其是在异步编程中. /* * pre:发布-订阅 ...
- checkbox全选、全不选实现
<form name=aa1 > <p> <input type=checkbox name="radiot ...
- gradle 使用总结
什么是gradle 书面化解释: Gradle可以自动化地进行软件构建.测试.发布.部署.软件打包,同时也可以完成项目相关功能如:生成静态网站.生成文档等. Gradle是一种依赖管理工具. 它和ma ...
- linux 下用renameTo方法修改java web项目中文件夹名称问题
经测试,在Linux环境中安装tomcat,然后启动其中的项目,在项目中使用java.io.File.renameTo(File dest)方法可行. 之前在本地运行代码可以修改,然后传到Linux服 ...
- C# DataGridView显示日期格式问题
给DataGridView单元格绑定或者赋值DataTime数据后有时会发现不能显示完整的数据格式,怎么办呢?给出解决方案如下:1.指定整列的显示格式:m_dataGridView.Columns[c ...
- 浅析TCP/IP 协议
TCP/IP协议不是TCP和IP这两个协议的合称,而是指因特网整个TCP/IP协议族. TCP/IP协议模块关系 从协议分层模型方面来讲,TCP/IP由四个层次组成:网络接口层.网络层.传输层.应用层 ...
- Java总结之线程(1)
java线程是很重要的一项,所以作为java程序员必须要掌握的. 理解java线程必须先理解线程在java中的生命周期.. 1.java线程生命周期 1.new 创建一个线程 java中创建线程有 ...
- AngularJS4.0环境搭建详解--windows系统环境
第一步:安装NodeJS 下载最新版的NodeJS并安装,安装完成后打开CMD命令行,输入以下命令: node -v 若返回类似版本号则代表NodeJS安装成功,如下: 第二部 安装npm 新版的No ...
- tomcat+jdk+mysql
转自 http://www.cnblogs.com/liulinghua90/ ,写的很详细,转来共享私藏 按照下面的步骤一步一步来搭建tomcat+jdk+mysql环境. [Linux环境]- ...
- lsdslam代码笔记
0.1. question 0.2. 算法框架 0.3. 代码解析 0.3.1. 数据结构 0.3.1.1. Frame 0.3.1.2. FrameMemory 0.3.1.3. FramePose ...