Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • Java基础

    • Java 基础
    • Java 集合
    • JUC
    • JVM
      • JVM的垃圾回收算法
        • 引用计数法
        • 可达性分析算法(标记阶段)
        • 标记-清除算法(年轻代清除阶段)
        • 复制算法(年轻代清除阶段)
        • 标记整理算法(老年代清除阶段)
        • 分代收集算法
        • 增量收集算法
        • 分区算法(G1 收集器)
      • JVM有哪几种垃圾回收器,各自的优缺点?
        • 新生代收集器
        • 老年代收集器
        • 堆内存收集器
        • G1
        • CMS与G1的区别
        • CMS原理
        • 新生代在几次gc之后会变成老生代
        • full gc频繁,有哪些原因
      • JVM 内存模型
      • 类加载
        • 类加载机制
        • 类加载器
        • 类的卸载条件是什么?
        • 双亲委派机制
        • 双亲委派模型的优点
        • 可以更换掉双亲委派策略吗?
      • 内存泄漏
      • 项目中对应JVM的设置
        • JVM的调优参数可以在哪里设置?
        • JVM调优的参数有哪些?
        • JVM的调优工具
        • 内存泄漏的排查
      • 堆
        • 堆内存怎么查看
      • 栈
        • 本地方法栈
    • Linux
    • 设计模式
  • 框架

  • 数据库

  • 消息队列

  • 408

  • 大数据

  • 面试
  • Java基础
Iekr
2023-11-11
目录

JVM

# JVM

# JVM 的垃圾回收算法

tag: 腾讯 、 携程 、 用友 、 快手

count:23

as:垃圾回收机制有哪几个部分

GC ROOTS 一般从哪里选择。

GC root 和垃圾回收算法

GC 了解多少 介绍一下 G1

GC 回收器有哪些,区别是什么

GC 算法有哪些,为什么要分代收集,为什么新生代用复制算法,展开讲一下自己的相关了解

GC 算法,对应的应用场景

Java 内存回收,新生代和老年代使用到的垃圾回收算法

JVM 垃圾回收的整个流程

JVM 怎么找到垃圾对象(可达性分析算法)

下面介绍七种回收算法:

  1. 标记 - 清除(Mark-Sweep)算法:
    • 标记阶段标识出存活的对象。
    • 清除阶段则回收未被标记的对象。
    • 缺点是会产生内存碎片。
  2. 复制(Copying)算法:
    • 将内存分为两个相等的部分,每次只使用其中一个部分。
    • 当这部分内存用完时,就清理不再使用的对象,并将剩余的存活对象复制到另一个部分。
    • 缺点是浪费了一半的内存空间。
  3. 标记 - 压缩(Mark-Compact)算法:
    • 结合了标记 - 清除算法和复制算法的优点。
    • 标记阶段与标记 - 清除算法相同,但清除阶段会将存活的对象压缩到内存的一端,然后清理边界外的内存。
  4. 分代收集(Generational Collection)算法:
    • 基于 “弱引用” 的观察,大多数对象很快就会变得不可达。
    • JVM 将堆划分为几代,如新生代(Young Generation)和老年代(Old Generation)。
    • 新生代使用复制算法,而老年代则使用标记 - 清除或标记 - 压缩算法。
  5. 增量(Incremental)算法:
    • 目标是减少停顿时间,让 GC 工作可以和应用程序交替进行。
    • 通过逐步完成 GC 工作,使得每次只做一部分,以减少对应用程序的影响。
  6. 并行(Parallel)算法:
    • 使用多个线程同时进行垃圾收集工作。
    • 适用于多核处理器环境,可以加快垃圾回收的速度。
  7. 并发(Concurrent)算法:
    • 试图在不影响应用程序运行的前提下进行垃圾回收。
    • 垃圾回收过程与应用程序运行交错进行,尽量减少应用程序的暂停时间。

# 引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

image-20240907000248748

所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 可达性分析算法(标记阶段)

原理: 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分析。

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。

image-20240907000351558

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈 (栈帧中的局部变量表) 中引用的对象
  • 本地方法栈 (Native 方法) 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象

即使在可达性分析法中不可达的对象,也并非是 “非死不可” 的,这时候它们暂时处于 “缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;

可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

# 标记 - 清除算法(年轻代清除阶段)

原理: 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为 stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。 清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收

缺点:

  • 标记清除算法的效率不算高。
  • 在进行 GC 的时候,需要停止整个应用程序,用户体验较差。
  • 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。

标记 - 清除(Mark-and-Sweep)算法分为 “标记(Mark)” 和 “清除(Sweep)” 阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

