JVM知识点打卡学习(2021年3月27日打卡)

2021年3月18日打卡内容

今天学习JVM主要是看看书,感觉看书记点东西还是不错的。有需要《深入理解JVM第三版》的小伙伴也可以私我,我给你们发PDF。这本书的话,PDF上看有700多页,也不知道啥时候能看完,尽快吧。

第一部分:走进JAVA(略过)

我好残忍,有点吃快餐直接学习知识的味道了。

第二部分:(洗了个澡就开始看哪个网站写笔记比较好,看了下舍友用有道云整理感觉还挺好看的,一溜烟时间就过去了。)


2021年3月21日打卡内容

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

2.2 运行时数据区域

  JAVA运行时数据区域包括以下五个区域:

  方法区Method Area

  虚拟机栈 VM Stack

  本地方法栈 Native Method Stack

  堆Heap

  程序计数器 Program Counter Register

2.2.1 程序计数器

  一块较小的内存空间,是当前线程所执行的字节码的行号指示器。可以通过改变这个计数器的值来选取下一条需要执行的字节码指令,是程序控制流的指示器。如分支、跳转、循环、异常处理、线程恢复。

  JAVA虚拟机多线程是通线程轮流切换,分配处理器执行时间的方式来实现的。

  为了线程切换后能恢复到正确的执行位置,每条线程都需要有个独立的程序计数器,各条线程之间程序计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

2.2.2 JAVA虚拟机栈

  JAVA虚拟机栈也是线程私有的,生命周期和线程相同。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  栈更多情况下指局部变量表,里面存放各种JAVA虚拟机基本数据类型,对象引用和returnAddress类型。数据类型在局部变量表中的存储空间用局部变量槽(Slot)来表示。long和double占用两个Slot而其他的数据类型只占用一个。当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小(槽的数量)。

2.2.3 本地方法栈

  与虚拟机栈作用类似。

  区别:

  虚拟机栈为虚拟机执行JAVA方法(字节码)服务。

  而本地方法栈为虚拟机使用到的本地方法服务。

2.2.4 JAVA堆

  堆是虚拟机所管理的内存中最大的一块。

  堆被所有线程共享,在虚拟机启动时创建。

  唯一目的就是存放对象实例。

  堆是垃圾收集器管理的内存区域,因此也被称作为GC堆。(Garbage Collected Heap)

  JAVA堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。

2.2.5 方法区

  和堆一样,被各个线程共享。

  用来存储已被虚拟机加载的类型信息,常量,静态变量、即时编译器变异后的代码缓存等区域。

2.2.6 运行时常量池

  运行时常量池(Runtime Constant Pool)是方法区的一部分,受到方法区内存的限制。具备动态性,运行期间也可以将新的常量放入池中。

2.2.7 直接内存

这部分没怎么看懂。

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建

  当JAVA虚拟机遇到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类的加载过程(第7章)。

  类加载检查通过之后,为新生对象分配内存。如果JAVA堆内存绝对规整,那么分配方式使用指针碰撞(Bump The Pointer)。一半使用过,一半没使用过,中间是指针分隔,分配内存时指针向空闲区移动一块对象大小相等的距离即可。如果JAVA堆内存不规整,虚拟机就会维护一个列表记录哪些可用和不可用,分配时找一块足够大的空间给对象实例,并且更新记录,这种分配方式称为“空闲列表”(Free List)。系统是否规整,视垃圾收集器是否有空间压缩整理(Compat)的能力决定。有,则使用指针碰撞,简单高效;使用基于清除算法的收集器,则只能使用空闲列表。

2021年3月23日打卡

  分配内存需要考虑到并发的问题。解决方法:一种是对分配内存的动作进行同步处理——使用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行——每个线程在JAVA堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allcation Buffer,TLAB),哪个线程需要分配内存,就在哪个线程的本地缓冲区分配。本地缓冲区用完了分配新的缓存区时才需要同步锁定。虚拟机可以决定是否使用TLAB。

  内存分配完成后,需要将分配到的空间(不包括对象头)初始化为零值。保证对象的实例字段不赋初值就可以直接使用。

  之后就对对象进行必要的设置,将这些信息存放在对象的对象头(Object Header)中。

2.3.2 对象的内存布局

  HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
  HotSpot第一部分对象头包含两类信息。第一类是存储对象自身的运行时数据,这部分数据被称为Mark Word。另一类是类型指针,即对象指向它的类型元数据的指针,JAVA虚拟机通过这个指针来确定该对象是哪个类的实例。对象数据上不一定有类型指针存在。如果对象是一个JAVA数组,对象头中还有一块用于记录数组长度的数据
  第二部分实例数据部分便是对象真正存储的有效信息
  第三部分是对齐填充,并不是必须的。对象头被设计为正好是8字节的整数倍。如果没有对齐,则填充补全。

