JVM《深入理解Java虚拟机》重点

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

运行时数据区域,如图所示。

image-20201127122533943

1.运行时数据区

1.程序计数器

2.虚拟机栈

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 如果Java虚拟机栈容量可以动态扩展(Hotspot不支持),当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。Hotspot不支持,但如果申请时就失败,仍会出现OOM异常

3.本地方法栈

有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemoryError异常。

4.堆

如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常

5.方法区

逻辑概念。

用于存储已被虚拟机加载的:

  • 运行时常量池
  • 类型信息
  • 静态变量常量
  • 即时编译器编译后的代码缓存
  • 其他数据。

方法区实现:

  1. JDK6,是用永久代实现。
  2. JDK7,把永久代的符号引用移至native heap运行时常量池字符串常量池static静态变量移到Java(Class对象也在Java堆)。Intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上
  3. JDK8,把永久代中剩余的内容(主要是类型信息,即类的元数据信息)全部移到元空间,完全废弃永久代。元空间大小设置参数:-XX:MetaspaceSize。元空间存储初始加载的类的描述信息, 意味着它只存储原始类的信息。
  4. Class对象是根据类型信息产生的。方法区是一个逻辑区域。JDK6及之前,方法区确实是由永久代实现。JDK7及以后,就纯粹是一个逻辑上的表述了。因为1.7就字符串常量池等就移到堆上了。

6.运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。

主要用来存储编译期的Class文件中的常量池表、运行期间的直接引用、运行期间的新常量

字符串常量池是再运行时常量池上?

7.直接内存

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

在JDK 1.4中新加入了NIO(New Input/Output)类,它可以使用Native函数库直接分配堆外内存

2.HotSpot虚拟机对象

1.对象的创建

JVM遇到一条字节码new指令时,做以下动作:

  1. 先检查是否有该类,再检查该类是否初始化:
  2. 为新生对象分配内存
  3. 设置对象头信息
  4. 构造函数初始化对象

2.对象的内存布局

可以划分为三个部分:

  • 对象头(Header):1.对象自身的运行时数据。2.类型指针
  • *实例数据(Instance Data)
  • 对齐填充(Padding)

3.对象的访问定位

主流的访问方式有两种:

  • 使用句柄:Java堆中划分出一块内存来作为句柄池
  • 直接指针:reference中存储的直接就是对象地址。

Hotspot主要使用直接指针方式访问

3.模拟OOM和栈溢出异常

溢出总结

  • 堆溢出:OOM:List无限新增对象。
  • 栈溢出:OOM:无线申请线程。StackOverflowError:递归无限申请新方法。
  • 方法区(包含运行时常量池)溢出:大量加载类信息(运行时创建新类)。或大量String.intern()。
  • 本机直接内存溢出:直接用Unsafe::allocateMemory()来大量申请直接内存。

一般由直接内存导致的OOM,Heap Dump文件中看不到什么明显的异常。如果Dump文件很小,程序中又直接或间接用到了DirectMemory(典型的间接使用就是NIO),那么就要考虑下是不是直接内存溢出。

堆OOM排查:

  • 内存泄漏:垃圾收集器无法回收

  • 内存溢出:内存不够用。如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

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

1.如何回收对象

垃圾回收需要完成的三件事情

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

确定哪些对象需要回收主要是两种算法:

  • 引用计数算法:循环引用问题。主流的Java虚拟机都没有用它来实现。
  • 可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索引用链。如方法堆栈中使用到的 参数、局部变量、临时变量等,Java类的引用类型静态变量,字符串常量池(String Table)里的引用等。

各种引用:

  • 强引用:最传统的引用定义。
  • 软引用发生内存溢出异常前,才会考虑回收它们。SoftReference类实现软引用。
  • 弱引用:只能生存到下一次GC发生。WeakReference类来实现软引用。
  • 虚引用:虚引用的唯一目的只是为了能在对象被GC时收到一个系统通知。虚引用不会对对象的生存时间有任何影响,也无法通过虚引用来取得一个对象实例。PhantomReference类来实现。

怎么干死对象?

可达性分析算法判定不可达,此时对象还没死。要干掉一个对象,至少要经历两次标记过程:

  1. 第一次标记:在进行可达性分析后发现没有与GC Roots相连接的引用链。
  2. 第二次标记:无需执行finalize()方法,即对象没有覆盖finalize()方法,或者finalize()方法已经被JVM调用过。

2.垃圾回收算法

1.分代收集理论

这个理论实际上是经验法则,建立在两个分代假说上:

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

2.标记-清除算法

一般用于老年代。

3.标记-复制算法

一般用于新生代。

改进后的算法:Eden + Survivor + 老年代担保:新生代分为一块较大的Eden空间和两块较小的Survivor空间(Hotspot默认8:1:1)。另外为了防止回收时Survivor装不下所有的对象,老年代提供了担保,多余装不下的对象直接进入老年代。

4.标记-整理算法

