当前位置:网站首页>JVM 内存管理 你知道多少

JVM 内存管理 你知道多少

2022-07-28 12:06:00 daydreamed

JVM 内存管理 你知道多少


1、概述

​  “Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。”

Java 虚拟机运行时的数据区:
在这里插入图片描述


补充:

​  线程隔离的数据区:

  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 程序计数器(Program Counter Register)

​  线程共享的数据区:

  • 方法区(Method Area)
  • 堆(Heap)



2、Java 内存区域

  • 程序计数器(Program Counter Register)

    ​  程序计数器 是 用于存放下一条(Java 虚拟机)字节码指令所在单元的地址 的地方,该区域所在的内存 属于 “线程私有”内存。

    ​  如果 线程 正在执行的是一个 Java 方法,这个计数器的值为正在执行的 (Java 虚拟机)字节码指令的地址;如果 线程 正在执行的是本地(Native)方法,则这个计数器的值为空。

  • 虚拟机栈(VM Stack)

    ​  虚拟机栈 是 用于存放 栈帧(Stack Frame) 的地方,每当一个(Java)方法被执行时,Java 虚拟机 就会创建一个 栈帧,栈帧 存储着 局部变量表、操作数栈、动态连接、方法出口等信息,一个(Java)方法 从 调用 到 执行完毕 的过程 对应着 栈帧 入栈 到 出栈 的过程,该区域所在的内存 属于 “线程私有” 内存。

  • 本地方法栈(Native Method Stack)

    ​  本地方法栈 的功能与 虚拟机栈 相似,唯一不同的是 虚拟机栈 服务于 虚拟机执行 Java 方法,本地方法栈 服务于 虚拟机执行本地方法,该区域所在的内存 属于 “线程私有” 内存。

  • 堆(Heap)

    ​  堆 是 用于存放(Java)对象实例 的地方,该区域所在的内存 属于 “线程共享” 内存。

    ​  堆 是 垃圾收集器 管理的内存区域,因此也被称为 “GC堆”。

    ​  堆的大小 可以通过 (Java 虚拟机)参数 -Xmx 和 -Xms 进行设定。

  • 方法区(Method Area)

    ​  方法区 是用于存放 被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据 的地方,该区域所在内存 属于 “线程共享” 内存。


扩展:

  • 运行时常量池(Runtime Constant Pool)

    ​  运行时常量池 是 方法区 的一部分,用于存放 编译期生成的各种 字面量 与 符号引用。

  • 直接内存(Direct Memory)

    ​  直接内存 并不属于 Java 内存区域,但这部分内存也被频繁地使用到,例如:通过 Native 函数库直接分配 堆外内存,减少数据传输损耗,提高性能。

    ​  直接内存 只受本机内存限制。




3、Java 对象

3.1、对象的创建

​  在 Java 语言中,创建一个对象仅仅需要一个关键字 new ,但在 Java 虚拟机 中,创建一个对象则需要经过以下几个步骤。

​  1、当 Java 虚拟机 遇到一条字节码 new 指令时,首先会去检查 这个指令的参数 是否能在 常量池 中定位到 一个类的符号引用,并且检查这个 符号引用代表的类 是否 已被加载、解析和初始化过,如果没有就先执行 该类的类加载过程。

​  2、在类加载检查通过后,虚拟机 通过 “指针碰撞”(或 “空闲列表”)分配方式 在 堆 中为 该对象分配内存空间。

​  3、内存空间分配完成之后,虚拟机 必须将 分配到的内存空间(但不包括对象头)都初始化为 零值。

​  4、接下来,虚拟机 还将对 对象 进行必要的设置,如:该对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(延后至 Object:hashCode() 的调用 才进行计算)、对象的 GC 分代年龄等,这些信息均存放于 对象的对象头中。

​  5、至此,从虚拟机的角度来看,一个新的对象已经诞生,但从 Java 程序的角度来看,对象的创建才刚刚开始(构造函数还没有执行),接下来即是 执行构造函数(在虚拟机中体现为 <init>() 方法)完成对 对象的初始化,正式完成对 对象的构造。


3.2、对象的内存布局

​  对象 在 堆 中的内存布局可以划分为三个部分:

  • 对象头(Header)
    • 对象自身的运行数据:哈希码、GC 分代年龄、锁状态标志等
    • 类型指针:指向 对象的类型元数据(即 是哪个类的实例)
  • 实例数据(Instance Data)
    • 字段内容
  • 对其补充(Padding)
    • 占位符

3.3、对象的访问定位

​  Java 程序在使用对象时,会通过 栈 上的 reference 数据来操作 堆 中的具体对象,而 reference(引用) 访问定位 堆 中的对象可分两种方式:

  • 句柄访问

    ​  堆 中划分出一块内存作为句柄池(句柄包含 对象实例数据地址 与 类型数据地址),reference 中存储的就是 对象的句柄地址。

    ​  优势:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针。

在这里插入图片描述

  • 直接指针访问

    ​  reference 中直接存储 对象地址。
    在这里插入图片描述

     优势:相较于句柄方式 节省了一次指针定位的时间开销,提高了访问速度。