2.3.3 对象的访问定位

  JAVA程序会通过栈上的reference数据来操作堆上的具体对象。主流访问方式有使用句柄直接指针两种。
  - 使用句柄访问的话,JAVA堆中将划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址。句柄中包含了对象实例数据和类型数据各自具体的地址信息。
图片说明
  - 使用指针访问的好处是速度更快,节省定位的时间开销,HotSpot主要使用第二种方式进行对象访问。
图片说明

2.4 实战OutOfMemoryError异常

  除程序计数器外,其他运行时区域都有发生OOM异常的可能。
  实战目的:
  1.验证各个运行时区域存储的内容。
  2. 实际工作遇到内存溢出异常时,能根据异常提示信息得知哪块溢出,怎么发生,怎么处理。

2.4.1 JAVA堆溢出

  JAVA堆用于存储对象实例,只需要不断创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收清除这些对象,就可以造成内存溢出。
  按照书本如下写了一个简单的堆内存溢出异常测试。

通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析.

图片说明

  常规处理方法:通过内存映像分析工具堆Dump出来的堆存储快照进行分析。
    - 第一步确认导致OOM的对象是否是必要的,即分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)个人理解:导致OOM的对象并不必要,但是垃圾回收器无法对它们进行回收,就导致了内存泄漏。而内存溢出则是导致OOM的对象是必须使用的,但内存空间不足,无法进行空间分配,所以导致了内存溢出。
    - 第二步,如果是内存泄漏,通过工具查看泄漏对象到GC Roots的引用链,看怎样的引用路径,与哪些GC Roots相关联,才导致垃圾回收期无法回收,定位对象创建的位置,找出内存泄漏的代码的具*置。
如果是
内存溢出**,就应该检查Java虚拟机堆参数(-Xmx和-Xms)设置,与机器内存进行对比,看是否还有向上调整的空间。再从代码上检查是否有某些对象生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽可能的减少程序运行期的内存消耗。

2.4.2 虚拟机栈和本地方法栈溢出

  HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,栈容量只能由-Xss参数来设定。
  《Java虚拟机规范》中描述了两种异常:
    - 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
    - 如果栈内存允许动态扩展,扩展栈容量无法申请到足够内存时,抛出OutOfMemoryError异常。

  HotSpot虚拟机不支持扩展。所以除非线程申请内存时就无法获得足够内存,否则不会因为扩展内存不足导致OutOfMemoryError。
  如果测试时不仅限于单线程,通过不断建立线程的方式也可以在HotSpot上产生内存溢出异常。在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。总容量一定,每个线程栈空间越大,同时并存的线程数量就越少。在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

2021年3月24日打卡

2.4.3 方法区和运行时常量池溢出

  运行时常量池是方法区的一部分。JDK8中完全使用元空间来替代永久代。
  String.intern()是一个本地方法,字符串常量池中包含一个equals此字符串值得字符串,就返回此String对象的引用,否则将此字符串添加到常量池中,再返回。根据书上的测试实例。
图片说明
  str1用的是同一个字符串对象,所以返回true,而str2.intern()用的是已经存在的"java"字符串,str2则是new出来的"java"字符串,所以不相等,返回false.

  方法区的主要职责就是用来存放类型的相关信息。如类名,访问修饰符,常量池,字段描述,方法描述等。

2.4.4 本机直接内存溢出

  由直接内存导致的内存溢出,明显的特征是HeapDump文件中不会看见明显的异常情况,如果产生的Dump文件很小,程序中又直接或间接使用了DirectMemory(例如间接使用NIO),就可以重点检查一下直接内存方面的原因。

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

  垃圾收集(Garbage Collection,简称GC)。当需要排查各种内存溢出,内存泄漏问题时,当垃圾收集称为系统达到更高并发量的瓶颈时,就必须对这些“自动化”技术实施必要的监控和调节。
  第二章中可以知道,程序计数器,虚拟机栈,本地方法栈3个区域随线程生灭,栈中的栈帧随着方法的进入和退出有条不紊的执行出栈和入栈操作。这几个区域的内存分配和回收都具备确定性,当方法结束或者线程结束时,内存自然就跟随回收。
  而JAVA堆和方法区这两个区域有很显著的不确定性。只有处于运行期间才能知道程序究竟会创建哪些对象创建多少对象,这部分内存的分配和回收是动态的。GC关注的正是这部分内存该如何管理。

