《深入理解java虚拟机》——java内存区域与内存溢出异常

我是很喜欢用java语言编写代码的。从开始学习到现在其实也是在一步步体会java语言的各方面,开始看深入理解java虚拟机这本书觉得java虚拟机的内部感觉就像是一个操作系统,也可以说是个计算机。想要深入的理解我觉得需要先从整体去看。为什么需要java虚拟机,java虚拟机到底是个什么?我会先从这个方向开始理解。

JDK:java程序设计语言、java虚拟机、java API类库三部分。

JRE:支持java程序运行的标准环境。

java虚拟机实现了java语言与平台的无关性。意思就是java语言在不同的平台(操作系统等环境)运行都是可以的,因为java虚拟机将java源文件编译成class字节码文件,class字节码在java虚拟机中被解释成机器码。

JVM定义了控制java代码解释执行和具体实现的五种规格,他们是:

  • jvm指令系统

jvm指令系统同其他计算机的指令系统及其相似。java指令也是由操作码和操作数两部分组成。操作码为8位二进制数,操作数紧随操作码之后,其长度根据需要而不同。当操作数的长度大于8位时,会被分为两个以上的字节存放。它编码的方式和intel采用的方式不同,jvm是低字节放在高位,高位放在低字节中。java的8位操作码的长度使jvm最多有256条指令,java1.6及以上版本与使用了160多种操作码。

  • jvm寄存器(4中常用的)

    • pc:程序计数器
    • optop:指向操作数栈顶的指针
    • frame;指向当前执行方法的执行环境的指针
    • vars:指向将当前执行方法的局部变量区第一个变量的指针
  • jvm栈结构(java栈是JVM存储信息的主要方法)

    • 局部变量区
    • 运行环境区
    • 操作数区
  • jvm碎片回收堆

java类的实例所需的存储空间是在堆上分配的。

  • jvm存储区

jvm有两类存储区:常量缓冲池和方法区。

常量缓冲池:用于存储类名称、方法和字段名称以及串常量。

方法区:用于存储java方法的字节码。

这两种存储区域具体实现方式在jvm规格中没有具体说明,也就是说java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。

有一个类比的例子我觉得很形象:

如果把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。

了解到这里感觉jvm和最近学习的微机原理好像有异曲同工之妙。计算机在处理信息的时候也需要寄存器去存储数据或者存储指令地址等,有他的存储单元和运算单元。

运行时数据区区域

书里说java提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界的问题。在android中如果对某些对象的使用不当的话就会出现内存泄漏,为此就带着这个问题去看看,java虚拟机内存的各个区域,为什么避免了这些问题。。。

java虚拟机所管理的内存将会包含以下几个运行时数据区域:

1. 程序计数器

程序计数器是一块较小的内存空间,没有规定OOM情况的区域且是线程私有的,有两个主要功能

  • 是当前线程所执行的字节码的行号指示器。

字节码解释器通过改变程序计数器的值来选取下一条将要执行的字节码指令(是跳转 循环还是。。。)

  • 为了线程切换后能够恢复到正确的执行位置

在操作系统中进程切换执行也需要记录进程当前执行的状态。这里的作用是可以类比的。因为java虚拟机的多线程是通过线程的轮流切换并分配处理器执行时间的方式实现的,因此需要记录当前线程运行的状态以便于下次从上次的运行状态开始执行。

如果线程正在执行的是java方法,计数器记录正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,计数器值为空。

2. java虚拟机栈

线程私有,且与线程的生命周期相同。描述的是java方法执行的内存模型

有一个需要了解的概念栈帧:是方法运行时的基础数据结构。

每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行,就是一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表

局部变量表存放了编译期可知的各种数据基本类型、对象引用类型和returnAddress类型(指向了一条字节码指令的地址)。

有两个特点:

  • long型和double型的数据(64位)会占用2个局部变量空间,其余的数据类型只占用1个。
  • 局部变量所需的内存空间在编译期间完成分配。当进入一个方法,这个方法在栈帧中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部常量表的大小。

两种异常情况:

  • 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
  • 虚拟机可以动态扩展,若扩展时无法申请到足够的内尺寸,抛出OutOfMemoryError异常

3. 本地方法栈

本地方法栈为虚拟机使用到的native方法服务,虚拟机栈为虚拟机执行的java方法服务。

