前言

对于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. 再遇angular(angular4项目实战指南)

    这两天看了看angular4的文档,发现他和angular1.X的差别真的是太大了,官方给出的那个管理英雄的Demo是一个非常好的入门项目,这里给出一个管理个人计划的小项目,从头至尾一步一步讲解如何去 ...

  2. java代码之美(11)---java代码的优化

    java代码的优化 随着自己做开发时间的增长,越来越理解雷布斯说的: 敲代码要像写诗一样美.也能理解有一次面试官问我你对代码有洁癖吗? 一段好的代码会让人看就像诗一样,也像一个干净房间会让人看去很舒服 ...

  3. 一文读懂Asp.net core 依赖注入(Dependency injection)

    一.什么是依赖注入 首先在Asp.net core中是支持依赖注入软件设计模式,或者说依赖注入是asp.net core的核心: 依赖注入(DI)和控制反转(IOC)基本是一个意思,因为说起来谁都离不 ...

  4. mybatis入门系列一之创建mybatis程序

    Mybatis基础系列一 创建第一个mybatis程序 需要配置项 1. 在conf.xml的需要配置配置两个标签数据库连接和mapper,xml文件加载信息 <-- 进行数据库环境参数的配置 ...

  5. 4.熟悉App Inventor 2编程界面

    以下图片来自 https://www.17coding.net/的 俄罗斯方块开发笔记 的 第二章 开发与测试环境 .感谢金老师的分享. 建议大家打开上面第二章的链接,认真阅读.

  6. SQLServer存储过程自制数据字典

    相信很多小伙伴都对[数据字典]很头疼. 小编刚入职的时候,老大丢一个项目过来,就一个设计文档,数据字典木有,字段说明木有, 全部都需要靠“联系上下文”来猜.所以小伙伴门一定要养成说明字段的习惯哦. 说 ...

  7. 为什么在STM32F429工程配置中需要预先定义USE_STDPERIPH_DRIVER和STM32F429_439xx?

    如图: 1.查找USE_STDPERIPH_DRIVER,发现这个宏出现在stm32f4xx.h头文件中,并且有如下代码: 也就是说,通过已经定义了USE_STDPERIPH_DRIVER宏加载stm ...

  8. Spring boot 配置文件详解 (properties 和yml )

    从其他框架来看 我们都有自己的配置文件, hibernate有hbm,mybatis 有properties, 同样, Spring boot 也有全局配置文件. Springboot使用一个全局的配 ...

  9. June 30th. 2018, Week 26th. Saturday

    Curiosity is the wick in the candle of learning. 如果学习是一根蜡烛,那好奇心就是烛芯. From William Arthur Ward. Pleas ...

  10. 量化投资技术分析工具---ipython使用

    量化投资实际上就是分析数据从而做出决策的过程python数据处理相关模块NumPy:数组批量计算pandas:灵活的表计算Matplotlib:数据可视化 学习目标:用NumPy+pandas+Mat ...