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

简介: 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 以后,做某些事情的机制。

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

相关文章
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
2天前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。
|
6天前
|
存储 Java Linux
【JVM】JVM执行流程和内存区域划分
【JVM】JVM执行流程和内存区域划分
28 1
|
8天前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
15 1
|
19天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
76 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
1天前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
4天前
|
存储 安全 Java
JVM内存模型
JVM内存模型
|
2月前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你
|
2月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
2月前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区