JustNote
  • Introduction
  • DesignPattern
    • 七大原则
      • 开闭原则(OCP)
      • 依赖倒置原则(DIP)
      • 单一职责原则(SRP)
      • 接口隔离原则(ISP)
      • 迪米特法则(LoD)
      • 里氏代换原则(LSP)
      • 合成复用原则(CRP)
    • 创建型模式
      • 简单工厂模式
      • 工厂方法模式
      • 抽象工厂模式
      • 建造者模式
      • 单例模式
      • 原型模式
    • 结构型模式
      • 外观模式
      • 装饰模式
      • 适配器模式
      • 享元模式
      • 组合模式
      • 桥接模式
      • 代理模式
    • 行为型模式
      • 模板方法模式
      • 迭代器模式
      • 策略模式
      • 解释器模式
      • 观察者模式
      • 备忘录模式
      • 命令模式
      • 中介者模式
      • 责任链模式
      • 访问者模式
      • 状态模式
  • Java
    • Java Core
      • JVM 如何加载类
      • JVM 垃圾回收
      • JVM G1GC
      • JVM G1GC Q&A
      • JVM 与 Hbase
      • JVM ZGC Overview
      • JVM ZGC 内存管理
      • JVM ZGC 线程
      • JVM ZGC 垃圾回收
      • JVM ZGC 日志分析
      • JVM ZGC 参数调优
    • checkstyle
  • Golang
    • 源码阅读
      • Goroutines
      • Channel
    • gRPC
      • 1、快速开始
      • 2、什么是gRPC
      • 3、gRPC概念梳理
      • 4、基于Golang的gRPC入门
      • 5、gRPC组件ProtocolBuffers介绍
      • 6、gRPC组件Http 2.0
      • 7、错误处理和Debug
      • 8、gRPC身份验证
      • 9、服务注册与发现
      • 10、gRPC与gRPC Gateway
      • 11、gRPC与分布式链路追踪
  • Scala
    • 数据结构与算法
      • 数组
      • 队列
    • 函数式编程
      • 高阶函数
      • 偏函数
    • Immutable Collection
      • List
    • Mutable Collection
      • Array
    • 常见函数操作
      • A
        • aggregate
        • andThen
        • appended
        • appendedAll
      • C
        • chain
        • collect
        • collectFirst
        • combinations
        • compose
        • concat
        • cond
        • condOpt
        • const
        • contains
        • containsSlice
        • copyToArray
        • corresponds
        • count
        • curried
      • D
        • diff
        • distinct
        • distinctBy
        • drop
        • dropRight
        • dropWhile
      • E
        • empty(PartialFunction)
        • empty(collections)
        • endsWith
        • exists
      • F
        • fill
        • filter
        • filterKeys
        • filterNot
        • find
        • findLast
        • flatMap
        • flatten
        • fold
        • foldLeft
        • foldRight
        • forall
        • foreach
        • fromFunction
      • G
        • getOrElse (Map)
        • getOrElse (Option)
        • groupBy
        • groupMap
        • groupMapReduce
        • grouped
      • H
        • head
        • headOption
      • I
        • indexOf
        • indexOfSlice
        • indexWhere
        • indices
        • init
        • inits
        • intersect
        • isDefinedAt (Map)
        • isDefinedAt (Seq)
        • isEmpty
        • isTraversableAgain
      • K
        • keys
      • L
        • last
        • lastIndexOf
        • lastIndexOfSlice
        • lastIndexWhere
        • lastOption
        • length
        • lift
      • M
        • map
        • mapConserve
        • mapValues
        • max
        • maxBy
        • maxByOption
        • maxOption
        • min
        • minBy
        • minByOption
        • minOption
        • mkString
      • N
        • nonEmpty
      • O
        • orElse
      • P
        • padTo
        • par
        • partition
        • partitionMap
        • patch
        • permutations
        • prefixLength
        • prepended
        • prependedAll
        • product
      • R
        • range
        • reduce
        • reduceLeft
        • reduceLeftOption
        • reduceOption
        • reduceRight
        • reduceRightOption
        • reverse
        • reverseIterator
        • reverseMap
        • runWith
      • S
        • sameElements
        • scan
        • scanLeft
        • scanRight
        • search
        • segmentLength
        • size
        • slice
        • sliding
        • sortBy
        • sortWith
        • sorted
        • span
        • splitAt
        • startsWith
        • sum
      • T
        • tabulate
        • tail
        • tails
        • take
        • takeRight
        • takeWhile
        • transpose
        • tupled
      • U
        • unfold
        • union
        • unlift
        • untupled
        • unzip
        • unzip3
        • updated
      • V
        • values
        • view
      • W
        • withFilter
        • withDefault
        • withDefaultValue
      • Z
        • zip
        • zipAll
        • zipWithIndex
      • map
      • flatmap
      • filter
      • reduceLeft
      • foldLeft
    • Futures
      • Method with future as return type
      • Non blocking future result
      • Chain futures using flatMap
      • Chain futures using for comprehension
      • Future Option with for comprehension
      • Future Option with map
      • Composing Futures
      • Future Sequence
      • Future Traverse
      • Future foldLeft
      • Future reduceLeft
      • Future firstCompletedOf
      • Future zip
      • Future zipWith
      • Future andThen
      • Future configure threadpool
      • Future recover
      • Future recoverWith
      • Future fallbackTo
      • Future promise
    • Akka
  • Algorithm
  • Docker
  • Kubernetes
    • 二进制安装kubernetes
      • 00.从零开始
  • Architecture
    • Infrastructure
      • Opentracing
      • Jaeger && ZipKin
      • SkyWalking
      • Consul
      • Envoy
      • Service Mesh
      • Service Mesh: Istio 详解
      • Service Mesh: 基于 Istio 的落地实践(一)
    • CAS
      • CAS Server
      • CAS Service Management
      • CAS 集成LDAP
      • CAS 集成gitlab
      • CAS SSO & SLO
      • CAS Gitbook
    • xCAT
  • Netty
  • DDD
  • Reactive Programming
    • Reactor
      • Publisher
      • Subscriber
      • Subscription
      • Processor
    • WebFlux
  • Gitlab
    • Git Hook
  • CICD
    • Jenkins
      • Kubectl
