JVM核心知识点整理(内存模型),收藏再看!

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: JVM核心知识点整理(内存模型),收藏再看!

内存结构

堆:

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

虚拟机栈:

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

本地方法栈:

  • 和虚拟机栈非常相似,区别如下:
  • 虚拟机栈执行 Java 方法服务。
  • 本地方法栈使用到的 Native方法(例如 C++ 程序)服务。

方法区:

  • 各个线程共享的内存区域。
  • 存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
  • 具体实现: 永久代,元空间。

永久代和元空间的区别:

  • JDK1.8之前是永久代。
  • JDK1.8之后是元空间。
  • 存储位置:
  • 永久代使用的内存区域是 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
  • 空闲列表: GC带压缩功能, CMS

内存空间初始化为零值:

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

必要信息设置

  • 对象类的元数据
  • 对象的哈希码
  • GC分代年龄 -> 对象头

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();这种就是强引用。

软引用:

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

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

  • 使用软引用的方式是SoftReference。

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

弱引用:

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

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

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

虚引用:

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

  • 主要作用是跟踪对象垃圾回收的状态。
  • 提供了一种确保对象被 Finalize 以后,做某些事情的机制。

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

相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1055 55
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
1050 6
|
9月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
577 5
|
9月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
2799 1
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
1373 166
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
783 29
JVM简介—1.Java内存区域
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
265 6
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
408 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?