它是最基础的收集算法,后续的算法都是对其不足进行改进得到。

image-20240907000604184

# 复制算法(年轻代清除阶段)

为了解决标记 - 清除算法的效率和内存碎片问题,复制(Copying)收集算法出现了。

原理: 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点: 复制过去以后保证空间的连续性,不会出现 “碎片” 问题。

缺点: 需要多余的内存空间。

image-20240907000752947

虽然改进了标记 - 清除算法,但依然存在下面这些问题:

  • 可用内存变小:可用内存缩小为原来的一半。
  • 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。

# 标记整理算法(老年代清除阶段)

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

原理: 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后清理边界外所有的空间。

标记 - 压缩算法的最终效果等同于标记 - 清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记 - 清除 - 压缩(Mark-Sweep-Compact)算法。

优点:

消除了标记 - 清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。 消除了复制算法当中,内存减半的高额代价。

缺点: 从效率上来说,标记 - 整理算法要低于复制算法。 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 移动过程中,需要全程暂停用户应用程序。即:STW。

image-20240907000852818

由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。

# 分代收集算法

背景: 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

  • 年轻代:每次收集都会有大量对象死去,所以可以选择” 标记 - 复制 “算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  • 老年代:由标记 - 清除或者是标记 - 清除与标记 - 整理的混合实现。老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择 “标记 - 清除” 或 “标记 - 整理” 算法进行垃圾收集。

# 增量收集算法

原理: 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

缺点: 使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

# 分区算法(G1 收集器)

原理: 分区算法将整个堆空间划分成连续的不同小区间。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

# JVM 有哪几种垃圾回收器,各自的优缺点?

tag: 携程 、 用友 、 知乎

count:8

as:Java GC 的流程

Java 的垃圾回收策略

jvm 的垃圾收集器简述一下 cms 的工作流程,跟 G1 有什么区别

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。

JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):

  • JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • JDK 9 ~ JDK20: G1

image-20240918194159348

以上是 HotSpot 虚拟机中的 7 个垃圾回收器,连线表示垃圾收集器可以配合使用。

# 新生代收集器

  • Serial: Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停 **(Stop The World)**。 image-20240909185239481

    它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

  • ParNew:ParNew 就是一个 Serial 的多线程版本,其它与 Serial 并无区别。ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果,它默认开启的收集线程数和 CPU 数量一致,可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。

    如下是 ParNew 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。 image-20240909185404236

  • ParallelScavenge:Parallel Scavenge 也是一款用于新生代的多线程收集器,与 ParNew 的不同之处是 ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 的目标是达到一个可控制的吞吐量。 image-20240909185541431

# 老年代收集器

  • SerialOld:Serial Old 收集器是 Serial 的老年代版本,同样是一个单线程收集器,采用标记 - 整理算法。 image-20240909185646915

  • ParallelOld:Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记 - 整理算法。可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。 image-20240909185710060

  • CMS(ConcurrentMarkSweep):CMS 收集器是一种以最短回收停顿时间为目标的收集器,以 “最短用户线程停顿时间” 著称。从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种 “标记 - 清除” 算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个垃圾收集过程分为 4 个步骤:

    1. 初始标记:暂停所有的其他线程,标记一下 GC Roots 能直接关联到的对象,速度较快。
    2. 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。进行 GC Roots Tracing,标记出全部的垃圾对象,耗时较长。
    3. 重新标记:修正并发标记阶段因用户程序继续运行而导致变化的对象的标记记录,耗时较短。
    4. 并发清除:开启用户线程,用标记 - 清除算法清除垃圾对象,耗时较长。

    整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS 收集器垃圾收集可以看做是和用户线程并发执行的。

    image-20240909190017428

    从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

    • 对 CPU 资源敏感;
    • 无法处理浮动垃圾;
    • 它使用的回收算法 -“标记 - 清除” 算法会导致收集结束时会有大量空间碎片产生。

    CMS 垃圾回收器在 Java 9 中已经被标记为过时 (deprecated),并在 Java 14 中被移除。

# 堆内存收集器

# G1

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

G1 收集器是 jdk1.7 才正式引用的商用收集器,现在已经成为 jdk9 默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1 进行垃圾收集的范围是整个堆内存,它采用 “化整为零” 的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念。

它具备以下特点:

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的 “标记 - 清除” 算法不同,G1 从整体来看是基于 “标记 - 整理” 算法实现的收集器;从局部上来看是基于 “标记 - 复制” 算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