Powered by GitBook
On this page
  • JVM 中的内存区域划分
  • 第一,程序计数器(PC,Program Counter Register)。
  • 第二,Java 虚拟机栈(Java Virtual Machine Stack)
  • 第三,堆(Heap)
  • 第四,方法区(Method Area)。
  • 第五,运行时常量池(Run-Time Constant Pool)
  • 第六,本地方法栈(Native Method Stack)
  • 引用计数法与可达性分析
  • Stop-the-world 以及安全点
  • 垃圾回收的三种方式
  • Java 常见的垃圾收集器有哪些
  • 垃圾回收过程的理解
  • 补充
  • Q&A
  • 参考文献

Was this helpful?

  1. Java
  2. Java Core

JVM 垃圾回收

PreviousJVM 如何加载类NextJVM G1GC

Last updated 6 years ago

Was this helpful?

官方JVM

JVM 中的内存区域划分

第一,程序计数器(PC,Program Counter Register)。

在 JVM 规范中,每个线程都有它自 己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方 法,则是未指定值(undefined)。

第二,Java 虚拟机栈(Java Virtual Machine Stack)

早期也叫 Java 栈。每个线程在创建时 都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方 法调用。 前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧, 通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧 会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作 只有两个,就是对栈帧的压栈和出栈。 栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定 义等。

第三,堆(Heap)

它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建 的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定 的“Xmx”之类参数就是用来指定最大堆空间等指标。 理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一 步的细分,最有名的就是新生代、老年代的划分。

第四,方法区(Method Area)。

