Fork me on GitHub

《深入理解JVM虚拟机》垃圾回收部分 读书笔记

最近在看《深入理解JVM虚拟机》,把虚拟机部分单独记录一下,方便后面复习

自动内存管理机制

Java内存区域与内存溢出异常

运行时数据区域

程序计数器

“程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器”
“如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)“

Java虚拟机栈

“虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame[1])用于存储局部变量表、操作数栈、动态链接、方法出口等信息”
“每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程”
异常
“如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常”
“如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常”

本地方法中栈

“虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务”
“与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常”

java堆

“Java堆是被所有线程共享的一块内存区域”
“所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。”

内存回收

“Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等”

内存分配

“线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)”

方法区

“方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来”

运行时常量池

“运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。”

直接内存

HotSpot虚拟机对象探究

对象的创建

如何分配内存

  • 空闲列表(Free List)
    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
  • 指针碰撞(Bump the Pointer)
    对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)

    如何解决线程安全问题

  • 一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
  • 另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

注意:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

对象的内存布局

对象头(Header)

1.png

虚拟机对象头 Mark Word

实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响

对其填充(Padding)

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用.由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

使用句柄

2.png

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

直接指针

Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
3.png

垃圾收集器与内存分配策略

对象已死?

引用计数法(并未采用)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的
缺陷:遇到互相引用的对象时,无法通知GC收集器回收

可达性分析算法

通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的

1.png

作为GC Roots的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

    再谈引用

  • 当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景
  • 强引用就是指在程序代码之中普遍存在的,类似”Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。(如缓存?)
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
    通过此可以监听对象是否被GC?

    生存还是死亡

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。从代码清单3-2中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活
  • 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行
  • 它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序

    回收方法区

    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

    常量

    就是没有任何String对象引用常量池中的”abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个”abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    垃圾回收算法

    标记清除法(Mark-Sweep)

    “首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象”

不足

  • “一个是效率问题,标记和清除两个过程的效率都不高”
  • “一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作”

    复制算法(Copying)

    “它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉”

不足
“这种算法的代价是将内存缩小为了原来的一半,未免太高了一点”

标记整理算法(Mark-Compact)

“标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

“根据对象存活周期的不同将内存划分为几块”(新生代和老年代)

  • “在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集”
  • “老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收”

    HotSpot算法实现

    枚举根节点

    此操作必须要在整个执行系统”暂停”状态下执行,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证

    STW(Stop The World)

    “是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为”Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的”

    引用对象

    “虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用”

    安全点(SafePoint)

  • “特定位置”在OopMap中记录了信息
  • “选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的”
  • “如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来”
    • 抢断式中断(Preemptive Suspension)
      “不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上”
    • 主动式中断(Voluntary Suspension)
      主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起

      安全区域(Sage Region)

      Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint

垃圾收集器

2.png

Serial收集器

  • 单线程的收集器
  • 简单而高效
  • Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择
  • 过程
    3.png

ParNew收集器

  • Serial收集器的多线程版本
  • 运行在Server模式下的虚拟机中首选的新生代收集器
  • ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数
  • 过程
    4.png

Parallel Scavenge收集器

  • 一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器(吞吐量优先”)
  • 参数
    • -XX:MaxGCPauseMillis
      “制最大垃圾收集停顿时间”
    • -XX:GCTimeRation
      设置吞吐量大小
    • -XX:UseAdaptiveSizePolicy
      “GC自适应的调节策略(GC Ergonomics)
      “个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

      SerialOld收集器

  • “使用“标记-整理”算法”
  • “Serial收集器的老年代版本,它同样是一个单线程收集器”
  • “主要意义也是在于给Client模式下的虚拟机使用”
  • 用于Server
    • 一种用途是在JDK,1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
    • 另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
  • 过程
    5.png

Parallel Old收集器

  • “使用多线程和“标记-整理”算法”
  • Parallel Old是Parallel Scavenge收集器的老年代版本
  • 参数
    6.png

CMS收集器(Concurrent MarkSweep)

  • 一种以获取最短回收停顿时间为目标的收集器
  • CMS收集器是基于“标记—清除”算法实现的
    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)

速度比较
初始<重新<并发标记|清除

  • 过程
    7.png

缺点

  • CMS收集器对CPU资源非常敏感
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现”Concurrent Mode Failure”失败而导致另一次Full GC的产生
  • 收集结束时会有大量空间碎片产生

    G1收集器(Garbage First)

    特点

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合

它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)

步骤

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)
  • 过程
    8.png

内存分配与回收策略

分代回收名称

  • 新生代GC(Minor)
    “指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快”
  • 老年代GC(Major/Full)
    “指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。”

    对象优先在Eden分配

    “对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC”

    大对象直接进入老年代

    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)

调节参数
-XX:PretenureSizeThreshold
大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)

长期存活的对象进入老年代

对象在Eden出生并且经过一次MinorGC,如果能被移动到survivor中,则年龄为1,此后每经历过一次MinorGC年龄加一,默认为15则会进入老年代
调节参数
-XX:MaxTenuringThreshold=n
n为多少次MinorGC

动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

空间分配担保

发生MinorGC前,虚拟机会检查老年代可分配的连续区域是否大于整个Eden中所有对象大小,如果满足则MinorGC是安全的,否则则看HandlePromotionFailure是否允许失败,如果允许“那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC”

“JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC”

虚拟机性能监控工具与故障处理工具

JDK命令行工具

jps:虚拟机进程管理工具

“可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)”
jps[options][hostid]
参数

  • -p
    只输出LVMID省略Main类名
  • -m
    输出虚拟机进程启动时传递给main()函数的参数
  • -l
    输出主类的全名,如果进程执行的是jar包,则输出jar的路径
  • -v
    输出启动时的jvm参数

    jstat:虚拟机统计信息监视工具

    监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程[1]虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具

jstat[option vmid[interval[s|ms][count]]]
9.png

jinfo:Java配置信息工具

实时地查看和调整虚拟机各项参数

jinfo[option]pid
jinfo-flag CMSInitiatingOccupancyFraction 1444

jmap:Java内存映像工具

用于生成堆转储快照(一般称为heapdump或dump文件)

jmap[option]vmid
10.png

jhat:虚拟机堆转储快照分析工具

与jmap搭配使用,来分析jmap生成的堆转储快照

jstack: java堆栈跟踪工具

生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
jstack[option]vmid

  • -F
    当正常输出不被响应时,强制输出线程堆栈

  • -l
    出堆栈外,显示关于锁的附加信息

  • -m
    如果调用本地方法的话,显示c/c++的堆栈

HSDIS:JIT生成代码反汇编

JDK的可视化工具

JConsole:Java监视与管理控制台

基于JMX的可视化监视、管理工具

VisualVM:多合一故障处理工具

VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了”All-in-One”的描述字样,预示着它除了运行监视、故障处理外,还提供了很多其他方面的功能

调优案例与奇淫技巧

一分多合理利用资源

建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存。另外建立一个Apache服务作为前端均衡代理访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比硬件升级前有较大提升

本文标题:《深入理解JVM虚拟机》垃圾回收部分 读书笔记

文章作者:wangxc

发布时间:2019年06月22日 - 10:06

最后更新:2019年06月22日 - 10:06

原始链接:http://blog.wangxc.club/2019/06/22/1/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------