浅谈 JVM 内存结构及 GC 机制

前言

JAVA GC(Garbage Collection,垃圾回收)机制是区别C++的一个重要特征,C++需要开发者自己实现垃圾回收的逻辑,而JAVA开发者则只需要专注于业务开发,因为垃圾回收这件繁琐的事情JVM已经为我们代劳了,从这一点上来说,JAVA还是要做的比较完善一些。但这并不意味着我们不用去理解GC机制的原理,因为如果不了解其原理,可能会引发内存泄漏、频繁GC导致应用卡顿,甚至出现OOM等问题,因此我们需要深入理解其原理,才能编写出高性能的应用程序,解决性能瓶颈。

想要理解GC的原理,我们必须先理解JVM内存管理机制,因为这样我们才能知道回收哪些对象、什么时候回收以及怎么回收。

JVM内存管理

根据JVM规范,JVM把内存划分成了如下几个区域:

  1. 方法区(Method Area)
  2. 堆区(Heap)
  3. 虚拟机栈(VM Stack)
  4. 本地方法栈(Native Method Stack)
  5. 程序计数器(Program Counter Register)

其中,方法区和堆所有线程共享。

方法区(Method Area)

方法区存放了要加载的类的信息(如类名、修饰符等)、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。方法区是全局共享的,在一定条件下也会被GC。当方法区超过它允许的大小时,就会抛出OutOfMemory:PermGen Space异常。

在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation),一般来说,方法区上执行GC的情况很少,因此方法区被称为持久代的原因之一,但这并不代表方法区上完全没有GC,其上的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件相当苛刻而且困难。

运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常量和引用。一般来说,常量的分配在编译时就能确定,但也不全是,也可以存储在运行时期产生的常量。比如String类的intern()方法,作用是String类维护了一个常量池,如果调用的字符”hello”已经在常量池中,则直接返回常量池中的地址,否则新建一个常量加入池中,并返回地址。

堆区(Heap)

堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由所有线程共享,在虚拟机启动时创建。堆区主要用于存放对象实例及数组,所有new出来的对象都存储在该区域。

虚拟机栈(VM Stack)

虚拟机栈占用的是操作系统内存,每个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。

局部变量表中存储着方法相关的局部变量,包括各种基本数据类型及对象的引用地址等,因此他有个特点:内存空间可以在编译期间就确定,运行时不再改变。

虚拟机栈定义了两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足时,抛出OutOfMemoryError。

本地方法栈(Native Method Stack)

本地方法栈用于支持native方法的执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。

程序计数器(Program Counter Register)

程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念模型,各种JVM所采用的方式不一样。字节码解释器工作时,就是通过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。

每个程序计数器只能记录一个线程的行号,因此它是线程私有的。

如果程序当前正在执行的是一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是native方法,则计数器的值为空,此内存区是唯一不会抛出OutOfMemoryError的区域。

GC机制

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。

在上面介绍的五个内存区域中,有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。

查找算法

  • 引用计数法
  • 可达性分析

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反之每当一个引用失效时,计数器减一。当计数器为0时,则表示对象不被引用,即认为该对象可以被回收了。但是这个算法有个明显的缺陷:当两个对象相互引用,但是二者都已经没有作用时,理应把它们都回收,但是由于它们相互引用,不符合垃圾回收的条件,所以就导致无法处理掉这一块内存区域。

可达性分析

设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找不到根时,就认为该对象不可达。
java中,可以作为GC Roots的对象包括:

  • java虚拟机栈中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

补充概念,在JDK1.2之后引入了四个概念:强引用、软引用、弱引用、虚引用。

  • 强引用:new出来的对象都是强引用,GC无论如何都不会回收,即使抛出OOM异常。
  • 软引用:只有当JVM内存不足时才会被回收。
  • 弱引用:只要GC,就会立马回收,不管内存是否充足。
  • 虚引用:可以忽略不计,JVM完全不会在乎虚引用,你可以理解为它是来凑数的,凑够”四大天王”。它唯一的作用就是做一些跟踪记录,辅助finalize函数的使用。

最后总结,什么样的类需要被回收:

a.该类的所有实例都已经被回收;
b.加载该类的ClassLoad已经被回收;
c.该类对应的反射类java.lang.Class对象没有被任何地方引用。

内存分区

内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会触发Minor GC(也称作Youn GC)。

旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。

持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。

GC算法

常见的GC算法:复制、标记-清除和标记-压缩

复制:
复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域。

当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。

标记-清除:

该算法采用的方式是从跟集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。在Marking阶段,需要进行全盘扫描,这个过程是比较耗时的。清除阶段清理的是没有被引用的对象,存活的对象被保留。

标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。

标记-压缩:
该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针。

由于进行了移动规整动作,该算法避免了标记-清除的碎片问题,但由于需要进行移动,因此成本也增加了。(该算法适用于旧生代)

垃圾收集器

在JVM中,GC是由垃圾回收器来执行,所以,在实际应用场景中,我们需要选择合适的垃圾收集器,下面我们介绍一下垃圾收集器。

串行收集器(Serial GC)

Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。比较适合于只有一个处理器的系统。在串行处理器中minor和major GC过程都是用一个线程进行回收的。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,而且事实上,它并没有让我们失望,几十毫秒的停顿,对于我们客户机是完全可以接受的,该收集器适用于单CPU、新生代空间较小且对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。

ParNew GC

基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以被用于服务端上(server),同时它可以与CMS GC配合,所以,更加有理由将他用于server端。

Parallel Scavenge GC

在整个扫描和复制过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的应用,是server级别的默认GC方式。

CMS (Concurrent Mark Sweep)收集器

该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的应用就适合这种收集器,因为其高并发、高响应的特点,CMS是基于标记-清楚算法实现的。

CMS收集器的优点:并发收集、低停顿,但远没有达到完美;

CMS收集器的缺点:

a. CMS收集器对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。
b. CMS收集器无法处理浮动垃圾,可能出现“Concurrnet Mode Failure”,失败而导致另一次的Full GC。
c. CMS收集器是基于标记-清除算法的实现,因此也会产生碎片。

G1收集器

相比CMS收集器有不少改进,首先,基于标记-压缩算法,不会产生内存碎片,其次可以比较精确的控制停顿。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

RTSJ垃圾收集器

RTSJ垃圾收集器,用于Java实时编程。

总结

深入理解JVM的内存模型和GC机制有助于帮助我们编写高性能代码和提供代码优化的思路与方向。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,946评论 2 31
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,519评论 3 83
  • 内存溢出和内存泄漏的区别 内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,...
    Aimerwhy阅读 727评论 0 1
  • 一、前言 JAVA GC(Garbage Collection,垃圾回收)机制是区别C++的一个重要特征,C...
    EnjoyAndroid阅读 1,404评论 0 3
  • Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进来,墙里面的人想出来。 对象...
    胡二囧阅读 1,070评论 0 4