深入理解Java虚拟机(一):自动内存管理机制

Java虚拟机

Oracle有两款实现了 Java SE 的产品:Java SE Development Kit(JDK)Java SE Runtime Environment(JRE)
JDKJRE 的超集,包含了 JRE 的所有内容,以及开发应用程序所需的编译器和调试器等工具。 JRE 提供了函数库、Java Virtual Machine(JVM) 和其它用来运行Java应用程序的组件。

Java SE8产品组件概念图

Java内存区域

Java虚拟机运行时数据区

程序计数器

程序计数器是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器。由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间来实现的,因此每个线程都有一个独立的程序计数器,用于线程切换后能恢复到正确的执行位置。如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native方法,这个计数器的值为 undefined。此区域是唯一一个在Java虚拟机规范中没有规定 OutOfMemoryError 情况的区域。

Java虚拟机栈

  • 虚拟机栈描述的是Java方法执行的动态内存模型,调用方法即创建栈帧并入栈,方法执行完毕栈帧出栈。
  • 栈帧:每个方法执行,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等。每一个方法从调用到执行完成的过程,就是一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 局部变量表:存放编译器可知的各种基本数据类型,引用类型,returnAddress类型。对象是在堆内存中创建的,局部变量表存放的是对象的引用,其大小是不会改变的。因此局部变量表的内存空间在编译期完成分配后,方法需要在帧分配多少内存是固定的。
  • 虚拟机栈异常:如果线程请求的栈深度超过虚拟机允许的深度,将抛出 StackOverFlowError 异常;如果虚拟机栈允许扩展,在扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到的 Native方法 服务。在 Hotspot虚拟机 的实现中是把本地方法栈和虚拟机栈合二为一的。与虚拟机栈一样,本地方法栈也会抛出 StackOverFlowError 异常和 OutOfMemoryError 异常。

Java堆

堆是线程共享的数据运行时区域,几乎所有的对象实例以及数组都要在堆上分配内存。堆是垃圾收集器管理的主要区域。Java堆可以处于物理上不连续的内存空间中。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区

方法区存储虚拟机加载的类信息(类的版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码等数据。于 Hotspot虚拟机 来说,将方法区纳入GC管理范围,这样就不必单独管理方法区的内存,所以就有了相对于新生代和老年代的永久代一说。

运行时常量池

运行时常量池(JDK6在方法区,JDK7在Java堆)用来存放编译器生成的各种字面量以及符号引用(类加载之后进入运行时常量池)。运行期间也能将新的常量放入池中。当常量池无法再申请到内存时,将会抛出 OutOfMemoryError 异常。

直接内存

Java NIO 使用 Native函数库 直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。通过避免在 Java堆Native堆 中来回复制数据来提高性能。直接内存大小不受虚拟机参数控制,如果各个内存区域总和大于物理内存限制,就会出现 OutOfMemoryError 异常。

对象

对象的创建

对象的创建过程

内存分配策略

根据 Java 堆 是否规整可以判断使用哪种内存分配策略。

  • 指针碰撞:堆内存中的空闲空间十分的规整,使用与未使用的空间全部为连续,分配内存只需移动指针。
  • 空闲列表:针对堆内存中的空间零散的存在,虚拟机维护着一个列表,记录那些内存未使用。

线程安全性

对象创建在虚拟机中是十分频繁的行为,在并发环境下需要考虑线程安全。

  • CAS失败重试:通过乐观锁实现线程安全。
  • TLAB(Thread Local Allocation Buffer):本地线程分配缓冲,内存为每个线程分配一个 TLAB区域,每个线程要创建对象时先在这个区域中创建,当原来的空间不足时再通过线程同步获取一块新的区域。

对象设置

将对象的哈希吗、GC年龄信息等存放在对象头中,执行 <init> 方法。

对象的结构

对象头(Header)

  • 自身运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
  • 类型指针:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,对象头中还必须有一块记录数组长度的数据。

实例数据(Instance Data)

实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

对齐填充(Padding)

HotSpot虚拟机 要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

对象的访问定位

Java程序通过栈上的 reference 数据来操作堆上的具体对象,由于 reference 类型在Java虚拟机规范中只规定了一个指向对象的引用,具体用何种方式去定位、引用堆中对象的具体位置,取决于虚拟机的实现,目前主要有 使用句柄直接指针 两种方式。

  • 使用句柄:引用类型指向堆中一块区域(句柄池),此区域保存了实例对象的地址(对象被移动时维护句柄中的指针数据,无需改变 reference 本身)。
  • 直接指针:从引用类型直接指向内存区域(速度更快,节省了一次指针定位带来的开销)。

垃圾回收

在Java堆上分配一个内存给实例对象时,此时在虚拟机栈上引用型变量就会存放这个实例对象的起始地址。当线程销毁后,其在虚拟机栈上的内存自然会被回收,也就是说虚拟机栈上的这块内存不在虚拟机GC范围内。

垃圾对象判定算法

  • 引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象时,引用计数器的值+1,当引用失效时,计数器的值-1。垃圾回收器遇到计数器为0的对象时就会回收。但是当堆内存中的对象相互引用,而外部不存在对这些对象的引用时,计数器值不为0,无法判定回收。
  • 可达性分析法:通过一系列名为 GC Roots 的对象(虚拟机栈、方法区类属性所引用的对象、方法区中常量所引用的对象、本地方法栈中引用的对象)作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则此对象证明是不可用的,将被判定为可回收对象。

Java引用

  • 强引用:类似 Obejct obj = new Object(),只要强引用还在,垃圾收集器就永远不会收集被引用的对象。
  • 软引用:还有用但并非必须的对象。在系统发生内存溢出之前,会将软引用关联的对象进行回收,如果回收后还没有足够的内存,才会抛出内存异常。
  • 弱引用:垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生产时间构成影响。也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被垃圾收集时收到一个系统通知。

finalize()

将对象回收至少要经历两次标记过程,如果在可达性分析中发现对象没有与 GC Roots 的引用链,那它将会被第一次标记并被进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法(当前对象没有覆盖此方法或者已经执行过此方法,则虚拟机认为“没有必要执行”),虚拟机不会承诺等待此方法执行结束。如果在 finalize() 方法中成功与引用链上的人一个对象建立关联,则对象不会被回收。

如何回收

回收策略

  • 标记-清除算法:标记可达对象,在清除阶段回收并没有被标记为可达的对象所占用的内存空间,并将原来的可达标记删除。但是效率不高且会造成内存碎片化。
  • 复制算法:标记待回收内存和不需回收的内存,将不需回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域是连续的。其缺点是需要损失部分系统内存用于复制,但是可以避免产生内存碎片。实例创建时通常发生在 Eden空间,发生 Minor GC 后,会将Eden和其中一个 Survivor空间 不需回收的对象内存复制到另一个 Survivor空间 中(HotSpot 虚拟机中 Eden空间Survivor空间 的比例为8:1),如果反复几次复制有对象一直存活,则会将相应的对象内存移至老年代。
Java垃圾回收管理
  • 标记-整理算法:是老年代中的垃圾回收算法,标记过后,将不用回收的对象内存压缩到空间的一端,再对另一端的内存空间进行垃圾回收。这样既避免了复制算法带来的效率问题,也避免了内存碎片化的问题。

垃圾收集器

垃圾收集器是垃圾回收算法的具体实现。

Serial垃圾收集器

Serial垃圾收集器 是最基本、发展历史最悠久的收集器。

  • 采用复制算法,针对新生代
  • 单线程垃圾回收,执行时必须暂停所有工作线程,直到完成
Serial - Serial Old组合收集器

ParNew垃圾收集器

ParNew垃圾收集器 是Serial收集器的多线程版本。

ParNew -Serial Old组合收集器

Parallel Scavenge垃圾收集器

Parallel Scavenge收集器 的目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU消耗的总时间的比值),即减少垃圾收集时间,让用户代码获得更长的运行时间。

  • 采用复制算法,针对新生代
  • 多线程垃圾回收
    Parallel Scavenge收集器 提供两个参数用于精确控制吞吐量:
  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间(ms),设置稍小会缩短停顿时间,但也可能会造成频繁发生垃圾回收,使得吞吐量下降。
  • -XX:GCTimeRatio:垃圾收集时间占总时间的比率,0 < n < 100的整数