3.2对象已死?

  GC在对堆进行回收之前,会先确定这些对象之中哪些还存活着,哪些已经“死去”。

3.2.1 引用计数算法

  在对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加一,引用失效时,计数器就减一。计数器为零时,就不能再被使用。
  客观来说,引用计数算法(Reference Counting)占用额外空间进行技术,但原理简单,判定效率较高。但JAVA中并没有选择该算法管理内存,主要原因是需要配合大量额外出来再能保证正确工作。
  如对象之间相互循环引用的问题。互相引用时,计数器不为零,但不可能再被访问。引用计数算法也就无法对他们进行回收。

3.2.2 可达性分析算法

  JAVA的内存管理子系统就是通过可达性分析(Reachability Analysis)算法来判定对象是否存活。
  基本思路是通过一些列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话说就是从GC Roots到这个对象不可达时,证明这个对象不可再被使用。
  如图所示。
图片说明
  JAVA技术体系中,固定座位GC Roots的对象包括以下几种:
  - 虚拟机栈(栈帧中的本地变量表)中引用的对象。如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  - 方法区类静态属性引用的对象,如JAVA类的引用类型静态变量。
  - 在方法区中常量引用的对象,如字符串常量池(String Table) 里的引用。
  - 在本地方法栈中JNI(Native方法)引用的对象
  - JAVA虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException、OutOfMemoryError)等,还有系统类加载器。
  - 所有被同步锁(synchronized关键字)持有的对象
  - 反映JAVA虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
  除这些固定的GC Roots集合以外,根据垃圾收集器和当前回收区域的不同还可以有其他对象“临时性”的加入,共同构成完整GC Roots集合。如分代收集和局部回收(Partial GC)。只针对JAVA堆中某一块区域发起垃圾收集时,某个区域里的对象完全有可能被位于堆中的其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。

3.2.3 再谈引用

  判断对象是否存活和“引用”离不开关系。JDK 1.2版之后,JAVA对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,引用强度依次逐渐减弱。

  - 强引用,指引用赋值。类似于“Object obj=new Object()”。无论任何情况,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。

  - 软引用,用来描述一些还有用,但非必须的对象。只被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  - 弱引用,被弱引用关联的对象只能生存到下一次垃圾手机发生为止,垃圾收集器工作时无论当前内存是否足够,都会回收被弱引用关联的对象

  - 虚引用也被称为“幽灵引用”或者“幻影引用”。一个对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

3.2.4 生存还是死亡?

  即便被判定为不可达对象,也还需要经历两次标记过程才能宣告一个对象死亡:如果对象在可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或该方法已经被虚拟机调用过,那么这两种情况都视为“没有必要执行”。
  如果被判定为有必要执行finalize()方法,那么该对象将会被放置在一个F-Queue的队列中,在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize()方法是对对象逃脱死亡命运的最后一次机会,稍后收集器对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中要拯救自己——只需要重新与引用链上的任何一个对象建立关联即可。如将自己赋值给某个类变量或者对象的成员变量,第二次标记时它将被移出“即将回收”的集合。否则基本被回收。
一个对象的finalize()方法最多只会被系统自动调用一次。
可以理解为,对象在被回收之前可以使用此方法进行一次自救,或者处理在生前处理一些工作。此方法运行代价高昂,不确定性大。不推荐使用。它能做的所有工作,try-finally或其他方式都可以做得更好。

3.2.5 回收方法区

  方法区的垃圾收集内容主要回收两部分:废弃的常量不再使用的类型。回收废弃常量与回收JAVA堆中的对象非常类似。如"java"曾经进入常量池中,但当前没有一个字符串对象值是"java",发生内存回收时判断有必要采耳话,就会被清理出常量池。常量池中其他类(接口),方法,字段的符号引用也与此类似。
  判断一个类型属于“不再使用的类”需要同时满足三个条件:

  - 该类的所有实例都被回收。即堆中不存在该类以及任何派生子类的实例。
  - 加载该类的类加载器已被回收。(通常很难达成)
  - 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  JAVA虚拟机可以对满足三个条件的无用类进行回收,但不是必然回收,通过参数控制。
  在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要JAVA虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

3.3 垃圾收集算法

  本章中暂不过多探讨算法实现,只重点介绍分代收集理论和几种算法思想及其发展过程
  从如何判定对象消亡的角度出发,垃圾收集算法可以分为“引用计数式垃圾收集”(Reference CountingGC)和“追踪式垃圾收集”(Tracing GC)两大类,也常常被称作为:“直接垃圾收集”“间接垃圾收集”。

3.3.1 分代收集理论

