前言

对于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. Android 7.0 存储系统—Vold与MountService分析(三)(转 Android 9.0 分析)

    Android的存储系统(三) 回顾:前帖分析了Vold的main()函数和NetlinkManager的函数调用流程,截止到NetlinkHandler的创建和start()调用,本帖继续分析源码 ...

  2. [翻译 EF Core in Action 2.2] 创建应用程序的数据库上下文

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  3. 简单Java类 全网最详细讲解 !!!

    最近学习java非常吃力,学习的进度很快,由于基础没打牢固,整体上项目理解很吃力,偶尔会遇到一些基本的概念,都会阻碍整体的理解.最近也看了不少的视频讲解,听得很迷,最后搞得很乱,没有明确的学习目标,今 ...

  4. SQL优化指南

    慢查询日志 开启撒网模式 开启了MySQL慢查询日志之后,MySQL会自动将执行时间超过指定秒数的SQL统统记录下来,这对于搜罗线上慢SQL有很大的帮助. SHOW VARIABLES LIKE 's ...

  5. Quartz.Net学习笔记

    一.概述 Quartz.NET是一个强大.开源.轻量的作业调度框架,是 OpenSymphony 的 Quartz API 的.NET移植,用C#改写,可用于winform和asp.net应用中.它灵 ...

  6. Odoo : 门店订货及在线签名免费开源方案

    引言 Odoo是欧洲开发的,世界排名第一的开源免费ERP系统.该系统从2002开始研发,经过十几年的发展,去年下半年发布了12.0版.该软件因为免费下载,源代码开放,吸引了世界范围很多人参与使用及开发 ...

  7. 供应链管理为什么要上企业自主可控的免费开源ERP Odoo

    引言 今天的很多企业,无论是制造业,还是商贸行业,如果说没有针对供应链管理的信息系统,那可能是真的冤枉他们了:采购.仓存.销售.存货核算这些模块,早早的买来,早早的用上了,但也早早的被下了结论:食之无 ...

  8. 【原】无脑操作:IDEA + maven + Shiro + SpringBoot + JPA + Thymeleaf实现基础授权权限

    上一篇<[原]无脑操作:IDEA + maven + Shiro + SpringBoot + JPA + Thymeleaf实现基础认证权限>介绍了实现Shiro的基础认证.本篇谈谈实现 ...

  9. RFID和QRCODE对比

    1.技术介绍 1.1 RFID 射频识别,RFID(Radio Frequency Identification)技术,又称无线射频识别,是一种通信技术,可通过无线电讯号识别特定目标并读写相关数据,而 ...

  10. Spring Boot获取前端页面参数的几种方式总结

    Spring Boot的一个好处就是通过注解可以轻松获取前端页面的参数,之后可以将参数经过一系列处理传送到后台数据库. 获得的方式有很多种,这里稍微总结一下,大致分为以下几种: 1.指定前端url请求 ...