算法

  • 标记:标记过程仍然与“标记-清除”算法一样。
  • 整理:清除时不是直接清除,而是让所有存活对象向内存空间一侧移动,然后清除边界外的内存。

标记整理算法,移动对象时,用户应用程序必须暂停,但是加快了整体的访问速度(因为没有复杂的内存访问方式)。

标记清除算法,虽然降低了延迟(因为不用Stop the world),但是也降低了吞吐量(因为内存访问更耗时了,总体上是更耗时的)。

基于以上考虑,HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。

还有一种“和稀泥式”解决方案:平时多数时间都采用标记-清除算法,内存碎片太多,就用用标记-整理算法收集一次。基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。

另外标记整理算法不一定要“Stop the world”:最新的ZGC和Shenandoah收集器使用读屏障(Read Barrier)技术实现了整理过程与用户线程的并发执行,稍后将会介绍这种收集器的工作原理。然后“标记-清除”算法其实也是需要停顿用户线程来进行标记、清除,只不过这个时间开销很小。

3 Hotspot算法细节

1.根节点枚举

所有的GC收集器,在“枚举根节点”这一步都需要暂停用户线程(在一个快照中)

OopMap的概念:

在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。

2.安全点

那么我们只有在特定的位置生成OopMap了,这些位置就是安全点(Safepoint)。那么也就是说,只能在安全点才能进行GC,因为只有安全点有OopMap信息,枚举需要根据OopMap来判断哪些变量是引用类型,从而快速找到对象。

3.安全区域

安全区域。安全区域是指在这一段代码片段之中,引用关系不会发生变化。那么这个区域里任何地方都可以GC。

4.记忆集与卡表

为了解决跨代引用,GC收集器会在新生代建立记忆集(Remembered Set)。

卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

5.写屏障

卡表实现了记忆集,缩小了GC Roots扫描范围。那么卡表元素如何维护呢?例如它们何时变脏、谁来把它们变脏等?

HotSpot通过写屏障来实现编译执行时卡表的维护。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP环形切面。

6.并发的可达性分析

  • 把原本存活的对象错误标记为已消亡:这个无法接受。

以下是示意:

image-20201202211837341

要解决对象消失问题,只需破坏上面两个条件中的一个就行,包括两种解决方案,选一个就行:

  • 增量更新:破坏第一个条件,黑色指向白色后,它自己要变回灰色
  • 原始快照:破坏第二个条件,只按原始快照扫描,后面删除的线都不认

4.经典垃圾收集器

1.综述

image-20201202214148275

  • 串行:新生代Serial收集器(标记复制) + 老年代Serial Old收集器(标记整理)。
  • 并行:新生代ParNew收集器(标记复制)。Serial收集器的多线程版本。除使用多线程GC之外,其余行为和控制参数与Serial完全一致。
  • 吞吐量优先:新生代Parallel Scavenge收集器(标记复制)。“标记-复制”算法。没有复杂的内存分配制度。老年代Parallel Old收集器(标记整理)
  • CMS(Concurrent Mark Sweep):老年代收集器(标记-清除),首次实现了让垃圾收集线程与用户线程(基本上)同时工作

CMS等收集器关注的是GC时减少用户线程停顿时间,更适用于交互多的程序。而Parallel Scavenge关注的则是吞吐量可控,因此可以最高效率地利用处理器资源,适合在后台运算而不需要太多交互的分析任务

2.CMS

跟其他收集器比起来,CMS复杂一些。整个过程分为4个步骤

  1. 初始标记(CMS initial mark): 仅仅标记一下GC Roots能直接关联到的对象,速度很快。用户停顿
  2. 并发标记(CMS concurrent mark) :耗时较长,但无需停顿,可与用户线程并发运行。
  3. 重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记变动的那部分对象。(黑灰白理论)用户停顿
  4. 并发清除(CMS concurrent sweep) :清理删除掉标记阶段判断的已经死亡的对象,标记清除,无需移动存活对象。

需要停顿的初始标记和重新标记的停顿时间都很短,而耗时最长的并发标记和并发清除阶段都可以与用户线程一起工作。所以CMS的优点是并发收集,低停顿。

示意图:

image-20201204201303980

3.Garbage First收集器

里程碑式的成果,开创了面向局部收集的设计思路和基于Region的内存布局形式。JDK 8 Update 40后全面成熟。

JDK 9发布后,,成为服务端模式下的默认垃圾收集器。

G1的设计目标是“停顿时间模型”(Pause Prediction Model),即支持指定在M毫秒的时间片内,GC上花费的时间大概率不超过N毫秒。

具体设计思路……

未完待续:…


Shenandoah收集器

ZGC收集器

各种性能监控工具

类加载过程

双亲委派模型

内存模型

线程安全/锁优化/偏向锁

image-20201228210247548

2、3、4、7、12、13章。


Spring:

IOC原理

Bean生命周期

循环依赖

AOP原理


SpringBoot:

自动配置原理


分布式:

分布式锁

分布式事务


设计模式

海量数据算法


转载请注明来源