它建立在两个分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  它们共同奠定多款常用垃圾收集器的一致设计原则:收集器应该将JAVA堆划分出不同区域,将回收对象按年龄(熬过垃圾收集过程的次数)分配到不同区域之中存储。每次回收时只关注如何保存少量存活而不是标记大量将要被回收的对象,就能以较低代价回收大量空间。同时虚拟机可以以较少的频率回收难以消亡的对象。同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
  JAVA堆分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而有了“MinorGC”、“MajorGC”、“FullGC”这样回收类型的划分。才能针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——“标记-复制算法”、“标记-清除算法”、“标记-整理算法”
  设计者一般至少会把JAVA堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。新生代中每次回收存活的对象会逐步晋升到老年代中存放。
  加上要进行一次只局限于新生代区域内的收集(Minor GC),但新生代对象完全可能被老年代引用,为了找出该区域中的存活对象,必须额外便利整个老年代所有对象来确保可达性分析结果的正确性。这无疑会给内存回收带来很大的性能负担。为此必须对分代收集理论添加第三条经验法则:
  3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
  可以得出的隐含推论是:存在互相引用关系的两个对象,应该倾向于同时生存或者同时消亡。根据这个假说,不必扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在以及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构将老年代划分成若干小块,标记老年代的哪一块内存存在跨代引用。此后发生MinorGC时,只有包含跨代引用的小块内存的对象才会被加入到GC Roots进行扫描。这种方法需要维护记录数据的正确性,增加运行时开销,但比起扫描整个老年代来说还是划算的。

一些名词:
  - 部分收集(Partial GC):指目标不是完整手机整个JAVA堆的垃圾收集。其中又分为:
    - 新生代收集(MinorGC/ YoungGC):指目标只是新生代的垃圾收集。
    - 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。MajorGC这个说和整堆收集有混淆。
    - 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。只有G1收集器会有这种行为。
  - *
整堆收集
(FullGC):收集整个JAVA堆和方法区的垃圾收集。

3.3.2 标记-清除算法

  “标记-清除”(Mark-Sweep)算法最早出现也最基础 。算法分为“标记”和“清除”两个阶段。分两个阶段,标记垃圾,清除。或者标记存活对象,清除未被标记的。
  后续收集算法大多是以“标记-清除”算法位基础,对其缺点改进而得到。主要缺点:
  1)执行效率不稳定,如果JAVA堆中包含大量对象,大部分需要被回收,就需要进行大量标记和清除动作。效率随对象数量增长而降低。
  2)空间碎片化问题,清除后会产生大量不连续的内存碎片。碎片过多可能导致分配较大对象时不得不提前触发另一次垃圾收集动作。执行过程如图所示。图片说明

3.3.3 标记-复制算法

  通常被简称为复制算法。1969年Fenichel提出“半区复制”(Semispace Copying)的垃圾收集算法。保留一半空间,当需要垃圾收集时,把存活对象复制到另一半区,然后一次清空之前使用过的半区。
  优点:实现简单,运行高效。缺点:浪费一半空间。
图片说明
  1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出更优化的半区复制分代策略,称为“Appel式回收”。具体做法是将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理Eden和使用过的Survivor空间。当Survivor空间不足以容纳一次MinorGC之后存活的对象时,就需要依赖其他内存区域(大多数是老年代)进行分配担保(Handle Promotion)。


2021年3月25日打卡

3.3.4 标记-整理算法

  标记-复制算法在对象存活率较高时要进行多次复制操作,效率低下。还需要有额外的分配担保。因此老年代不能直接选用这种算法。
  针对老年代对象存亡特征,1974年提出“标记-整理”(Mark-Compact)算法。标记过和“标记-清除”算法一样,然后让所有存活对象向空间一端移动,然后直接清理掉边界以外的内存。如图所示
图片说明
  是否移动回收后的存货对象是一项优缺点并存的风险决策:
  如果移动存活对象并更新引用,会是一种极为负重的操作。并且必须全程暂停用户应用程序。“Stop The World”。
  但如果不移动和整理存活对象的话,会导致空间碎片化问题。

3.4 HotSpot算法细节实现。(跳)

  为稍后介绍各款垃圾收集器做前置知识铺垫,可以先跳,后续遇到实际场景实际问题时再结合问题,重新翻阅和理解。

3.5 经典垃圾收集器

  各款经典收集器之间的关系如图所示(如果两个收集器之间存在连线,说明他们可以搭配使用)
图片说明

3.5.1 Serial收集器

图片说明
  最基础,历史最悠久。单线程工作,垃圾收集工作时暂停其他所有工作线程。
  对于运行在客户端模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew收集器

  实质上是Serial收集器的多线程并行版本。除了Serial收集器之外,只有它能与CMS收集器配合工作。
