Posted in

JVM 核心知识点整理(内存模型),收藏再看!_AI阅读总结 — 包阅AI

包阅导读总结

1. 关键词:JVM、内存模型、对象、元空间、引用

2. 总结:本文主要介绍了 JVM 内存模型的核心知识点,包括内存结构各部分(堆、虚拟机栈等)的作用和特点,对象的创建、内存分配、生命周期及访问方式,还阐述了永久代和元空间的区别,以及四种引用类型对对象生命周期的影响。

3. 主要内容:

– JVM 内存结构:

– 堆:存放对象实例及数组,分年轻代(含 Eden 区、Survivor 区)和老年代。

– 虚拟机栈:存储方法相关信息,线程私有。

– 本地方法栈:服务 Native 方法。

– 方法区:存储类型等信息,1.8 后改为元空间。

– 程序计数器:记录线程执行位置,线程私有。

– 对象相关:

– 生命周期:创建流程,在不同代的分配和转移。

– 内存分配:指针碰撞和空闲列表两种方式。

– 内存布局:对象头、类型指针、实例数据、对齐填充。

– 访问方式:句柄和直接指针。

– 元空间:

– 与永久代区别:存储位置、内容不同。

– 废弃永久代原因:大小难确定、为融合 VM、减少 GC 复杂度等。

– 引用类型:强、软、弱、虚引用,影响对象生命周期。

思维导图:

文章地址:https://mp.weixin.qq.com/s/H1XAPbxaR4N-CjZj4pziOw

文章来源:mp.weixin.qq.com

作者:程序员清风

发布时间:2024/8/23 7:13

语言:中文

总字数:4024字

预计阅读时间:17分钟

评分:83分

标签:JVM,内存模型,堆,虚拟机栈,方法区


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

点击关注公众号,“技术干货”及时达!

内存结构

堆:

  • 存放对象实例, 几乎所有的对象实例以及数组都在这里分配内存。

虚拟机栈:

    • 存储了方法的局部变量表, 操作数栈,动态连接和返回地址等地址。
    • 每个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
    • 栈内存为线程私有的空间, 每个线程都会创建私有的栈没存, 生命周期与线程相同。
    • 除了一些 Native方法调用是通过本地方法栈实现的, 其他所有Java方法调用都是栈来实现的。

本地方法栈:

    • 本地方法栈使用到的 Native方法(例如 C++ 程序)服务。

方法区:

  • 存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

永久代和元空间的区别:

    • 永久代使用的内存区域是 JVM进程所使用的区域,大小受 JVM的大小限制。
    • 元空间使用的是物理内存区域,元空间大小只受物理内存大小的限制。
    • 元空间只存储类的元信息,而静态变量和运行时常量池都在堆中。

程序计数器:

  • 一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
  • 每条线程都有一个独立的程序计数器,各线程之间的计数器互不影响,为线程私有。

程序计数器的作用:

  • 多线程的情况下,用来记录当前执行的位置,当线程切换回来时能正确运行。
  • 字节码解释器通过改变程序计数器来以此读取指令,从而实现代码的流程控制: 如: 选择, 循环, 顺序执行, 异常处理等。

程序计数器是唯一不会出现OutOfMemoryError异常的内存区域,其生命周期随着线程的创建而生,线程的销毁为亡。

堆结构

基于分代收集理论设计,分为年轻代和老年代,其中年轻代又分为Eden区和Survivor 0区和Survivor 1区(Survivor0和Survivor1也叫From和To区)

  • 默认情况下,老年代占总的堆内存的2/3,年轻代占1/3

什么E:S0:S1默认是8:1:1

新生代中有98%的对象是朝生夕灭的,每次MionrGC后存活的对象应该小于等于2%。

所以采用复制算法的新生代似乎可以不用将内存分成大小相等的两块了,但考虑到实验偏差以及实际情况的多样性。

默认预留了10%的内存用于存放存活对象。

堆内存大小

初始内存默认为电脑物理内存大小的1/64,最大内存默认为电脑物理内存的1/4,但是堆空间的大小可以调节。

符号引用

在JVM中,一个类的方法名、类名、修饰符、返回值等等都是一系列的符号,而这些符号都是一个个的常量,存储在常量池中。

同时这些个符号被加载到内存后,都有对应的内存地址,而这些内存地址就是直接引用。