这也是所有线程共享的一块内存区域,用于存储所谓的元 (Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。 由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。

第五,运行时常量池(Run-Time Constant Pool)

这是方法区的一部分。如果仔细分析过反 编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是 常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在 运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。

第六,本地方法栈(Native Method Stack)

它和 Java 虚拟机栈是非常相似的,支持对本地 方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。

上面各个区域的之间的关系,就可以使用下面的图来进行。

为了更加直观和清晰的理解,画出了下面的一张内存区域图。

引用计数法与可达性分析

垃圾回收,顾名思义,便是将已经分配出去的,但却不再使用的内存回收回来,以便能够再次分 配。在 Java 虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间。这里便涉及了一个关键 的问题:如何辨别一个对象是存是亡?

我们先来讲一种古老的辨别方法:引用计数法(reference counting)。它的做法是为每个对象 添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说 明该对象已经死亡,便可以被回收了。

它的具体实现是这样子的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是 说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

举个例子,假设对象 a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b。在这种情况下,a 和 b 实际上已经死了,但由于它们的引用计数器皆不为 0,在引用计数法的心中,这两个对象 还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。

目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合 引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探 索到的对象便是死亡的,是可以回收的。

那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:

  • Java 方法栈桢中的局部变量;

  • 已加载类的静态变量;

  • JNI handles;

  • 已启动且未停止的 Java 线程。

可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象 a 和 b 相互 引用,只要从 GC Roots 出发无法到达 a 或者 b,那么可达性分析便不会将它们加入存活对象 合集之中。

虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。

比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将 引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。

误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃 圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有 可能会直接导致 Java 虚拟机崩溃。

Stop-the-world 以及安全点

怎么解决这个问题呢? 在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式, 那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了 垃圾回收所谓的暂停时间(GC pause)。

Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机 收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。

当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状 态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性 分析。

垃圾回收的三种方式

当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分 为三种。

第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表 (free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划 分给新建的对象。

清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机 的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。

另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够 放入新建对象的空闲内存。

第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续 的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。

第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并 且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够 解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下

当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。

Java 常见的垃圾收集器有哪些

实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不同厂商 (IBM、Oracle),不同版本的 JVM,提供的选择也不同。一般我们说的JVM指的就是Oracle JDK 的JVM.

Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进 行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也 意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式 下 JVM 的默认选项。

从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。

Serial GC 的对应 JVM 参数是:

-XX:+UseSerialGC

ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应 用场景是配合老年代的 CMS GC 工作,下面是对应参数

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标 是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍 然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问 题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了 并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。

Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作 是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生 代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。 开启选项是:

-XX:+UseParallelGC

另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标, JVM 会自动进行适应性调整,例如下面参数:

-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1)

G1 GC ,这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选 项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情 况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的 一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark- Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更 加明显。

G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中 被标记为废弃(deprecated),所以 G1 GC 值得你深入掌握。

上面介绍了GC的分析,接下来就来看下JVM堆内存是如何进行分配和回收的。

垃圾回收过程的理解

在垃圾收集的过程,对应到 Eden、 Survivor、Tenured 等区域会发生什么变化呢?实际上取决于具体的 GC 方式,先来熟悉一下通常的垃圾收集流程.

第一,Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间占用达到一定阈值时, 触发 minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到 JVM 选择的 Survivor 区 域,而没有被引用的对象(黄色方块)则被回收。注意,存活对象标记了“数字 1”,这是为了表明对象的存活时间。

第二, 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时 候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会 被复制到 to 区域,并且存活的年龄计数会被加 1。

第三, 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓 的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。这个阈值是可 以通过参数指定:

-XX:MaxTenuringThreshold=<N>

后面就是老年代 GC,具体取决于选择的 GC 选项,对应不同的算法。下面是一个简单标记 - 整 理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。

通常我们把老年代 GC 叫作 Major GC,将对整个堆进行的清理叫作 Full GC,但是这个也没有 那么绝对,因为不同的老年代 GC 算法其实表现差异很大,例如 CMS,“concurrent”就体现 在清理工作是与工作线程一起并发运行的。

补充

Q&A

1、什么是本地方法JNI ?

比如我们希望使用汇编语言 (如 X86_64 的 SIMD 指令)来提升关键代码的性能;再比如,我们希望调用 Java 核心类库无 法提供的,某个体系架构或者操作系统特有的功能。 在这种情况下,我们往往会牺牲可移植性,在 Java 代码中调用 C/C++ 代码(下面简述为 C 代 码),并在其中实现所需功能。这种跨语言的调用,便需要借助 Java 虚拟机的 Java Native Interface(JNI)机制。 关于 JNI 的例子,你应该特别熟悉 Java 中标记为native的、没有方法体的方法(下面统称为 native 方法)。当在 Java 代码中调用这些 native 方法时,Java 虚拟机将通过 JNI,调用至对 应的 C 函数(下面将 native 方法对应的 C 实现统称为 C 函数)中。

例如下面的方法,使用native方法定义的方法。

public class Object {
  public native int hashCode();
}

2、为什么第二次GC 的时候,eden 的的对象为什么会被直接复制到s2?

第一次 从 eden 复制到 s1 。

当eden区的内存再次被使用完时,会触发第二次minor gc,将eden区和S1区中存活的对象拷贝到S2区,并将eden区和S1区的内存清空以备使用。

复制的过程 引用计数会增加

当eden区的内存再次被用完时,会触发第三次minor gc,将eden区和S2区存活的对象拷贝到S1区,并清空eden区和S2区的内存,以备使用。

当引用计数达到一定程度时,就会被复制到 老年代。

老年代没有内存的时候也会触发GC

以此往复.

jvm 中默认的 eden与s0,s1 的比例是 8:1:1

3、堆与栈的具体区别是什么?

  • 堆和栈之间的主要区别在于栈内存用于存储局部变量和函数调用,而堆内存用于在Java中存储对象。无论在何处,对象是在代码中创建的,例如作为成员变量,局部变量或类变量, 它们总是在Java中的堆空间内创建。

  • Java中的每个线程都有自己的堆栈,可以使用-Xss JVM参数指定,类似地,也可以使用JVM选项-Xm 和-Xmx 指定Java程序的堆大小,其中-Xms是堆的起始大小和-Xmx 是java堆的最大大小。

  • 如果栈中没有用于存储函数调用或局部变量的内存,JVM将抛出java.lang.StackOverFlowError,而如果没有更多的堆空间用于创建对象,JVM将抛出java.lang.OutOfMemoryError;

  • 如果正在使用递归,在哪个方法调用自己,你可以快速填充栈内存。堆栈和堆之间的另一个区别是栈内存的大小比 Java中的堆内存大小要小得多。

4、GC复制存活的对象,内存地址会变吗?以前的引用怎么办?

这里 stackoverflow上的一个question获取能够解答这个疑问。

gc 复制存活的对象,其原来的地址会发生变化。不过这些jvm就帮我们处理好了,对于程序员来说,这些是无感知的。

参考文献

JVM 的可配置参数列表

JVM GC
JVM 中的内存区域划分
第一,程序计数器(PC,Program Counter Register)。
第二,Java 虚拟机栈(Java Virtual Machine Stack)
第三,堆(Heap)
第四,方法区(Method Area)。
第五,运行时常量池(Run-Time Constant Pool)
第六,本地方法栈(Native Method Stack)
引用计数法与可达性分析
Stop-the-world 以及安全点
垃圾回收的三种方式
Java 常见的垃圾收集器有哪些
垃圾回收过程的理解
补充
Q&A
参考文献
Java Garbage Collection Basics
JVM规范
java vmoptions
After GC , the address of the object in memory be changed and why the object reference still valid?
深入拆解JVM
JVM 垃圾回收
JVM 内存区域
JVM (Java Virtual Machine) Architecture
Java 核心技术
Java Virtual Machine(oracle)
JAVA GARBAGE COLLECTION HANDBOOK
jvm-architechture
JVM内存区域划分
gc-roots
gc sweep
gc compact
gc-copy
jvm-eden
jvm-eden2
jmv-eden3
jvm-tenured
java heap stack defference