CMS(Concurrent Mark Sweep)垃圾收集器

CMS收集器是一款真正意义上的并发收集器,实现了让垃圾收集线程与用户线程(基本上)同时工作。适用于与用户交互较多的场景。

  • 针对老年代
  • 基于标记-清除算法
  • 以获取最短回收停顿时间为目标

G1垃圾收集器

  • 充分利用多CPU、多核环境下的硬件优势,可以使垃圾收集和用户程序同时进行。
  • 分代收集,能够独立管理整个GC堆,采用不同方式处理不同时期的对象。整个堆被划分为多个大小相等的 不连续独立区域(Region),新生代和老年代不再是物理隔离,它们都是一部分Region的集合。
  • 从整体看基于标记-整理算法,从局部看(两个Region间)是基于复制算法,不会产生内存碎片,有利于长时间运行。
  • 可预测的停顿,可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
  • 适用于服务端,针对大内存,多处理器的机器。

内存分配

HotSpot一般的年代内存划分

对象的内存分配主要在堆上分配(JIT编译优化后可能在栈上分配),在新生代的Eden空间中分配,如果启用了本地线程分配缓冲,则线程优先在TLAB上分配。少数情况下对象会直接分配在老年代中。

内存分配策略

  • 优先分配到Eden空间:大多数情况下,对象在新生代 Eden区域 中分配。当 Eden区域 没有足够空间时将会发起一次 Minor GC
  • 大对象直接分配到老年代:所谓大对象是指需要大量连续空间的Java对象,直接在老年代分配空间就避免了大对象在新生代各个区域间反复复制。
  • 长期存活的对象分配到老年代:虚拟机给每个对象定义了一个对象年龄计数器,如果对象在 Eden区域 出生且经过一次 Minor GC 后仍然存活,并且能被 Survivor空间 容纳,对象年龄就会被设为1,此后对象每熬过一次 Minor GC,其年龄就会增加1,当年龄增加到一定程度(默认为15岁),就会晋升到老年代中。
  • 动态对象年龄判断:Survivor空间 中相同年龄的所有对象大小的总和大于 Survivor空间 的一半,则年龄大于等于该年龄的对象可以直接进入老年代。
  • 空间分配担保:有可能在 Minor GC 执行后仍有大量对象在新生代存活,就需要老年代进行分配担保,将 Survivor空间 无法容纳的对象晋升到老年代,老年代要进行这样的担保,前提是老年代中有容纳这些对象的足够空间。因此在进行 Minor GC 前,虚拟机会检查老年代最大连续可用内存是否大于新生代所有对象总空间,如果条件成立,则 Minor GC 可以确保是安全的,如果不成立虚拟机则查看 HandlePromotionFailure 参数判断是否允许担保失败。如果允许,则会继续检查老年代的最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试一次 Minor GC,如果小于或 HandlePromotionFailure 参数不允许则改为进行一次 Full GC,发生担保失败也会重新进行一次 Full GC。空间分配担保可以避免 Full GC 过于频繁。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容