动态链接

符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。

另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

方法出口

方法出口就是方法执行完成后,需要返回的一些信息。

一般来说,方法正常退出时,方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。

而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

操作数栈

就是存放方法运行过程中产生的一些临时数据,目的是为了计算,以栈的数据结构进行存储。

局部变量表

方法内部的变量都是存放在局部变量表中。

如果是对象类型,局部变量表中存放的是堆给对象分配的内存地址,也就是指针,而不是对象直接存在局部变量表中。

元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代(方法区的落地实现)来进行垃圾回收。

JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

HotSpots取消了永久代,那么是不是也就没有方法区了呢?

  • 不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。

它和永久代有什么不同的?

存储位置不同:

  • 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。

存储内容不同:

  • 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。
  • 现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

为什么要废弃永久代,引入元空间?

在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。

它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。

移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

废除永久代的好处:

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,不会遇到永久代存在时的内存溢出错误。

将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。

将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

对象

对象的生命周期

对象的创建流程:

开始 new一个对象

进行常量池检查

  • 看能否在常量池中定位到这个类的符号引用, 定位不到则加载类

分配内存空间

  • 指针碰撞: GC不带压缩功能, Serial和ParNew

内存空间初始化为零值:

  • 保证了对象的实例字段在不赋初始值就直接使用, 程序能访问到这些字段的数据类型所对应的零值

必要信息设置

init()

一个对象产生到灭亡的过程

新产生的对象优先分配在Eden区。

当Eden区满了或放不下了进行GC,这时候其中存活的对象会复制到From区。

如果From区放不下则会全部进入老年代,然后Eden内存全部清除。

之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和From区存活下来的对象复制到To区。

同理,如果存活下来的对象To区都放不下,则这些存活下来的对象全部进入年老代,之后回收掉Eden区和From区的所有内存。

如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。

当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是最需要减少的,因为耗时很严重)。

对象的内存分配方式

指针碰撞:

假设堆中的内存是绝对规整的, 所有用过的内存放一边, 未使用内存放另一边, 中间边界线就可以类比为指针, 内存分配就是把指针向未分配的区域挪一段与对象大小相等的距离, 这就是指针碰撞。

空闲列表:

如果堆中的内存不是很规整的, 已使用和未使用的内存就会相互交错, 这个时候就要维护一个列表来记录所有已使用和未使用的内存块, 在分配内存时从列表找到一块足够大的空间划分给对象实例, 并更新内存列表。

分配方法 说明 收集器
指针碰撞 内存地址是连续的(新生代) Serial和 ParNew收集器
空闲列表 内存地址不连续(老年代) CMS收集器和 Mark-Sweep收集器

对象的内存布局

在堆内存中, 一个对象的的存储布局可以分为三个区域:

对象头:

  • 存储对象自身的运行时数据(哈希吗+GC分代年龄+锁状态标准)
  • 类型指针: 类元素的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例

实例数据:

  • 存储对象真正的有效信息, 例如: 非静态变量也会存入堆空间

对齐填充:

  • JVM内对象都采用8byte对齐, 不够8byte整数倍的就需要通过对齐填充来补全

如何访问一个对象

对象的访问方式由虚拟机决定, 目前主流的访问方式有以下两种

句柄:

  • 使用句柄的话, 堆中会专门划分出一块内存来作为句柄池, reference中存储对象句柄的地址, 句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。

直接指针:

  • 直接访问, reference中存储的就是对象的地址, 节省了一次指针定位的开销

强、软、弱、虚引用

四种引用的目的是让程序自己决定对象的生命周期,通过垃圾回收器对这四种引用做不同的处理,来实现对象生命周期的改变。

强引用:

如果一个对象具有强引用,那垃圾收器绝不会回收它

当内存空间不足,宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用对象来解决内存不足的问题。

如:Object obj = new Object();这种就是强引用。

软引用:

在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。

如果这次回收还没有足够的内存,才会抛出内存溢出异常。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。

弱引用:

在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。

  • 只要垃圾回收,不管内存够不够用,弱引用都会被回收。

使用弱引用的方式是类WeakReference。

虚引用:

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(RefenenceQueue)联合使用。

  • 提供了一种确保对象被 Finalize 以后,做某些事情的机制。

设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理