JDK9以前,老年代用CMS,新生代默认用ParNew收集器配合工作。JDK9以后,G1全堆收集器不需要其他新生代收集器配合工作。
图片说明


在谈论垃圾收集器的上下文语句中。有必要先解释一下这其中的并发和并行:

  • 并行(Parallel):描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同过左,通常默认此时用户线程处于等待状态。
  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间收集器线程与用户线程都在运行。用户线程虽然未被冻结,但吞吐量会收到垃圾收集器线程影响。

    3.5.3 Parallel Scavenge收集器

      新生代收集器,基于标记-复制算法实现,也能够并行收集。区别是CMS等收集器关注如何缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器目标则是达到一个可控制的吞吐量(Throughput)。吞吐量的定义如图所示。
    图片说明
      停顿时间短能提升用户体验,高吞吐量可以更高效率利用处理器资源,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的分析任务。

  Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
  Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一
个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区
的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数
了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时
间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)
自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。

3.5.4 Serial Old收集器

  Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。供客户端模式下的HotSpot虚拟机使用。服务端模式下也有两种用途:1)JDK5以前和Parallel Scavenge收集器搭配使用。2)CMS收集器发生失败时的后背预案,冰法收集发生Concurrent Mode Failure时使用。

3.5.5 Parallel Old收集器

  老年代版本,支持多线程,基于标记-整理算法。
  在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

3.5.6 CMS收集器

  CMS(Concurrent Mark Sweep)收集器以最短回收停顿时间为目标的收集器。适合集中在网站上或基于浏览器B/S系统这类关注响应速度的的服务端上。基于标记-清除算法实现。整个过程分为四个步骤,包括:
  1)初始标记(CMS initial mark)
  2)并发标记(CMS concurrent mark)
  3)重新标记(CMS remark)
  4)并发清除(CMS concurrent sweep)
  初试标记和重新标记这两个步骤仍然需要“Stop The World”。初始标记时仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  并发标记阶段则是从直接关联对象开始遍历整个对象图的过程,耗时较长但是不需要停顿用户线程,能与垃圾收集线程一起并发运行。
  重新标记阶段则是为了修正并发标记期间,因用户程序继续运作导致标记长生变动的那一部分对象的标记记录,停顿时间比初始标记阶段稍长,但远比并发标记阶段时间短。
  并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,因为不需要移动存活对象,可以与用户线程同时并发。
  耗时最长的并发标记和并发清除阶段都可以与用户线程一起工作,整理来说,回收垃圾的过程是与用户线程一起并发执行的。工作如图所示。
图片说明
  最主要的优点:并发收集,低停顿,也被称为“并发低停顿收集器”(Concurrent Low Pause Collector)。
  但也存在三个明显的缺点:
  1)对处理器资源非常敏感。处理器核心数量四个以上时,占用很少,但不足四个时,影响就会很大。因为要分出一半的运算能力去执行垃圾收集器线程,导致用户程序的执行速度忽然大幅降低。为缓解这种状况,虚拟机提供“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种。在并发标记,清理的时候让收集器线程、用户线程交替运行,清理垃圾时间会变长,但对程序影响会小一点。实践证明增量式CMS处理器效果很一般,JDK7开始被声明为deprecated。JDK9发布后i-CMS被完全废弃。
  2)CMS收集器无法处理“浮动垃圾”(Floating Garbage) 有可能出现“Concurrent Mode Failure"失败导致另一次完全“Stop The World”的Full GC产生。在并发标记和并发清理过程中用户线程仍在运行,会有新的垃圾,CMS无法在当次收集中处理它们,只能在下一次垃圾收集时清除,这部分垃圾就被叫做“浮动垃圾”。同时垃圾收集阶段用户线程还需要继续运行,就需要预留足够内存给用户线程使用,因此CMS不能像其他收集器那样等老年代被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。因此CMS不能等老年代快被填满了再收集,而是到一定阀值就开始运作。可以通过调整参数来设置CMS触发运作的百分比。如果出现“Concurrent Mode Failure”。虚拟机将会启动后背预案:冻结用户线程的执行,然后临时启用Serial Old收集器来重新进行老年代的垃圾收集,产生很长的停顿时间。


2021年3月26日打卡

  3)最后一个缺点就是因为CMS基于“标记-清除”算法实现,意味着收集结束时会有大量空间碎片产生。可以通过参数调整CMS进行FullGC时合并内存碎片,和执行若干次不整理空间的FullGC之后在下次FullGC时先进行碎片整理。