image-20240909190311849

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region (这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

# CMS 与 G1 的区别

tag: 美团

count:18

as:CMS 和 G1 各自的差别优缺点讲一下

CMS 和 G1 和 ZGC 的工作流程,优缺点

cms 使用的垃圾回收算法

CMS 和 G1 收集器收集过程

G1 收集器优缺点,内部实现,G1 中为什么划分 Region

g1 hop 参数,G1 为什么低延迟

G1 回收器的优点

g1 如何回收老年代对象,G1 怎么处理浮动垃圾

  1. CMS 收集器是老年代的收集器,可以配合新生代的 Serial 和 ParNew 收 集器一起使用;G1 收集器收集范围是老年代和新生代,不需要结合其他收集器使用;

  2. G1 收集器可预测垃圾回收的停顿时间 CMS 收集器是使用 “标记 - 清除” 算 法进行的垃圾回收,容易产生内存碎片 G1 收集器使用的是 “标记 - 整理” 算法,进行了空间整合,降低了内存空间碎片。

# CMS 原理

tag:

count:4

as:CMS 四个阶段

cms 在并发标记那几个过程中出现 stw 的时候,是用户自己选择还是较短的时间

cms 是怎么实现的

CMS 用的什么算法?是在什么区域采用的?

# 新生代在几次 gc 之后会变成老生代

在 Java 中,堆内存被划分为不同的区域,其中最重要的是新生代(Young Generation)和老年代(Old Generation)。

新生代(Young Generation)

新生代是用于存放新创建的对象的区域。大多数情况下,对象会在新生代中分配空间。当一个新对象被创建时,它通常首先被分配到 Eden 区。每个新生代包含两个 Survivor 区,分别称为 From Survivor 区和 To Survivor 区。它们大小相同,并且任何时候只有一个处于使用状态。当发生 Minor GC 时,Eden 区存活的对象会被移动到空闲的 Survivor 区;如果对象在 Survivor 区已经经历过多次 GC(次数由 -XX:MaxTenuringThreshold 参数控制,默认值为 15),则会被晋升到老年代。

新生代主要分为三个区域:

  • Eden Space:新创建的对象首先在这里分配内存。
  • Survivor Spaces:通常有两个 Survivor 空间(Survivor 0 和 Survivor 1),对象在 Eden 区域中存活过至少一次 Minor GC 后,会被移动到 Survivor 空间之一。

当 Eden 区域满时,会触发一次 Minor GC。Minor GC 的过程中,Eden 区域的对象会被检查,如果对象仍然被引用,则会被移动到一个 Survivor 空间。如果对象在 Survivor 空间中经过多次 GC 后仍然存活,则会被移动到另一个 Survivor 空间。如果 Survivor 空间也满了,或者对象在 Survivor 空间中存活了一定次数(默认是 15 次),那么这个对象就会被晋升到老年代。

老年代(Old Generation)

老年代用来存储那些经过多次 Minor GC 后仍然存活的对象,或者直接分配给较大对象的空间。

老年代是存放长期存活对象的地方。当老年代的空间不足时,会触发一次 Full GC 或 Major GC,这时候不仅会清理老年代,还会清理新生代。相对于新生代,老年代的垃圾回收频率较低,因为这里存放的主要是生命周期较长的对象。

晋升到老年代的条件

对象晋升到老年代有几个条件:

  1. 年龄足够大:对象在 Survivor 区域中存活的次数达到一定的阈值(默认是 15 次),就会晋升到老年代。这个次数可以通过 -XX:MaxTenuringThreshold 参数配置。
  2. 对象太大:如果新创建的对象太大,以至于无法在 Eden 区域或 Survivor 区域中存放,那么这个对象会直接晋升到老年代。
  3. 老年代空间不足:如果在进行 Minor GC 后,发现没有足够的空间存放存活的对象,那么这些对象也会直接晋升到老年代。

MinorGC,MixedGC,FullGC 的区别

  • MinorGC:发生在新生代的垃圾回收。停止时间短(STW)。
  • MixedGC:发生在新生代和老年代的部分区域的垃圾回收。G1 收集器是特有的。
  • FullGC:发生在新生代和老年代完整的垃圾回收。停止时间长。

# full gc 频繁,有哪些原因

  • 堆内存不足:如果老年代或永久代 / 元空间的容量不足以容纳所有存活的对象,JVM 会触发 Full GC 来尝试释放更多空间。 增加堆内存大小(通过 -Xms 和 -Xmx 参数调整)。 调整老年代的比例(例如使用 -XX:NewRatio 设置新生代与老年代的比例)。 对于永久代 / 元空间,根据需要增加其大小(如 -XX:MaxPermSize 或 -XX:MaxMetaspaceSize )。
  • 对象晋升过快:大量对象在短时间内从新生代晋升到老年代,导致老年代快速填满。 减少对象晋升频率,优化代码以减少短生命周期大对象的创建。 调整新生代大小(如使用 -Xmn 参数),使其足够大以容纳更多的临时对象。 修改晋升阈值( -XX:MaxTenuringThreshold ),控制对象晋升的速度。
  • 长生命周期对象过多:应用程序中存在大量长期存活的对象,这些对象占据了老年代的大部分空间。 使用分析工具(如 VisualVM、JProfiler 等)来识别和处理潜在的内存泄漏问题。
  • 不合适的垃圾收集器选择:不同的应用场景适合不同的垃圾收集器。如果选择了不适合当前工作负载的 GC 策略,可能会导致 Full GC 频繁。 根据应用特点选择合适的垃圾收集器,例如 G1、CMS 或 ZGC 等。
  • 内存泄漏:程序中的某些部分错误地保持了对不再需要的对象的引用,导致这些对象不能被 GC 回收。 使用内存分析工具查找并修复内存泄漏点。

# JVM 内存模型

tag:

count:26

as:java 内存管理怎么实现的

JVM 中的内存结构 对象存放在哪

JVM 内存分为那几块

jvm 内存区域

JVM 内存区域划分,分别的作用,GC 发生在哪里

jvm 内存模型,各部分作用。 jvm 内存模型,线程共享的区域是那些

JVM 内存结构?静态变量在哪?常量在哪?构造函数在哪?

JVM 原理,内存管理,直接内存是否受到 JVM 的管理?

JVM 的运行时内存结构,细讲了一下堆

image-20240906232840491

image-20240906232848597

JVM 两个子系统和组件,运行时数据区、本地接口、类加载器、执行引擎

运行时数据区包括:本地方法栈、Java 虚拟机栈、堆、方法区、程序计数器

Java 虚拟机栈:存放局部变量,参与方法的调用与返回

堆:存放对象的实例

方法区:存放常量、静态变量等

线程私有的:

  • 程序计数器:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

    在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了、

  • 虚拟机栈:除了一些 Native 方法调用是通过本地方法栈实现的 (后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

  • 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

线程共享的:

  • 堆:堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  • 方法区:当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

  • 直接内存 (非运行时数据区的一部分):直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。

    直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

# 类加载

tag: 携程

count:7

as:类的生命周期

ClassNotFound、类加载原理

class 文件存一些什么

Class 类会占用多少内存空间?

一个 Java 代码是如何运行的,底层过程

Java 8 里面的类加载器

java 加载的最核心的类是什么

类装载的执行过程

其中类加载的过程包括了 加载 、 验证 、 准备 、 解析 、 初始化 五个阶段。

在这五个阶段中, 加载 、 验证 、 准备 和 初始化 这四个阶段发生的顺序是确定的,而 解析 阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定 (也成为动态绑定或晚期绑定)。

另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

image-20240918194531357

  1. 加载:加载 Class 文件,在方法区中存储类的信息并在堆内存中类的 Class 对象。

  2. 验证:格式检查和判断符号引用是否正确。

  3. 准备:为静态变量开辟内存空间,并为其初始化值。

  4. 解析:将类中指令的符号引用转换为直接引用。

  5. 初始化:对静态变量和静态块初始化。

  6. 使用:jvm 去执行程序代码,就比如:new 对象,和调用静态方法等。

  7. 卸载:当程序代码执行完之后,jvn 就会销毁 class 对象。

image-20241029232505877

# 类加载机制

tag: 携程 、 字节 、 趣链科技 、 恒生

count:29

as:类加载器加载的先后顺序,如果一个父类的方法不想被子类继承

类加载器有哪些类 聊一下这个过程

类加载机制?双亲委派机制?双亲委派机制的好处?类加载器?类加载过程?验证的目的?什么样的代码会危害 JVM 安全?

类加载生命周期

类加载过程,几种类加载器介绍。

类的加载的过程

# 类加载器

  1. 启动类加载器(BootStrap ClassLoader):会去加载 / JAVA_HOME/jre/lib 中的类。
  2. 扩展类加载器(ExtClassLoader): 会去加载 / JAVA_HOME/jre/lib/ext 中的类。
  3. 应用类加载器(APPClassLoader): 会去加载 ClassPath 下的类。
  4. 自定义加载器:实现自定义的加载规则。

image-20241029232257578

# 类的卸载条件是什么?

  1. 当类的实例全部被 gc 了,也就是说 jvm 中没有类的实例了,就会进行卸载。

  2. 当类的 classloader 被 gc 了,就会进行老垃圾回收。

  3. 当类的 **.class 没有被任何的引用 ** 时,也就是我们无法通过发射获取实例,就会进行卸载。

# 双亲委派机制

tag: 趣链科技

count:15

as:双亲委派机制的设计有什么好处?

Java 双亲委派了解吗?有哪些加载器?详细讲一讲

双亲委派是什么,为什么这么设计?(答到了安全性,顺序性和避免重复漏了)

双亲委派机制的作用

如何打破双亲委派模型 (这里我说的例子不太好说重写 String,然后后面就属于衍生出来的场景题了)

怎么打破双亲委派模型?要注意什么

类加载的双亲委派机制

在加载来的时候,会先委派上级去加载类,如果上级也有上级就会继续向上委派。如果上级加载了类,则下级就无需加载了。如果上级没有加载类,则下级才会尝试去加载类。

image-20241029232313730

# 双亲委派模型的优点

  1. 因为会先委派上级去加载类,如果加载成功则下级加载器就无需加载类,保证了类加载的唯一性。
  2. 为了安全,保证类库中的 API 不会被修改。(就比如:我们编写了一个 String 类,在 String 类中有一个 main 方法。在进行类加载的时候就会报错,因为启动类加载器会去加载已有的 String,会发现没有 main 函数,直接报错,防止库里的 API 被修改)

# 可以更换掉双亲委派策略吗?

可以的,通过继承 ClassLoader 重写 loadClass 方法,去覆盖调用我们的双亲委派策略来进行自定义。

# 内存泄漏

tag: 腾讯

count:1

as:

# 项目中对应 JVM 的设置

tag: 美团 、 阿里

count:5

as:jvm 调优步骤,举两种情况。

jvm 调优 (说了几个可以调整的参数,什么新生代比例,G1 什么容忍上限,剩下的说不上来了) JVM 调优的经历(都是线下调优)

# JVM 的调优参数可以在哪里设置?

  • 在 war 部署到 Tomcat 时时候,通过 / TOMCAT_HOME/bin/catanial.sh 进行设置。
  • 在部署 jar 包的时候通过在启动指令上进行设置。就比如:java -Xmas 100m -Xmax 1024m -jar xxx.jar。

# JVM 调优的参数有哪些?

  • 设置堆的大小。-Xmas -Xmax :设置堆的初始大小和最大值,初始值一般为物理内存的 1/64,最大值为物理内存的 1/4。
  • 设置虚拟栈的大小。-Xss 128k
  • 设置年轻代中 Eden 和幸存者区比例。-XXsurvivorRatio = 8 也就是 8 : 1 : 1。
  • 设置新生代晋升老年代的阈值。-XX:MaxTenuringThreshold 默认为 15。
  • 设置垃圾回收器。 -XX:+UseG1GC。

# JVM 的调优工具

命令工具

  • jps:查询进程的状态。

  • jstack:查看线程的详细信息。

  • jmap:查看堆转的信息。

  • jhat:堆转储快照分析工具。

  • jstat:JVM 统计检查工具。

可视化工具

  • jconsole:查询 JVM 的信息的可视化工具。

  • VisualVM:监控线程和内存的情况。

# 内存泄漏的排查

java 的内存泄漏主要就是指堆内存的溢出。我们可以通过 jmap 或者设置 JVM 的参数获取堆内存的 dump 文件。通过 VisualVM 区分析堆内存的信息,定位内存溢出的代码,最终去修改代码。

# 堆

tag: 核桃编程

count:4

as:

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存 (Young Generation)
  2. 老生代 (Old Generation)
  3. 永久代 (Permanent Generation)

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

image-20240906233845108

JDK 8 版本之后 PermGen (永久代) 已被 Metaspace (元空间) 取代,元空间使用的是本地内存。 (我会在方法区这部分内容详细介绍到)。

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1 (Eden 区 ->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。不过,设置的值应该在 0-15,否则会爆出以下错误:

MaxTenuringThreshold of 20 is invalid; must be between 0 and 15
1

# 堆内存怎么查看

tag: 知乎

count:1

as:

# 栈

tag: 阿里

count:3

as:JVM 的栈和我们平时的栈是一个东西嘛

Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的 (后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

image-20240906233450750

局部变量表

主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

image-20240906233521467

操作数栈

主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接

主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。

image-20240906233543142

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

除了 StackOverFlowError 错误之外,栈还可能会出现 OutOfMemoryError 错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。

简单总结一下程序运行中栈可能会出现两种错误:

  • StackOverFlowError : 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError : 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。

# 本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

编辑 (opens new window)
上次更新: 2025/01/01, 10:09:39
JUC
Linux

← JUC Linux→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式