【JAVA 学习基础】java内存区域 探究

前言

文章仅是笔者个人的学习笔记,存在一些只有笔者个人能看到的用词或者描述,如果有不明确的地方,欢迎留言,理性讨论。

一、概述

  • Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。(这个比喻真是绝了
  • java开发不必亲自管理每个new出来的对象的生命周期
    • 好处是大大方便了开发。
    • 坏处是一旦出现问题,如果不够熟悉虚拟机的机制,排查难度会大大增加。

二、运行时数据区域

1.内存区域划分
  • 内存区域划分大致如下:

image.png

  • 所以每个线程有自己的:虚拟机栈、本地方法栈、程序计数器

2.程序计数器
  • 线程私有
  • 可以看作当前线程执行代码的行号指示器,程序执行的分支、循环、跳转、异常处理等都要依靠它来完成
  • 为了线程切换之后,两边线程都能从正确的位置开始执行,所以计数器是每个线程独立存储的
  • 如果当前正在执行 Native 方法,这个计数器的值应该是空(Undefined)
  • 此内存区域是唯一一个按规范没有任何 OOM 情况的区域(这句值得深思啊,后面再来解析一下这句话
3.Java虚拟机栈
  • 线程私有,生命周期与线程相同
  • 每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。
  • 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  • 局部变量表:
    • 方法,或者说栈帧,当然都有自己的局部变量表
    • 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用和returnAddress 类型(指向了一条字节码指令的地址)。
    • 局部变量表所需的内存空间在编 译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间(如果是同一个虚拟机)是完全确定的。
    • 如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;
    • 如果Java虚拟机栈容量可以动态扩展[2],当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
4.本地方法栈
  • 线程私有
  • 本地方法栈 与 虚拟机栈所发挥的作用是非常相似的,区别是:
    • 本地方法栈:为虚拟机使用 Native 方法服务
    • 虚拟机栈:为虚拟机使用 java 方法服务
  • 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时,分别抛出StackOverflowError 和 OutOfMemoryError异常。

5.Java堆
  • 所有线程共享的一块内存区域
  • 按规范来说,所有的Java对象实例都分配在堆上,但是!实际上很多实例可能分配在栈上等
    • (栈上分配、标量替换等优化手段,后面记得要去看这部分内容,然后来完善这里
  • 堆内存的分代管理,只不过是一种技术实现而已,并不是说虚拟机规范就一定要分代!
  • 所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,这种细分只是为了更好的回收管理内存。

6.方法区
  • 所有线程共享的一块内存区域
  • 方法区是一个逻辑上的概念,它不等于永久代
    • 仅仅是当时的HotSpot虚拟机设计团队,选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已。
    • 从jdk 8 开始,已经不存在永久代,替代它的一块空间叫做 “ 元空间 ”,和永久代类似,都是 JVM 规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存。
  • 它用于存储已被虚拟机加载的类型信息(类信息?、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。
  • 运行时常量池:
    • class文件中的常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
    • 符号引用:比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language。(就是直接用包名来记录引用的类吧

三、HotSpot虚拟机创建对象

  • 真的难受,看类加载的时候,提到java内存区域的知识,看java内存区域的知识,又跟我提类加载。。。
  • (先留空吧,后面看了类加载再来补充,现在先默认类已经加载到内存里面了
  1. 内存分配
    • 分配方式:
      • “指针碰撞”(Bump The Pointer):假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离(我先来:太理想了!
      • “空闲列表”(Free List):如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录(这种应该是常见的情况
    • 分配内存的线程安全问题:
      • 同步处理:对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;
      • 线程分配缓冲:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定(那么这块区域实际上变成了线程私有?
  2. 空间初始化:
    • 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值(也就是进行成员变量的默认值赋值,如果 使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。
  3. Java虚拟机对对象进行必要的设置(以下信息会放在对象头中
    • 对象是哪个类的实例、
    • 如何才能找到 类的元数据信息
    • 对象的GC分代年龄等信息
    • 等等
  4. 对象初始化:
    • 调用构造方法,即Class文件中的()
    • 其他诸如父类构造方法等,,都是这个时候才开始执行
  5. 将对象地址返回给 new 调用的地方(这个是我自己推的,存疑

四、HotSpot虚拟机对象的内存布局

  • 对象在堆中可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
  • 对象头:
    • 是用于存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态、线程持有的锁等
    • 类型指针(getclass() 方法?
    • 数组的话,还会有数组长度信息
  • 实例数据
    • 代码中定义的各类型字段内容
    • 默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放
    • 在父类中定义的变量会出现在子类之前
  • 对齐填充:这个并不是必要的,只是HotSpot虚拟机的自动内存管理系统,要求对象的大小都必须是8字节的整数倍
    • (感觉有点像c++的结构体的字节对齐

五、HotSpot虚拟机对象的访问定位

  • 创建对象当然是为了使用对象,但是怎么找到对象呢?
  • 这时候你可能会说:变量就是对象的引用啊,通过引用就可以找到对象了(突然的感想:找到正确的对象真有这么容易就好了
  • 言归正传,主流的引用有两种:
  • 句柄:
    • 首先定义:一个唯一的整数,作为对象的身份id。
    • 按我理解,相当于形成:引用-句柄,句柄-实际对象地址的结构
    • 增加多一层的好处和代码加中间层是一样的:就是把两个隔离开,在对象移动的时候,句柄可以不变,然后把句柄映射的对象地址改了就行
    • 看下图:

image.png

  • 直接指针:
    • 这个很好理解吧,就是引用直接就是对象地址
    • 好处当然是速度肥肠快,访问对象不用多做一次映射,少了开销
    • 但是他当然就没有句柄的优势了–对象移动的话,要修改对应的值
    • 看下图:

image.png

六、OOM异常分析

1.Java堆溢出
  • Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况
  • (这种溢出是比较常见的,需要分析是不是由于内存泄漏导致的内存不足,安卓现在应该不存在单纯的内存不够用吧,联系 leakcanary

2.虚拟机栈和本地方法栈溢出
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。
    • ART虚拟机应该是不支持动态扩展的
  • 出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。
3.方法区和运行时常量池溢出
  • 方法区的主要职责是用于存放类型的相关信息
  • 当前的很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到 CGLib这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。
  • 另外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性
  • 类要被垃圾回收器回收的条件是很苛刻的。
    • 所以jdk8之前,如果是生成大量动态类的场景里,处理不好,就会导致OOM异常。
    • 在JDK 8以后,元空间代替永久代,本地内存是很难被方法区占满的,所以OOM异常是很难出现的了。
4.本机直接内存溢出
  • 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常 情况,
  • 如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查
  • 应该很少是这种情况吧

七、总结

  • 大概了解了JAVA的几个内存区域,以及它们分别的作用,但是都浅尝辄止,并没有深入的去说它们的各种变化,还有联系。
  • 大概了解了OOM异常
    • 事实上,并不是说有很多种情况会导致OOM,而是JAVA虚拟机规范,对不同的内存区域,定义了OOM异常的触发条件
    • 所谓的不同情况触发的OOM异常,其实含义并不一样,因为它们代表的是不同内存区域的OOM。

八、引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值