3.5.7 Garbage First收集器

  简称G1收集器。开创了收集器面向局部收集器的设计思路和基于Region的内存布局形式。G1收集器从JDK 6 Update 14开始测试,到JDK 7 Update 4才移除试验状态标识,直到JDK8 Update 40,提供并发的类卸载的支持,才被Oracle官方称为“全功能的垃圾收集器”(Full-Featured Garbage Collector)。
  G1是主要面向服务端应用的垃圾收集器。社记者们希望能够做出一款建立起“停顿时间模型”(Pause Prediction Model)的收集器,意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾手机上的时间大概率不超过N毫秒这样的目标。
  G1收集器出现之前所有其他收集器目标范围要么是整个新生代(MinorGC),要么是整个老年代(Major GC),要么是整个JAVA堆(FullGC)。G1则面向堆内存任何部分来组成回收集(Collection Set,简称CSet)进行回收。衡量标准不再是哪个分代,而是哪个内存中放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
  G1开创的基于Region堆内存布局是它能够实现这个目标的关键。G1仍遵循分代收集理论。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的JAVA堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演Eden空间,Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略处理,这样无论是新对象还是旧对象都能取得良好的收集效果。
  Region中还有一类特殊的Humongous区域,专门用来存储大对象,G1认为只要大小超过一个Region容量一半的对象就是大对象,每个Region大小可以通过参数设定,取值范围1MB~32MB,大小为2的N次幂。对于超过整个Region容量的超级大对象来说,会被存放在N个连续的Humongous Region之中,大多数行为都把HR作为老年代的一部分来看待。
  G1仍保留新生代老年代的概念,但它们不再是固定的,它们是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为他将每块Region作为单次回收的最小单元,每次收集的空间都是Region大小的整数倍,能有计划的避免在整个JAVA堆中进行全区域的垃圾收集。更具体的思路,在后台维护一个优先级列表,每次根据用户设定允许的手机停顿时间,优先处理回收机制最大的Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的手机效率。
  G1收集器Region分区示意图:
图片说明
  但G1收集器的细节问题却有很多需要解决:
  1)Region里面存在的跨Region引用对象如何解决?(使用记忆集,本质是一个哈希表,双向卡表结构,记录下“我指向谁”和“谁指向我”,因此G1收集器比其他传统垃圾收集器有更高的内存占用负担。)
  2)在并发标记阶段如何保证收集线程与用户线程互不干扰的运行?(3.4.6节)与CMS中的“Concurrent Mode Failure”失败会导致FullGC类似。内存回收的速度赶不上内存分配的速度,G1收集器也会被迫冻结用户线程执行,导致FullGC。
  3)怎样建立起可靠的停顿预测模型?

  G1收集器运作过程大致可以分为以下四个步骤:
  1.初始标记(Initial Marking):借用Minor GC的时候同步完成的。和CMS过程类似。
  2.并发标记(Concurrent Marking):这个过程会重新处理SATB记录下有并发时有引用变动的对象
  3.最终标记(Final Marking):短暂停顿,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
  4.筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。操作涉及到存活对象的移动,必须暂停用户线程,多条收集器线程并行完成。
  G1除了并发标记外,其余阶段完全暂停用户线程。并非纯粹追求低延迟,官方设定目标是延迟可控的情况下获得尽可能高的吞吐量。担当起“全功能收集器”的重任与期望。
  G1收集器运行示意图:
图片说明
  G1收集器设置的停顿时间必须符合实际。默认200ms。
  G1和CMS相比的优点:指定最大停顿时间,分Region的内存布局,按收益动态确定回收集。整体来看基于“标记-整理”算法,但从局部(两个Region之间)来看又基于“标记-复制”算法。意味着G1运作期间不会产生内存碎片,收集完成之后提供规整的可用内存,有利于程序长时间运行,不容易因分配大对象内存不足而提前触发下一次收集。
  相比CMS的缺点:内存占用(Footprint)(卡表更复杂)和运行时的额外执行负载(Overload)(除了实现写屏障,还实现了写前屏障,用来实现原始快照搜索(SATB))都要比CMS高。

3.6 低延迟垃圾收集器

  衡量垃圾收集器的三项最重要指标:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。一款优秀的收集器通常最多可以同时达成其中两项。
  这三项指标中,延迟的重要性日益凸显。各类收集器并发情况如图所示:
图片说明
  Shenandoah,ZGC垃圾收集器(略)

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器

  一款不能进行垃圾收集器的垃圾收集器。