3.4、对象的引用

  • 强引用(Strongly Reference)

    ​  指 在程序代码中的引用赋值( Object obj = new Object() ),只要 强引用关系 还存在,垃圾收集器 就永远不会回收掉 被引用的对象。

  • 软引用(Soft Reference)

    ​  指 一些还有用、但非必须的对象,在系统将要发生 内存溢出异常 前,垃圾收集器 会将这些对象进行回收。

    ​  JDK1.2 后,提供 SoftReference 类 实现 软引用。

  • 弱引用(Weak Reference)

    ​  指 一些非必须的对象,当 垃圾收集器 开始工作时,无论当前内存是否充足,都会将这些对象进行回收。

    ​  JDK1.2 后,提供 WeakReference 类 实现 弱引用。

  • 虚引用(Phantom Reference,又称 “幽灵引用” 或 “幻影引用”)

    ​  一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,它存在的唯一目的只是:为了能在这个对象被 垃圾收集器 回收时收到一个系统通知。

    ​  JDK1.2 后,提供 PhantomReference 类 实现 虚引用。


3.5、对象的存亡

​  当一个对象 “死亡” 时,GC(Garbage Collection,垃圾收集)会将该对象清除 并 回收分配给该对象的内存空间。那么,GC 怎么判断一个对象是否 “死亡” 了呢?

​  GC 判断一个对象是否 “死亡” 通常有以下两种算法:

  • 引用计数算法

    ​  在 对象 中添加一个 引用计数器,每当有一个地方引用它时,计数器值加一;每当有一个地方的引用失效时,计数器值减一;任何时刻计数器值为 零 的对象就是 “已死亡” 的对象(即 该对象不可能再被使用)。

    • 优点:原理简单,判断效率高
    • 缺点:占用额外的内存空间、无法解决对象间的循环引用问题
  • 可达性分析算法

    ​  以 “GC Roots”(根对象)作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为 “引用链(Reference Chain)”,如果 某个对象 到 “GC Roots” 间没有任何 引用链 相连(或者 用图论的话来说就是 从 “GC Roots” 到 这个对象不可达)时,该对象 “已死亡”(即 该对象不可能再被使用)。

    • 优点:解决了对象间的循环引用问题
    • 缺点:相较于 引用计数算法 判断效率 没那么高

    扩展:Java 中的 “GC Roots”

    • 虚拟机栈(栈帧中的本地变量表)中 引用的对象,如:当前正在运行的方法 所使用到的 参数、局部变量、临时变量等
    • 方法区中 类静态属性 引用的对象,如:Java类 的引用类型静态变量
    • 方法区中 常量 引用的对象,如:字符串常量池里的引用
    • 本地方法栈中 JNI(Native 方法)引用的对象
    • Java 虚拟机 内部的引用,如:基本数据类型对象的 Class 对象、一些常驻的异常对象(NullPointException、OutOfMemoryError)、系统类加载器
    • 所有被同步锁(synchronized 关键字)持有的对象
    • 反映 Java 虚拟机 内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
    • 不同垃圾收集器 和 不同回收的内存区域 中的 “临时性” 对象

在这里插入图片描述


补充:

  • 对象的 “缓刑”:

​  当一个对象在 可达性分析算法 中判定为 不可达对象时,该对象会进入 “缓刑” 阶段:检测该对象是否有必要执行 finalize() 方法。

​  如果对象没有覆盖 finalize() 方法(或 finalize() 方法已经被虚拟机调用过),则 虚拟机 认为该对象 没必要执行 finalize() 方法,真正宣布其 “死亡”;

​  否则,虚拟机 认为该对象 有必要执行 finalize() 方法,并将该对象放入 F-Queue 队列中,然后由虚拟机 自动建立的、低调度优先级 的 Finalizer 线程 去执行它的 finalize() 方法(这里的 “执行” 是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束)。

  • 对象的 “重生”:

​  当对象在 finalize() 方法中 重新与引用链上的任何一个对象建立关联,如:把自己(this 关键字)赋值给某个类变量或者对象的成员变量,即 该对象获得 “重生”(移出 “即将回收” 的集合)。

​  相反,如果该对象在 finalize() 方法中 没能重新与引用链上的任何一个对象建立关联,该对象也将面临 “死亡”。




4、垃圾收集算法

4.1、分代收集理论

​  分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说和一条经验法则之上。

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的

  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡

  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数

​  基于 分代收集理论,垃圾收集器 一致的设计原则为:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。


补充:

Java 堆的分代

  • 新生代(Young Generation)
  • 老年代(Old Generation)

分代收集

  • 部分收集(Partial GC):
    • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集
    • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集
    • 混合收集(Mixed GC):既对 整个新生代进行垃圾收集,也对 部分老年代进行垃圾收集
  • 整堆收集(Full GC):对整个 Java 堆 和 方法区 进行垃圾收集


4.2、标记 - 清除算法

​  算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象(或统一回收掉所有未被标记的对象)。

​  缺点:产生碎片空间。

在这里插入图片描述


4.3、标记 - 复制算法

​  将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块上,然后再把已使用过的内存空间一次性清理掉。

​  缺点:可用内存减半。

在这里插入图片描述


4.4、标记 - 整理算法

​  算法的标记过程 与 “标记 - 清除算法” 一致,整理过程则是:将所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

​  缺点:移动 大对象 可能造成较久的停顿。

在这里插入图片描述



以上全部内容均来自于对 周志明老师的 《深入理解 Java 虚拟机》的部分内容的总结。
我的总结于这本书的内容相比,无亦于水滴同大川,真心推荐大家去细读这本书。
以上内容仅用于个人学习。

原网站

版权声明
本文为[daydreamed]所创,转载请带上原文链接,感谢
https://blog.csdn.net/weixin_51123079/article/details/126011158