深入学习Java虚拟机——虚拟机内存区域与内存溢出异常
1. 运行时数据区域
1.1 程序计数器
1. 程序计数器是一段较小的内存空间,可以看作为当前线程所执行字节码的行号指示器。通过改变这个计数器的值来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器。
2. 每条线程都会有一个独立的程序计数器,各线程间程序计数器互不影响,独立存储,所以这个内存区域是线程私有的。
3. 如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是本地(Native)方法,则这个计数器值为空,此内存区域是唯一一个在Java虚拟机中没有OutOfMemoryError情况的区域。
1.2 虚拟机栈
1. 首先,虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法的执行模型:每个方法在执行的同时都会创建一个栈桢,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成,就对应着一个栈桢从入栈到出栈的过程。
2. 局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,long,float,double)、对象引用类型和returnAddress类型(指向一条字节码指令的地址)。且局部变量表的内存空间会在编译期完成分配,方法运行期间不会改变局部变量表的大小。
3. 异常状况:
(1)如果线程请求的栈深度大于虚拟机所允许的深度,则将抛出StackOverflowError异常。
(2)如果虚拟机栈可以动态扩展,而扩展时无法申请的足够的内存,就会抛出OutOfMemoryError异常。
1.3 本地方法栈
1. 本地方法栈与虚拟机栈类似,但虚拟机栈是为虚拟机执行Java方法服务的,而本地方法栈是为虚拟机使用的本地方法服务。
2. 同样的,该内存区域也会有StackOverflowError异常和OutOfMemoryError异常。
1.4 Java堆
1. Java堆是被所有线程共享的内存区域,在虚拟机启动时创建。此区域只用来存储对象实例,几乎所有的对象都会在这里被创建(并不是所有的对象都在堆中创建)。
2. Java堆是垃圾收集器管理的主要区域。从内存回收角度看,垃圾收集器主要采用分代收集算法,所以还可以将Java堆分为新生代和老年代,进一步细分为Eden区,From Survivor区和To Survivor区。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
进行这些划分的目的都是为了更快更好的回收内存或者分配内存。
3. 可能发生的异常:Java堆可能会处于物理上内存空间不连续的内存空间中,但逻辑上必须是连续的。其空间大小可以通过-Xmx和-Xms来控制,可以实现为固定大小,也可以为可扩展大小。当没有足够的内存空间完成分配并且堆无法扩展时,就会抛出OutOfMemoryError异常。
1.5 方法区
1. 方法区是线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法区属于堆的一个逻辑部分,但它却仍然要与Java堆区分开。
2. 方法区与堆类似,不需要连续的内存空间、内存空间大小可以固定或者可扩展,还可以选择不实现垃圾收集。在方法区,很少出现垃圾收集,这个区域的内存回收主要针对常量池的回收以及对类型的卸载。
3. 当方法区无法满足内存分配需求时,就会抛出OutOfMemoryError异常。
4. 运行时常量池:该区域是方法区的一部分,Class文件中除了有类似的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类被加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,即运行期间也可能将新的常量放入运行时常量池,比如String类中的intern()方法。
1.6 直接内存
1.直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
2.应用:在jdk1.4以后加入了NIO类,引入了一种基于通道与缓冲区的新IO方式,它使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中DirectByteBuffrer对象作为这块内存的引用进行直接操作,避免了在Java堆与Native堆之间来回复制数据,显著提高了性能。由于分配的是Native内存空间,所以大小不会受到Java堆大小的限制,但是肯定会受到本机总内存的限制。在通过设置虚拟机参数来设置堆等区域的内存空间时,忽略直接内存大小就有可能导致各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
2. Java虚拟机对象
2.1 对象的创建
1. 创建过程(一般对象,即不包括数组和Class对象):
(1)加载对象类:当虚拟机运行一条new指令时,首先检查这个指令的参数(也就是new后面的类名)是否能在常量池中定位到一个类的符号引用,并且检查这个类是否已被加载、解析和初始化过。如果没有,那就必须进行相应的类加载过程(该过程在后面会详细分析)。
(2)分配对象所需内存空间:
在类加载完成后即可确定对象所需的内存空间大小,给对象分配内存空间就是把一块确定大小的内存从Java堆中划分出来。分配方式主要取决于虚拟机所采用的GC是否带有压缩整理功能。
有压缩整理功能的GC会把Java堆分成两部分,一部分是被占用的,一部分是空闲的,通过一个指针作为分界的指示器,分配内存是只需要把指针向空闲区移动即可;不带压缩整理功能的GC会导致Java堆处于一种空闲与占用交错的内存空间,这时虚拟机就必须维护一个列表,记录堆中可用的内存空间,分配时只需要找到一个足够大小的空间划分给对象即可。
但是,在并发情况下,以上两种方式也并不安全,比如,正在给对象A分配空间时,指针还未修改,对象B又占用了该指针来分配空间。所以,虚拟机采用了CAS加上失败重试的方式保证更新操作的原子性;另一种方式是把内存分配动作按照线程划分在不同的空间中进行,即每个线程在堆中先分配一小块内存,称为本地线程分配缓冲(TLAB),那个线程要分配内存,就在那个线程的TLAB上分配,当使用完TLAB并分配新的TLAB时,才需要同步锁定,虚拟机使用TLAB可通过 -XX:+/-UseTLAB参数来设定。
(3)内存空间初始化:内存空间分配后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一过程也会提前至TLAB分配时执行。该过程保证了对象即使不赋予初始值额可以使用,能访问到的字段的数据类型均为所对应的零值。
(4)设置对象信息:例如这个对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分带年龄等信息。这些信息存放在对象的对象头中,根据虚拟机当前运行状态的不同,是否启用偏向锁等。
(5)对象数据初始化:以上步骤完成后,对象已经创建成功,但此时对象内所有字段为零或null,此时便进行对象数据初始化,创造程序员所需要的对象。
2.2 对象的内存布局
虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据,对齐填充。
1. 对象头:包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据被称为“Mark Word”,这部分数据长度在32或64位的虚拟机中分别为32bit或64bit。对象需要存储的运行时数据很多,超出32或64bit所能记录的限度,但对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word被设计为一个非固定的数据结构以便在极小的空间内存储尽量多的信息,例如在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,则Mark Word的32bit空间中,25bit用于存储对象哈希值,4bit用于存储对象分带年龄,2bit用于存储锁标志位,1bit固定为0,其他状态下的存储内容如下
存储内容 | 标志位 | 状态 |
对象哈希码、对象分代年龄、 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空 | 11 | GC标记 |
偏向线程id、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
对象头的另一部分数据是类型指针,即对象指向他的类元数据的指针,虚拟机通过该指针确定对象所属的类,但并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据信息并不一定要经过对象本身。对于数组对象,对象头中还必须友谊路爱用于记录数组长度的数据。
2.实例数据:这里是对象真正存储的有效信息,包括各个字段的内容,无论是当前类的还是父类的。
3.对齐填充:这一部分并不是必要存在的,也没有特殊含义,仅仅是为了使对象的大小必须是8字节的整数倍,如果对象的大小不足,则会进行填充补全。
2.3 对象的访问定位
创建对象是为了使用对象,在Java程序中,通过栈上的对象引用来操作堆上的对象,那么这个引用通过何种方式去定位和访问堆中的对象的具体位置?具体实现取决于虚拟机实现,主要有两种方法,使用句柄和直接指针。
1.使用句柄访问:Java堆中划分一块内存作为句柄池,reference(引用)中存储的就是句柄池中存储的对象的句柄地址,而句柄包含了对象的实例数据与类型数据各自的具体地址信息。
2.使用直接指针访问:reference(引用)中存储的就是对象地址
这两种访问方式各有优势,使用句柄访问的好处是引用中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的示例数据指针,而引用本身不需要修改。而使用直接指针访问方式的最大好处是速度更快,大部分虚拟机都会采用这种方式。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
区块链开发公司谈区块链的应用价值有哪些
区块链技术应该应用到更多真正的场景当中,区块链技术需要与实体经济产业结合,通过减少交易的中间环节、中介环节,减少交易对账、记录等等辅助性工作,为实体经济降成本,才能充分发挥区块链的价值。 市场经济活动中存在众多信息中介和信用中介问题,由于信息不对称导致交易双方无法建立有效的信用机制,区块链技术恰好为解决这一问题提供了全新思路。 无论是传统金融服务,还是P2P、众筹等互联网金融创新,或是强化金融监管、防范金融风险、打击非法集资等领域。 以目前的发展来看, 区块链技术在支付、金融交易、物联网等多领域存在广大应用潜力,其中智能合约是关键。支付方面,传统支付采用“拉式”模式(传统拉式是用户将个人信息提供给 第三方,第三方利用这些信息进行支付处理),而区块链技术采用“推式”模式,直接绕开第三方,大幅改善安全性。 中心化存在一些问题,比如资产权属问题,你在银行里的个人信息是银行的,不是你个人的,银行可以用来做推销;也比如安全问题,银行赖账、受到黑客攻击、数据被篡等,你的资产可能受到损失,区块链通过去中心化,解决信息不对称性问题,降低交易成本,提高生产效率。区块链的意义不在于取代现有法币,而是在于改...
- 下一篇
Julia语言初体验
最近MIT发布的julia 1.0.0版,据传整合了C、Python、R等诸多语言特色,是数据科学领域又一把顶级利器。 周末心血来潮赶快体验了一把,因为用习惯了jupyter notebook,安装完julia 1.0.0之后就配置好了jupyter notebook。 在安装配置环境阶段就遭遇了不少坑,吃了不少苦头,这里不得不吐槽级距,julia的安装配置一点儿也不比python简单,自己配置原生环境,结果下载包各种不兼容,想要导入本地数据,需要解决CSV包、xlsx包的接口问题,总之一路坎坷。 这里把自己走过的弯路总结一下,方便后来者学习! 1、环境选择: 强烈建议选择JuliaPro来安装,这里稍稍说明一下,julia虽然在8月8日更新了Julia 1.0.0版本,但是作为一门新兴语言,它的版本后向兼容实在是不敢恭维,原生环境里面一个包都不给配置,需要自己一个一个下载。所以选择了JuliaPro这个集成环境(主要集成了Atom+juno【julia的第三方IDE】、jupyter notebook【浏览器端的编辑器】)。主要是JuliaPro初始化就配置了好几十个常用的包,省的自...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Red5直播服务器,属于Java语言的直播服务器