本地方法区域会抛出StackOverFlowError异常和OutOfMemoryError异常。

4. java堆

java堆是java虚拟机所管理的最大的一块内存(对大多数应用),是被所有线程共享的一块内存区域,在虚拟机启动时创建。内存区域的唯一目的就是存放对象实例
几个特点:

  • 是各个线程的共享内存。
  • java堆是垃圾回收器管理的主要区域。
  • 从内存分配角度来看,线程共享的java堆可能划分出多个线程私有的分配缓冲区。
  • java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就可以。

无论如何划分,无论哪个区域,存储的都是对象实例。划分目的就是为了更好的回收内存或者更快的分配内存。

5. 方法区

几个特点:

  • 是各个线程的共享内存区域
  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器翻译后的代码等数据
  • 不需要连续的物理内存和可以选择固定大小或者可扩展
  • 可以选择不实现垃圾回收机制。内存回收目标主要是针对常量池的回收和对类型的卸载。
  • 无法满足内存分配需求会抛出OOM。

6. 运行时常量池

几个特点:

  • 是方法区的一部分

Class文件有类的版本、字段、方法、接口等描述信息,还有一个就是常量池。

  • 常量池用于存放编译期生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池。还会把翻译出来的直接引用存储在运行时常量池。

java虚拟机对class文件的每一部分都有严格的格式要求,每个字节用于存储什么样的数据都必须符合规范才可以被虚拟机认可、装载和执行。

  • 对运行时常量池java虚拟机没有做任何细节要求,不同提供商实现的虚拟机可以按照自己的需要来实现内存区域。
  • 具备动态性(相对于class文件常量池)。运行期间也可以将新的常量放入池中。

java语言并不要求常量一定只要编译期才能产生,也就是并不是在class文件常量池中的内容才能进入方法区的运行时常量池。

  • 受到方法区的限制。无法申请内存时抛出OOM。

7. 直接内存

  • 并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。
  • 也可能导致OOM。
  • 不会受到java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。

对象的创建

对象的内存布局

对象的访问定位

这里看了很久的时间,发现其实做一个大纲就很好理解并且把他们串起来。


image

OutOfMemoryError异常

在android的日常开发中就很可能在写代码的过程中遇到OOM的问题。我遇到过OOM的场景就是在加载Bitmap的时候还有就是多线程的情况下,线程开的太多以至于在线程池中都无法挽救。

现在就先从底层来分析一下OOM出现的情况:

1. java堆溢出

java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。

在android可以通过下面的方法获得堆的大小,也就是每个程序可使用的内存的上限:

ActivityManager manager = (Activity)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();//结果以MB为单位返回

有一个东西叫GC Roots到底是什么呢?

有一个算法叫做根搜索算法。是JVM用来*判断对象是否存活的算法,此算法的基本思路就是通过一系列的GC Roots对象作为起始点,从这些结点往下搜索,当一个对象和GC Roots不可达时,则该对象是无用的。

image

从上面的图可以看到5、6、7都到达不了GC Roots,所以会被回收掉。

可以作为GC Roots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native)中引用的对象

java堆内存的OOM异常是实际中经常会遇到的情况。当出现堆内存溢出时,为了解决,一般的手段是先通过内存映像分析工具对dump出来的堆转储快照进行分析。重点是确认内存中的对象是内存泄漏还是内存溢出。

  • 内存泄漏:可进一步通过工具查看泄漏对象到GC Roots的引用链。就能找到泄露对象是通过怎样的路径和GC Roots相关联并导致垃圾收集器无法自动回收他们。
  • 内存溢出:也就是不存在内存泄漏,内存中的对象确实都还活着。应该检查虚拟机的堆参数(-Xms或-Xmx),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行时期的内存消耗。(像service...)

虚拟机栈和本地方法栈溢出

  • 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常

方法区和运行时常量池溢出

就是在这两部分中的数据超过他们的范围就会溢出。有一个很有意思的例子

public class Test{public static void main(String[] args){String str1 = new StringBuilder("计算机").append("软件").toString();System.out.println(str1.intern() == str1);//true-jdk1.7以后 之前为false//在这之前java这个字符串在常量池中存在String str2 = new StringBuilder("计算机").append("软件").toString();System.out.println(str2.intern() == str2);//false 因为在new StringBuilder之前str2已经有指向常量池中的引用了}
}

首先去看了下intern这个方法是干嘛的,原来是返回字符串的对象。String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

产生注释中结果不同的原因是:

  • 在jdk1.6中,intern方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在java堆上,所以必然不是同一个引用。
  • 在jdk1.7中,intern方法不会再复制实例,而是在常量池中记录首次出现的实例引用。

本机直接内存溢出

直接内存容量可通过-XX:MaxDirectMemorySize指定,如果不指定则默认与java堆最大值一样。

分配本机内存可以用两种方式:

用DirectByteBuffer

用过java NIO的话应该都用过ByteBuffer,用来作为消息的缓冲区,它其实是在直接内存开辟了一块空间。(看了虚拟机才知道 之前真的是蒙着头用啊 哈哈哈)

ByteBuffer BUFFER = ByteBuffer.allocateDirect(1*1024*1024);

获取UnSafe实例进行内存分配

public class DirectMemory{public static void main(String[] args){Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessiable(true);Unsafe unsafe = (Unsafe)unsafeField.get(null);unsafe.allocateMemory(1*1024*1024);}
}

在jdk1.4中新加入了NIO类,引入了一种基于通道(channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这可以提高性能,因为避免了在java堆和native堆中来回复制数据。

所以直接内存区域的溢出,发生在可能忽略了分配直接内存的大小,在项目中使用了NIO的时候发生了OOM可以看是不是直接内存溢出的原因。

参考文章

百度知道 统领全文的作用

GC Roots 例子

java堆还是本地内存



喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/55424.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ProtoBuf快速上手

文章目录 创建 .proto文件编译 .proto文件编译后生成的文件序列化与反序列化的使用 此篇文章实现内容: 对一个通讯录的联系人信息,使用PB进行序列化,并将结果输出对序列化的内容使用PB进行反序列化,解析联系人信息并输出联系人信…

redis-数据类型

十大数据类型 学习 redis 操作手册 英文 Commands 中文 Redis命令中心(Redis commands) – Redis中国用户组(CRUG) 学习方法 举出一个数据结构的应用场景(理解数据结构特点),并操作&…

【Java的SPI机制】Java SPI机制:实现灵活的服务扩展

在Java开发中,SPI(Service Provider Interface,服务提供者接口)机制是一种重要的设计模式,它允许在运行时动态地插入或更换组件实现,从而实现框架或库的扩展点。本文将深入浅出地介绍Java SPI机制&#xff…

JAVA开源项目 旅游管理系统 计算机毕业设计

本文项目编号 T 063 ,文末自助获取源码 \color{red}{T063,文末自助获取源码} T063,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

TypeScript 封装 Axios 1.7.7

随着Axios版本的不同,类型也在改变,以后怎么写类型? yarn add axios1. 封装Axios 将Axios封装成一个类,同时重新封装request方法 重新封装request有几个好处: 所有的请求将从我们定义的requet请求中发送&#xff…

Vue3实现动态菜单功能

文章目录 0.效果演示1.搭建Vue3项目1.1 vite 脚手架创建 Vue3 项目1.2 设置文件别名1.3 安装配置 element-plus1.4 安装配置路由2.登录页面3.后台管理页面3.1 搭建后台框架3.2 左侧菜单栏3.3 header 用户信息3.4 主要内容3.5 footer4.配置静态路由5.记录激活菜单5.1 el-menu 绑…

信号处理快速傅里叶变换(FFT)的学习

FFT是离散傅立叶变换的快速算法,可以将一个信号变换到频域。有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了。这就是很多信号分析采用FFT变换的原因。另外,FFT可以将一个信号的频谱提取出来&am…

webpack信息泄露

先看看webpack中文网给出的解释 webpack 是一个模块打包器。它的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用,但它也能够胜任转换、打包或包裹任何资源。 如果未正确配置,会生成一个.map文件,它包含了原始JavaScript代码的映…

课设实验-数据结构-线性表-手机销售

题目&#xff1a; 代码&#xff1a; #include<stdio.h> #include<string.h> #define MaxSize 10 //定义顺序表最大长度 //定义手机结构体类型 typedef struct {char PMod[10];//手机型号int PPri;//价格int PNum;//库存量 }PhoType; //手机类型 //记录手机的顺序…

【HTTP(3)】(状态码,https)

【认识状态码】 状态码最重要的目的&#xff0c;就是反馈给浏览器:这次请求是否成功&#xff0c;若失败&#xff0c;则出现失败原因 常见状态码: 200:OK&#xff0c;表示成功 404:Not Found&#xff0c;浏览器访问的资源在服务器上没有找到 403:Forbidden&#xff0c;访问被…

springboot系列--web相关知识探索三

一、前言 web相关知识探索二中研究了请求是如何映射到具体接口&#xff08;方法&#xff09;中的&#xff0c;本次文章主要研究请求中所带的参数是如何映射到接口参数中的&#xff0c;也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、…

【案例】距离限制模型透明

开发平台&#xff1a;Unity 2023 开发工具&#xff1a;Unity ShaderGraph   一、效果展示 二、路线图 三、案例分析 核心思路&#xff1a;计算算式&#xff1a;透明值 实际距离 / 最大距离 &#xff08;实际距离 ≤ 最大距离&#xff09;   3.1 说明 | 改变 Alpha 值 在 …

stm32f103调试,程序与定时器同步设置

在调试定时器相关代码时&#xff0c;注意到定时器的中断位总是置1&#xff0c;怀疑代码有问题&#xff0c;经过增大定时器的中断时间&#xff0c;发现定时器与代码调试并不同步&#xff0c;这一点对于调试涉及定时器的代码是非常不利的&#xff0c;这里给出keil调试stm32使定时…

自用Proteus(8.15)常用元器件图示和功能介绍(持续更新...)

文章目录 一、 前言二、新建工程&#xff08;以51单片机流水灯为例&#xff09;2.1 打开软件2.2 建立新工程2.3 创建原理图2.4 不创建PCB布版设计2.5 创建成功2.6 添加元器件2.7 原理图放置完成2.8 编写程序&#xff0c;进行仿真2.9 仿真 三、常用元器件图示和功能介绍3.1 元件…

【回眸】Tessy 单元测试软件使用指南(四)常见报错及解决方案与批量初始化的经验

前言 分析时Tessy的报错 1.fatal error: Tricore/Compilers/Compilers.h: No such file or directory 2.error: #error "Compiler unsupported" 3.warning: invalid suffix on literal;C11 requires a space between literal and string macro 4.error: unknown…

螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习01(环境准备)

1 准备工作 由于创建数据中心需要安装很多服务器&#xff0c;这些服务器要耗费很所物理物理计算资源、存储资源、网络资源和软件资源&#xff0c;作为穷学生只有几百块的n手笔记本&#xff0c;不可能买十几台服务器来搭建数据中心&#xff0c;也不愿意跑实验室&#xff0c;想躺…

文件上传之%00截断(00截断)以及pikachu靶场

pikachu的文件上传和upload-lab的文件上传 目录 mime type类型 getimagesize 第12关%00截断&#xff0c; 第13关0x00截断 差不多了&#xff0c;今天先学文件上传白名单&#xff0c;在网上看了资料&#xff0c;差不多看懂了&#xff0c;但是还有几个地方需要实验一下&#…

SpringBoot整合异步任务执行

同步任务&#xff1a; 同步任务是在单线程中按顺序执行&#xff0c;每次只有一个任务在执行&#xff0c;不会引发线程安全和数据一致性等 并发问题 同步任务需要等待任务执行完成后才能执行下一个任务&#xff0c;无法同时处理多个任务&#xff0c;响应慢&#xff0c;影响…

VirtualBox+Vagrant快速搭建Centos7系统【最新详细教程】

VirtualBoxVagrant快速搭建Centos7系统 &#x1f4d6;1.安装VirtualBox✅下载VirtualBox✅安装 &#x1f4d6;2.安装Vagrant✅下载Vagrant✅安装 &#x1f4d6;3.搭建Centos7系✅初始化Vagrantfile文件生成✅启动Vagrantfile文件✅解决 vagrant up下载太慢的问题✅配置网络ip地…

咸鱼sign逆向分析与爬虫实现

目标&#xff1a;&#x1f41f;的搜索商品接口 这个站异步有点多&#xff0c;好在代码没什么混淆。加密的sign值我们可以通过搜索找到位置 sign值通过k赋值&#xff0c;k则是字符串拼接后传入i函数加密 除了开头的aff…&#xff0c;后面的都是明文没什么好说的&#xff0c;我…