###3.7.2 收集器的权衡
  收集器的权衡主要受以下三个因素影响:
  1)应用程序的主要关注点:吐吞量?延迟?内存占用?
  2)运行应用的基础设施如何?如硬件规格,处理器数量,内存大小,操作系统是何种。
  3)使用JDK的发行商是什么?版本号是多少?
  实战中切不可纸上谈兵,根据实际情况进行测试才是选择收集器的最终依据。
###3.7.3 虚拟机及垃圾收集器日志
  JDK9以前没有统一日志处理框架。日志级别从低到高,有Trace,Debug,Info,Warning,Error,Off六种级别,日志级别决定了输出信息的详细程度。默认级别为Info,还可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容。包括:

·time:当前日期和时间。
·uptime:虚拟机启动到现在经过的时间,以秒为单位。
·timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
·uptimemillis:虚拟机启动到现在经过的毫秒数。
·timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
·uptimenanos:虚拟机启动到现在经过的纳秒数。
·pid:进程ID。
·tid:线程ID。
·level:日志级别。
·tags:日志输出的标签集。

2021年3月27日 打卡

获得垃圾收集器相关信息

1)查看GC基本信息:JDK9之前使用-XX:+PrintGC

2)查看GC详细信息:JDK9之前使用-XX:+PrintGCDetails

3)查看GC前后的堆、方法区可用容量变化:JDK9之前使用-XX:+PrintHeapAtGC

4)查看GC过程中用户线程并发时间以及停顿时间:JDK9之前使用:-XX:+PrintGCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime

5)查看收集器Ergonomic机制(自动设置堆空间各分代区域大小、收集目标等内容,Parallel收集器开始支持)自动调节的相关信息:JDK9之前使用-XX:+PrintAdaptiveSizePolicy

6)查看熬过收集之后剩余对象的年龄分布信息:JDK9之前使用-XX:+PrintTenuringDistribution

3.7.4 垃圾收集器参数总结

(略)

3.8 实战:内存分配与回收策略

JAVA技术体系的自动内存管理,最根本的目标是自动化的解决两个问题:

1)自动给对象分配内存

2)自动回收分配给对象的内存

对象的内存分配,概念上来说,都是在堆上分配(实际也有可能经过即时编译猴被拆散为标量类型并间接的在栈上分配(即时编译器的栈上分配优化参加11章))。经典分代的设计下,新生对象分配在新生代,少数情况(如对象大小超过一定阀值)也可能直接分配在老年代。对象分配的规则并不固定,取决于当前虚拟机使用的垃圾收集器,和内存相关的参数设定。

内存分配上,大家主要学习的是分析方法,列举的分配规则反而是次要的。

3.8.1 对象优先在Eden分配

大多数情况下,对象在新生代Eden区分配。Eden区没有足够空间分配时,虚拟机将发起一次MinorGC。

3.8.2 大对象直接进入老年代

典型的大对象是很长的字符串,或者元素数量很庞大的数组。 JAVA虚拟机中要避免大对象的原因是,分配空间时,容易导致内存明明还有不少空间就提前出发垃圾收集,以获取足够的连续空间,复制对象时,意味着高额的内存复制开销。HotSpot虚拟机提供一个参数(只支持Serial和ParNew收集器),指定大于该设置值的对象直接分配在老年代。目的就是为了避免在Eden区以及两个Survivor区之间来回复制,产生大量的内存复制操作。

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

HotSpot虚拟机多数收集器采用分代收集来管理内存,回收内存时就要决策哪些存货对象放在新生代,哪些放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。存储在对象头中。对象通常在Eden区诞生,经过第一次MinorGC后仍然存活,并能被Survivor容纳的话,就会被移到Survivor空间中,并设置年龄为一岁。每熬过一次MinorGC,年龄就增加一岁,年龄增加到一定程度(默认15)就会被晋升到老年代中。如设置年龄为1,则第二次GC发生后,年龄为1的对象就会进入老年代。

3.8.4 动态对象年龄判定

为了更好适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄段必须达到阀值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到阀值中要求的年龄。

3.8.5 空间分配担保

发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么这一次MinorGC可以确保是安全的。如果不成立,虚拟机会先查看-XX:HandlePromotionFailure参数是否允许担保失败(Handle Promotion Failure);如果允许,继续检查以上条件,如果大于,将尝试一次MinorGC,尽管有风险;如果小于,或者设置不允许冒险,那么就要改为进行一次FullGC。

冒险:最极端的情况,新生代所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。

3.9 本章小结

介绍了垃圾收集算法,HotSpot虚拟机垃圾收集器的特点和运作原理。以及自动内存分配和回收的主要规则。

第4章 虚拟机性能监控,故障处理工具

4.1 概述

