前言

对于java程序员来说,在虚拟机自动内存管理的机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出的问题,由虚拟机管理内存,这一切看起来都很美好。不过,也正是因为java程序员把内存控制的权利交给了java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项艰难的工作。所以本篇博文主要介绍java虚拟机内存的各个区域。

运行时数据区域

运行时数据区域就是指虚拟机在运行java程序时,把虚拟机自己管理的内存划分为若干个不同的数据区,如下图:

程序计数器

程序计数器是线程私有;

程序计数器是较小的一块内存,用于记录当前线程执行到哪一步;

如果执行的是java方法,则程序计数器记录的是正在执行的虚拟机字节码指令的地址;

如果执行的是native方法,则程序计数器为空;

此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;

java虚拟机栈

线程私有;

描述java方法执行的内存模型,每个方法在执行时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链表、方法出口等信息;

每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈出栈的过程;

此内存区域会出现两种异常状况:

如果线程请求的栈深度大于虚拟机栈允许的深度,则抛出StackOverflowError异常;

如果虚拟机栈可动态扩展,且在扩展时无法申请到足够的内存时,则抛出OutOfMemoryError;

本地方法栈

线程私有;

与虚拟机栈基本一致,唯一不同的地方在于:虚拟机栈中执行的是java方法,而本地方法栈中执行的是native方法

java堆

线程共享;

java堆是虚拟机内存管理中最大的一块区域,虚拟机启动时创建;

几乎所有的对象实例都在堆内存中分配;

java堆可以处于物理上不连续的空间中,只要逻辑上是连续的即可;

如果堆中没有内存分配去完成实例化,且堆无法扩展时,将会抛出OutOfMemoryError;

方法区

线程共享;

用于存储虚拟机加载的类信息、常量、静态变量、字节码文件等;

当方法区无法分配内存时,则抛出OutOfMemoryError;

运行时常量池

运行时常量池是方法区的一部分;

用于存储编译时期产生的字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError;

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存被频繁使用,而且也可能导致OutOfMemoryError;

JDK1.4中新加入的NIO(New Input/Output)类,引入一种基于通道(Channel)与缓冲区(buffer)的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中DirectByteBuffer对象作为这块内存的引用进行操作;

直接内存不会受java堆大小的限制,但是,受本机总内存大小以及处理器寻址空间的限制;

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各区域内存总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError;

HotSpot虚拟机中对象

对象的创建

检查类加载是否完成:虚拟机遇到new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引号,并且检查这个符号引用代表的类是否已被虚拟机加载、解析、初始化过,如果没有,那必须先执行相应的类加载过程;

为新生对象分配内存

  1. 为对象分配内存,相当于把一块确定大小的内存从java堆中划分出来。假设java堆内存时规整的,所有已用内存一边,未用内存一边,中间放置一个指针作为分界点指示器,那分配内存就是指针想空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”;如果java堆不规整,已使用和未使用内存相互交错,这时就不能指针碰撞来分配内存,而是需要维护一张列表,记录哪些内存可用,在分配时从列表中找出一块足够大的空间划分给对象实例,并更新列表的记录,这种分配方式叫做“空闲列表”,至于选择哪一种分配方式,取决于java堆内存是否规整,而这又取决于所采用的垃圾收集器是否带有压缩整理功能决定的。
  2. 解决分配内存时的并发问题:一种方法是对分配内存的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来决定

初始化对象分配到的内存空间

虚拟机设置对象:例如对象是哪个类的实例、如何找到类的元数据信息、对象的额哈希码、对象的GC分代年龄等信息;

对象的内存形态

对象在内存中的布局可以分为三个部分:对象头、实例数据、对齐填充

对象头:对象头包含两部分数据——

  1. 第一部分用于存储对象运行时的数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别位32bit和64bit,官方称为“Mark Word”。
  2. 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:对象真正存储的有效信息,也是程序中定义的各种类型的字段内容;

对齐填充:不是必然存在的,也没有特别的含义,他仅仅起着占位符的作用。由于HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象大小必须是8字节的倍数。而对象头部分正好是8字节的整数倍,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全;

对象的访问定位

java程序需要通过栈上的refrence数据来操作堆上的具体对象,由于refrence类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象的访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种

使用句柄

使用句柄访问的话,java堆将会划分出一块内存来作为句柄池,refrence中存放的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,这样做的好处是,无论对象怎么被移动(垃圾收集时经常发生移动),refrence本身不会变化,只需要修改句柄中的实例数据地址即可,如下图

直接指针访问

这种情况需要考虑的是,在java堆中对象布局如何放置对象的类型数据信息,而refrence中存储的直接就是对象的实例数据地址信息,这样做的好处是,少了一次指针定位的开销,如下图:

Java内存区域和内存溢出异常的更多相关文章

  1. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  2. 《深入理解java虚拟机》第二章 Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 2.2 运行时数据区域  

  3. 深入了解Java虚拟机(1)java内存区域与内存溢出异常

    java内存区域与内存溢出异常 一.运行时数据区域 1.程序计数器:线程私有,用于存储当前所执行的指令位置 2.Java虚拟机栈:线程私有,描叙Java方法执行模型:执行方法时都会创建一个栈帧,存储局 ...

  4. 深入理解java虚拟机---->java内存区域与内存溢出异常

    2. java内存区域于内存溢出异常 2.1 概述: 对于C/C++而言,内存管理具有最高的权利,既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到结束的维护责任. 对于java而言,则把内存 ...

  5. 第二章Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 一.概述 对与Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个new操作去写delete/free代码,不容易出现内存泄露和内存溢出问 题, ...

  6. 2.1 自动内存管理机制--Java内存区域与内存溢出异常

    自动内存管理机制 第二章.Java内存区域与内存溢出异常 [虚拟机中内存如何划分,以及哪部分区域.什么样代码和操作会导致内存溢出.各区域内存溢出的原因] 一.运行时数据区域 Java虚拟机所管理的内存 ...

  7. 虚拟机--第二章java内存区域与内存溢出异常--(抄书)

    这是本人阅读周志明老师的<深入理解Java虚拟机>第二版抄写的,有很多省略,不适合直接阅读,需要阅读请出门左转淘宝,右转京东,支持周老师(侵权请联系删除) 第二章java内存区域与内存溢出 ...

  8. 深入理解Java虚拟机之Java内存区域与内存溢出异常

    Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...

  9. 深入理解Java虚拟机之图解Java内存区域与内存溢出异常

    Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...

  10. JVM内存区域与内存溢出异常

    Java虚拟机在执行java程序时会把它所管理的内存会分为若干个不同的数据区域,不同的区域在内存不足时会抛出不同的异常. >>运行时数据区域的划分 (1)程序计数器程序计数器(Progra ...

随机推荐

  1. 从壹开始微服务 [ DDD ] 之十 ║领域驱动【实战篇·中】:命令总线Bus分发(一)

    烽火 哈喽大家好,老张又见面了,这两天被各个平台的“鸡汤贴”差点乱了心神,博客园如此,简书亦如此,还好群里小伙伴及时提醒,路还很长,这些小事儿就随风而去吧,这周本不打算更了,但是被群里小伙伴“催稿”了 ...

  2. jdk源码阅读笔记-ArrayList

    一.ArrayList概述 首先我们来说一下ArrayList是什么?它解决了什么问题?ArrayList其实是一个数组,但是有区别于一般的数组,它是一个可以动态改变大小的动态数组.ArrayList ...

  3. 前端笔记之ES678&Webpack&Babel(上)初识ES678&Babel&let和const&解构&语法

    一.ES版本简介和调试运行方法 1.1 ECMAScript简介 MDN手册:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript JavaS ...

  4. 关于页面传参,decodeURI和decodeURIComponent

    之前写过一个关于页面传参的,但是是前端相对于自己的页面做的跳转,也就是页面1,跳转到页面2,里面带的参数.这里可以参考我上一篇文章,包括里面参数中如果有数组和json格式的情况.但是需要注意的是,我前 ...

  5. Win64下编译OSG详细过程(Win10+VS2015+OSG3.6.3)

    目录 1. 数据资源准备 2. 编译第三方库 3. 编译GDAL 4. 编译OSG 1) CMAKE_INSTALL_PREFIX: 2) BUILD_OSG_EXAMPLES/BUILD_MFC_ ...

  6. SQLServer之附加数据库

    附加数据库注意事项 必须首先分离数据库. 尝试附加未分离的数据库将返回错误. 附加数据库时,所有数据文件(MDF 文件和 LDF 文件)都必须可用. 如果任何数据文件的路径不同于首次创建数据库或上次附 ...

  7. Mysql B+Tree原理

    B+树索引是B+树在数据库中的一种实现,是最常见也是数据库中使用最为频繁的一种索引.B+树中的B代表平衡(balance),而不是二叉(binary),因为B+树是从最早的平衡二叉树演化而来的.在讲B ...

  8. 【转】Python爬取AES加密的m3u8视频流的小电影并转换成mp4

    最近发现一个视频网站,准备去爬取得时候,前面很顺利利用fiddler抓包获取网站的post数据loads为python字典数据,分析数据就能发现每个视频的连接地址就在其中, 发现这些都是m3u8文件流 ...

  9. Chrome内核浏览器打开网页报 错误代码: ERR_TIMED_OUT

    升级win10之后如果出现chrome内核的浏览器网页总是打不开 打开很慢  而ie和edge是可以正常访问的 用这个方法可以  我弄了几天终于 搞好了我直接转载过来了近期,工程师收到大量反馈360浏 ...

  10. JDK中的Atomic包中的类及使用

    引言 Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作.原子变量的底层使用了处理器提供的原子指令,但是不同的CPU ...