把前面所学的知识应用到实际工作中才是我们的最终目的。接下来的两张从实践的角度认识虚拟机内存管理的世界。

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。

数据:包括但不限于异常堆栈、虚拟机运行日志、垃圾收集器日志、线程快照(threaddump/javacore文件)、堆存储快照(heapdump/hprof文件)。

工具永远都是知识技能的一层包装,没有什么工具是“秘密武器”,拥有了就能包治百病。

4.2 基础故障处理工具

JDK的bin目录下有java.exe、javac.exe这两个命令行工具,除此之外还有一些其他小工具。

这些故障工具并不单纯是被Oracle公司作为“礼物”赠送给JDK使用者,根据软件可用性和授权的不同,可以把它们分为三类:

  • 商业授权工具:主要是JMC(Java Mission Control)以及它需要是用到的JFR(Java Flight Recorder),来自于JRockit的运维监控套件。JDK11之前无需独立下载,但商业环境需要付费。
  • 正式支持工具:被长期支持,不会突然消失。
  • 实验性工具:可能转正,可能突然消失,但通常非常稳定而且功能强大。

这些工具体积大多数体积都异常小,并非JDK开发团队刻意制作,而是因为这些命令行工具大多仅是一层包装,真正的代码是现在JDK的工具类库中。

用JAVA语言本身来实现这些故障处理工具是有特别用意的:应用部署到生产环境后,无论人工物理解除到服务器还是远程Telnet到服务器上都可能会受到限制。借助这些工具类库里面的接口和实现代码,开发者可以选择直接在应用程序中提供功能强大的监控分析功能。

4.2.1 jps:虚拟机进程状况工具

(JVM Process Status Tool).可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类),名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。功能比较单一,但使用频率最高。其他的JDK工具大多数需要输入它查询的LVMID来确定监控哪一个虚拟机进程。

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

(JVM Statistics Monitoring Tool)用于监视虚拟机各种运行状态信息的命令行工具。可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,没有GUI图形界面只提供纯文本控制台环境的服务器上它将是运行期定位虚拟机性能问题的常用工具。

4.2.3 jinfo:JAVA配置信息工具

(Configuration Info for Java)作用是实时查看和调整虚拟机各项参数。可以使用-flag选项查看虚拟机启动时未被显式指定的参数的系统默认值。也可以使用-sysprops把虚拟机金城的System.getPropeties()内容打印。

4.2.4 jmap:Java内存映像工具

(Memory Map for Java)用于生成堆转储快照(一般称为heapdump或dump文件),还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率,使用哪种收集器等。

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

(JVM Heap Analysis Tool)。此命令和jmap搭配使用, 来分析jmap生成的堆转储快照。jhap内置了一个微型的HTTP/WEB服务器,生成结果后可以再浏览器中查看。

然而实际工作中,多数人并不会直接使用jhat命令分析堆转储快照文件。原因有二:

1)分析工作耗时而且耗费硬件资源,一般会复制到其他机器上进行,这时就没必要使用命令行工具分析。

2)jhat分析功能简陋。

4.2.6 jstack:JAVA堆栈跟踪工具

(Stack Trace for Java)用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待什么资源。

4.2.7 基础工具总结

讲解了6个常用的命令行工具。

  • 基础工具:用于支持基本的程序创建和运行
  • 安全:用于程序前名、设置安全测试等
  • 国际化:用于创建本地语言文件
  • 远程方法调用:用于跨Web或网络的服务交互
  • JavaIDL与RMI-IIOP
  • 部署工具:用于程序打包、发布和部署
  • Java Web Start
  • 性能监控和故障处理:用于监控分析JAVA虚拟机运行信息,排查问题。
  • WebService工具:与CORBA一起在JDK11中被移除。
  • REPL和脚本工具

4.3 可视化故障处理工具

主要包括JConsole、JHSDB、VisualVM和JMC四个。

4.3.1 JHSDB:基于服务性代理的调试工具

一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。

服务性代理是HotSpot虚拟机中一组用于映射JAVA虚拟机运行信息的、主要基于JAVA语言(含少量JNI代码)实现的API集合。

服务性代理以HotSpot内部的数据结构为参照物进行设计,把这些C++的数据抽象出JAVA模型对象,相当于HotSpot的C++代码的一个镜像。

通过服务性代理的API,可以再一个独立的JAVA虚拟机的进城里分析其他HotSpot虚拟机的内部数据,或者从HotSpot虚拟机进程内存中dump出来的转储快照里还原出它的运行状态细节。

(这段东西属实给我绕晕了。真的。)

全部评论

相关推荐

头像
点赞 评论 收藏
转发
点赞 1 评论
分享
牛客网
牛客企业服务