JVM(GC算法理论 垃圾收集器 调优 G1 类加载)

GC

垃圾回收,首要明确什么是垃圾?其实是内存,java中主要是针对对象,当一个对象不能再从正在运行的程序中的任何指针到达时,它就被认为是垃圾
Java 是自动内存回收,编程上简单,系统不容易出错,不像C,C++,需要开发者手动管理内存的回收,一般手动内存管理普遍存在如下两个问题:
1、忘记回收,造成内存泄露。
2、多次回收,造成运行出错。

1.如何判断对象可以回收

在堆里面存放着几乎所有的对象实例,垃圾回收器在对对象进行回收前,要做的事情就是确定这些对象中哪些还是“存活”着,哪些已经“死去”(死去代表着不可能再被任何途径使用了) 。

1.1引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1。
引用计数法最大的问题在于:循环引用,比如A引用B,B引用A,但是A和B都没有被其他任何对象所引用,也不可能被任何途径所使用了,但是A和B各自引用计数为1,无法被回收。如果要想被回收需要采取额外的手段,但是这样很影响效率。

但是主流的虚拟机并没有采用这种方式
在这里插入图片描述

1.2.可达性分析算法

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为
引用链( Reference Chain ),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的
作为 GC Roots 的对象包括下面几种(重点是前面 4 种):

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
  • 方法区中常量引用的对象;比如:字符串常量池里的引用。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
  • JVM 的内部引用( class 对象、异常对象 NullPointException 、 OutofMemoryError
    ,系统类加载器)。(非重点)
  • 所有被同步锁( synchronized )持有的对象。(非重点)
  • JVM 内部的 JMXBean 、 JVMTI 中注册的回调、本地代码缓存等(非重点)
  • JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收时只回收部分代的对象)(非重点)
    在这里插入图片描述
    以上的回收都是普通的对象,类( Class )的回收条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
    1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
    2、 加载该类的 ClassLoader 已经被回收。
    3、 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    4、 参数控制 -Xnoclassgc ,就是禁用 Class 的回收
    在这里插入图片描述
    另外:
    废弃的常量和静态变量的回收其实就和 Class 回收的条件差不多!!!

1.3、finalize

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize ),我们可以在 finalize 中去救,俗称对象的自我救赎。

需要注意的是:
1、 finalize 只会执行一次,不会多次执行。
2、建议大家尽量不要使用finalize,因为这个方法太不可靠。

GC算法理论

2.1、分代回收理论

当前商业虚拟机的垃圾回收器,大多遵循“分代收集”的理论来进行设计,这个理论大体上是这么描述的:
1、 绝大部分的对象都是朝生夕死。
2、 熬过多次垃圾回收的对象就越难回收。
根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代老年代,并且不同的分代采用的回收算法不一样。
在这里插入图片描述
同时对于GC的叫法,大体有这么几种:
1、 新生代回收( Minor GC/Young GC ):指的是进行新生代的回收。
2、 老年代回收( Major GC/Old GC ):指的是进行老年代的回收。目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。( Major GC 定义相对没有那么明确,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法,有时候 Major GC 和 Full GC 大致是等价的)
3、 整堆回收( Full GC ):定义相对明确,收集整个 Java 堆和方法区(注意包含方法区)

2.2、复制算法

原始的复制算法(Copying)是这样的:
1、将内存按容量划分为大小相等的两块,每次只使用其中的一块
2、当其中一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
在这里插入图片描述
带来的好处是:
1、实现简单,运行高效,
2、每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,

存在的弊端是:
1、内存的使用率缩小为原来的一半。
2、内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。

适用场景
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
但是像 hotspot 这样的虚拟机大都对原生的复制算法进行了改进,因为它对内存空间的利用率不高,而且专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,所以改进后的复制算法策略是:
1、将新生代划分为一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To ) , HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 。
2、每次使用 Eden 和其中一块 Survivor ,当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
在这里插入图片描述
在这样的算法下,
1、每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被 “浪费”
2、当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保( Handle Promotion )

2.3、标记-清除算法

标记-清除(Mark-Sweep)算法分为“标记”和“清除”两个阶段:
1、首先扫描所有对象标记出需要回收的对象,
2、在标记完成后扫描并回收所有被标记的对象,故需要两次扫描

在这里插入图片描述
注意:
1、回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低,所以该算法不适合新生代。
2、它的主要问题是在标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
3、标记清除算法适用于老年代。

2.4、标记-整理算法

标记-整理(Mark-Compact)算法逻辑如下:
1、首先标记出所有需要回收的对象,
2、在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动
3、然后直接清理掉端边界以外的内存。
在这里插入图片描述
注意:
1、标记整理需要扫描两遍
2、标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新(直接指针需要调整)。
3、标记整理算法不会产生内存碎片,但是效率偏低。
4、标记整理算法适用于老年代。
所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。

)

2.5、分代垃圾回收机制

在这里插入图片描述
针对不同区域,分配不同的回收机制
新生代:用完就可以丢弃,朝生夕死,频繁
老年代:更有价值,长时间使用

在这里插入图片描述
在这里插入图片描述

伊甸园:对象创建,诞生,分配在伊甸园区,当空间不够时会触发一次Minor GC,采用复制算法,把存活的放到to,让幸存的对象寿命+1,然后交换from和to的位置,当寿命超过阈值(最大是15(4bit)),会放入老年代(每个jvm的阈值不同)
Minor GC会引发stop the world,必须暂停其他用户的线程,垃圾回收线程先工作,当我把垃圾回收动作完成后,其他用户线程才恢复运行(对象地址会改变)

在这里插入图片描述
当老年代内存不够,会先尝试一次MInor gc,如果之后空间扔不足,那么触发一次Full GC,也会引发stop the world ,STW的时间更长

在这里插入图片描述

一个线程中出现问题,不会导致主线程结束

3、垃圾回收器

常见的垃圾回收器如下:

在这里插入图片描述
官方文档建议多阅读:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/
Java HotSpot VM 包括三种不同类型的收集器,每种收集器具有不同的性
能特征:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/
collectors.html#sthref27

1、串行收集器( SerialGC ):使用单个线程来执行所有垃圾收集工作,这使得它相对高效,因为线程之间没有通信开销。它最适合单处理器机器,因为它不能利用多处理器硬件。
2、并行收集器(也称为吞吐量收集器, ParallelGC ):并行执行 Minor Gc ,可以显着减少垃圾收集开销,它适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序(有多个 GC 线程)。
3、并发收集器( CMS , G1 ):大多数并发收集器同时执行其大部分工作(例如,当应用程序仍在运行时)以保持垃圾收集暂停较短。它专为具有中型到大型数据集的应用程序而设计,其中响应时间比总吞吐量更重要,因为用于最小化暂停的技术会降低应用程序性能。

另外必须要知道的几个知识点:
1、用户/工作线程:java程序运行后,用户不停请求操作jvm内存,这些称为用户线程
2、GC线程:jvm系统进行垃圾回收启动的线程
3、串行:GC采用单线程,收集时停掉用户线程
4、并行:GC采用多线程,收集时同样要停掉用户线程
5、并发:用户线程和GC线程同步进行,这意义就不一样了
6、 STW ,很重要的一个概念,
6.1、 Stop一the一World ,简称 STW ,指的是 GC 事件发生过程中,会暂停所有的工作线程,产生应用程序的停顿,没有任何响应,有点像卡死的感觉,这个停顿称为 STW 。
6.2、被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,所以我们要减少 STW 发生的频率和时间。
6.3、 STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1也不能完全避免 Stop一the一world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
6.4、 STW 是 JVM 在后台自动发起和自动完成的,用户不可控。
6.5、参数: -XX:MaxGCPauseMillis= 用于设置最大暂停时间,
这被解释为对垃圾收集器的提示,即 需要毫秒或更短的暂停时间。垃圾收集器将调整与垃圾收集相关的 Java 堆大小和其他参数,以尝试将垃圾收集暂停短于 毫秒。默认情况下没有最大暂停时间目标。这些调整可能会导致垃圾收集器更频繁地发生,从而降低应用程序的整体吞吐量。

3.1、SerialGC

1、JVM 刚诞生就只有这种,比较古老,串行的,独占式,成熟,适合单CPU。
2、比较适用于小数据集(约100M)的应用程序在多处理器上适用,超过这个大小的内存回收速度很慢,所以对于现在来说这个垃圾回收器已经是一个鸡肋。
3、要启动可以通过参数: -XX:+UseSerialGC
4、 Minor GC 采用的是复制算法, Major GC 采用的是标记-整理算法。
在这里插入图片描述

3.2、ParallerGC(吞吐量)

1、 Parallel Scavenge(ParallerGC) :叫做并行收集器,关注的指标是吞吐量,它充分利用多核多线程使得 Minor GC 有着良好的性能表现,可以显着减少垃圾收集开销。线程数可通过 -XX:ParallelGCThreads=threads 设置,默认值取决于CPU核数。
2、启用该收集器通过参数: -XX:+UseParallelGC 。
3、吞吐量指标的定义:根据收集垃圾所花费的时间和应用程序时间的时间来衡量的,即吞吐量=应用程序执行时间/(应用程序执行时间+垃圾收集时间),比如:虚拟机总共运行了100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
4、吞吐量指标可以通过参数: -XX:GCTimeRatio= ,该设置表明垃圾回收时间与应用程序时间之比为 1 / (1 + )。例如, - XX:GCTimeRatio=19 ,证明垃圾收集时间不能超过总时间的 1/20 即 5% 。
5、开启并行压缩特性有助于在 Major GC 时采用多线程并行处理,否则Major GC 将采用单线程,默认情况下当我们使用 -XX:+UseParallelGC 启用并行收集器时,并行压缩特性也默认开启,当然也可以通过参数 -XX:- UseParallelOldGC 关闭。
6、参数 -XX:+UseAdaptiveSizePolicy :是一个开关, 当这个参数被激活之后,就不需要人工指定新生代的大小( -Xmn )、 Eden 与 Survivor 区的比例( -XX:SurvivorRatio )、 晋升老年代对象大小( - XX:PretenureSizeThreshold=value )等细节参数了,虚拟机会根据当前系统
的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。默认开启。
7、 Minor GC 采用的是复制算法, Major GC 采用的是标记-整理算法。
8、适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序。

在这里插入图片描述
9、 jdk1.8 hotspot 64bit vm 默认采用的是 ParallelGC ,通过如下命令可查看

C:\Users\22863>java -XX:+PrintCommandLineFlags -version 
-XX:InitialHeapSize=264361280 -XX:MaxHeapSize=4229780480 - 
XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers - 
XX:+UseCompressedOops -XX:- 
UseLargePagesIndividualAllocation -XX:+UseParallelGC 
java version "1.8.0_281" 
Java(TM) SE Runtime Environment (build 1.8.0_281-b09) 
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed 
mode) 

3.3、CMS(响应时间优先)

CMS全称:Concurrent Mark Sweep,并发标记清除,该收集器开拓了并发回收的先河,是一种以获取最短 STW 时间为目标的收集器,它的主要特征如下:
1、前面的收集器都是要停止用户线程的,而CMS收集器可以在某一阶段让用户线程和GC线程在同一时间一起工作,以减少 STW 时间,
2、CMS主要用于回收老年代。
3、使用CMS可以通过参数: -XX:+UseConcMarkSweepGC ,

在这里插入图片描述

4、该垃圾回收器适合回收堆空间在 几个G~ 20G 左右
5、从名字(包含 Mark Sweep )上就可以看出,CMS 收集器是基于 标记— 清除 算法实现的
CMS的整体执行过程分成5个步骤,其中标记阶段包含了三步,具体细节如下:
1、初始标记:标记 GC Roots 直接关联的对象,会导致 STW ,但是这个没多少对象,时间短 。
2、并发标记:从 GC Roots 开始关联的所有对象开始遍历整个可达路径的对象,这步耗时比较长,所以它允许用户线程和GC线程并发执行,并不会导致STW ,但面临的问题是可能会漏标,标记变动等问题。
3、重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段会导致 STW ,但是停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4、并发清除;将被标记的对象清除掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程并发运行。
5、重置线程:重置GC线程状态,等待下次CMS的触发,与用户线程同时运行。
在这里插入图片描述
当然,在CMS中也会出现一些问题,主要有以下几点:
1、CPU敏感:对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大
2、浮动垃圾由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉,这一部分垃圾就称为“浮动垃圾”(比如用户线程运行产生了新的 GC Roots )。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收,在 1.6 的版本中老年代空间使用率阈值(92%) ;如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure ,这时虚拟机将临时启用 Serial Old 来替代 CMS。
3、空间碎片:这是由于CMS采用的是标记-清除算法导致的,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个参数: -XX:+UseCMSCompactAtFullCollection ( HotSpot™ 64-Bit Server VM is deprecated ),一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程;这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,回收时会暂停用户线程,然后进行空间整理。所以如果分配的对象较大,且较多时,CMS 发生这样的情况会很卡。

3.4、ParNew

多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。和 Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少。(在 JDK9 以后,把ParNew 合并到了 CMS 了) 大致了解下搭配关系即可,后续版本已经接近淘汰。

3.5、G1

G1全称:Garbage First,是一种服务器式垃圾收集器,针对具有大内存的多处理器机器。它试图以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。
G1是全堆操作且与应用程序线程并发执行,并通过多种技术实现高性能和暂停时间目标。G1的产生是为解决CMS算法产生空间碎片和其它一系列的问题缺陷,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
JDK9默认G1为垃圾收集器的提案:https://openjdk.java.net/jeps/24
8 将CMS标记为丢弃的提案:https://openjdk.java.net/jeps/291

3.5.1、设计思想

关于G1: 1、G1打破了之前的传统观念,将堆被划分为一组大小相等的堆区域(称
作: Region ),每个 Region 都是一个连续的虚拟内存, Region 的大小可以通过参数 -XX:G1HeapRegionSize=value 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。
2、G1 执行并发全局标记后,可以确定整个堆中对象的活跃度。标记阶段完成后,G1 知道哪些区域大部分是空的。它首先收集这些区域,这通常会产生大量可用空间,得到的收益是最高的。这就是为什么这种垃圾收集方法被称为垃圾优先。顾名思义,G1 将其收集和压缩活动集中在堆中可能充满可回收对象的区域,即垃圾。
3、G1 使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量
4、G1 将存活的对象从堆的一个或多个 Region 复制到堆上的单个其他Region ,并在此过程中压缩和释放内存。这个工作是在多处理器上并行执行,以减少暂停时间并提高吞吐量。因此,每次垃圾回收时,G1 都会不断努力减少碎片。CMS(并发标记清除)垃圾收集不进行压缩。并行压缩仅执行全堆压缩,这会导致相当长的暂停时间。

5、G1并没有抛弃之前对堆内存逻辑上的划分,它依然存在 eden 、 survivor 、 old ,同时多了一个 humongous (巨大的)区来存大对象。但是,这些区在物理地址上不再连续,也就是说G1划分出来的每一个 Region 都可以是以上角色中的一个,还可以在某个时刻转变角色,比如从 eden 变成 old!(这些角色就是个标签)。
Humongous 专门用来存储大对象。 G1 认为只要大小超过了一个Region 容量一半的对象即可判定为大对象,对于那些超过了整个 Region容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中。

在这里插入图片描述在这里插入图片描述

3.5.2、参数设置

1、启用G1收集器: -XX:+UseG1GC
在这里插入图片描述

2、设置分区大小: -XX:G1HeapRegionSize=value ,
在这里插入图片描述3、设置最大GC暂停时间: -XX:MaxGCPauseMillis=time

在这里插入图片描述
4、设置堆的最大内存,对于需要大堆( >6GB )且GC延迟需求有限(稳定且可预测的暂停时间低于0.5秒)的应用程序,推荐使用G1收集器。

3.5.3、运行过程

在这里插入图片描述
1、初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,STW,单线程执行。
2、并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
3、重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录。STW,并发执行。
4、筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出后移动合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必须STW。

在这里插入图片描述

在这里插入图片描述

把整个堆内存划分成大小相等的区域,每个区域都可以独立作为伊甸园,幸存区,老年代。

在这里插入图片描述
新创建的时候会放入中伊甸园,当慢慢被占满,会触发一个STW,新生代的垃圾回收,把幸存的对象以复制的算法放入幸存区

在这里插入图片描述
当幸存区对象多了,有部分幸存区的对象会晋升到老年代,不够年龄会放到另一个幸存区。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
CMS,G1并发收集时,回收速度比不上产生的速度时,才会触发full gc

跨代引用
在这里插入图片描述
有新生代引用老年代,有的话老年代标记一个臧卡,以后回收时查找标记部分就可以了。

Remark重新标记阶段
在这里插入图片描述

当对象的引用发生改变时,jvm会加入一个写屏障,会把C加入队列中,并且把C变成灰色,等并发标记结束了,重新标记的时候检查队列中的对象

JDK8以后的一些优化

8u20字符串去重
在这里插入图片描述
8u40并发标记类卸载
在这里插入图片描述
8u60回收巨型对象
在这里插入图片描述
巨型对象:占好几个区。
在这里插入图片描述
并发标记起始时间的跳转
在这里插入图片描述

思考一下,这属于什么算法呢???
答:从Region的动作来看G1使用的是标记-复制算法。而在全局视角上,类似标记 - 整理
总结
G1前面的几步和CMS差不多,只有在最后一步,CMS是标记清除,G1需要合并Region属于标记整理

3.5.4、优缺点

1、并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并发标记阶段。其他还是需要STW
2、分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代;
3、空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。
4、可预测性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

几点建议:
1、如果应用程序追求低停顿,可以尝试选择G1;
2、经验值上,小内存6G以内,CMS优于G1,超过8G,尽量选择G1
3、是否代替CMS只有需要实际场景测试才知道。(如果使用G1后发现性能还不如CMS,那么还是选择CMS)

垃圾回收调优

在这里插入图片描述

调优领域

  • 内存
  • 锁竞争
  • spu占用
  • io

确定目标

【低延迟】还是【高吞吐量】
CMS,G1,ZGC 低延迟
ParallelGC 高吞吐

最快的GC是不发送GC

自身程序出发,是不是加载了无用的数据,对象的大小,最后考虑第三方的优化
在查看FullGC前后的内存占用,考虑下面几个问题

  • 数据是不是太多?
    resultSet=statement.executeQuery("select * from 大表 limit ")
    内存再大也架不住多个数据加载多次到堆内存

  • 数据表示是否太臃肿

  • 对象图

  • 对象大小
    一次请求响应里,要的数据要有针对性,用到哪个查哪个
    new Object(对象大小) 就要占16字节,包装类要24个字节,int 4个字节

  • 是否有内存泄漏

  • static Map map不断往里放对象,会造成内存吃紧(长时间存活的对象)

  • 软引用

  • 弱引用

  • 第三方缓存实现

新生代调优

新生代的特点

  • 所有的new操作的内存分配非常廉价
  • TLAB thread-loacl allocation buffer 每个线程会给伊甸园分配一块私有的缓存区
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • Minor GC的时间远远低于Full GC
    在这里插入图片描述
    空间越大,吞吐量也会越来越高。主要耗费在复制上,但因为是朝生夕死,也还好

新生代能容纳所有【并发量*(请求-响应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得到,让长时间存活对象尽快晋升
    在这里插入图片描述

老年代调优

在这里插入图片描述

调优案例

1.Full GC和Minor GC频繁
空间紧张,如果是新生代空间紧张,业务高峰期来了,大量对象被创建,很快新生代空间塞满,幸存区空间紧张了,晋升阈值降低,本来生存周期很短的会晋升到老年代去,老年代有很多生命周期短的对象,进一步发生老年代的Full GC
增加新生代内存

2.请求高峰期发生Full GC,单词暂停时间特别长(CMS)
在这里插入图片描述

重新标记是最耗时的,会扫描整个堆内存,业务高峰的时候新生代对象产生多
重新标记前,先做一次垃圾清理 -XX:+CMSScavengeBeforeRemark

3.老年代充裕情况下,发生Full GC(CMS jdk1.7)
jdk1.7永久带空间设置小了,会触发Full GC,
jdk1.8元空间不由java控制,使用操作系统的空间

二、jvm优化

运行参数

  • 标准参数
    -help
    -version
  • -X参数(非标准参数)
    -Xint
    -Xcomp
  • -XX参数(使用率较高,调优时多用)
    -XX:newSize
    -XX:+UseSerialGC

具体参数

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等。

以上三个参数的设置都是默认以Byte为单位的,也可以在数字后面添加[k/K]或者[m/M]来表示KB或者MB。而且,超过机器本身的内存大小也是不可以的,否则就等着机器变慢而不是程序变慢了。

-Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M

-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存

-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

典型设置:

java-Xmx3550m -Xms3550m -Xmn2g -Xss128k

  • Xmx3550m

:设置JVM最大可用内存为3550M。

-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k-XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4

:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxPermSize=16m:设置持久代大小为16m。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

垃圾收集器-G1

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,代替CMS。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单三步即可完成调优:

  1. 开启G1垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大的停顿时间

-XX:+UseG1GC
使用G1垃圾收集器

-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒

-XX:G1HeapRegionSize=n
设置G1区域的大小。值是2的幂,范围是1MB到32MB之间。目标是根据最小的java堆大小划分出约2048个区域
默认是堆内存的1/2000

-XX:ParallelGCThreads=n
设置STW工作线程数的值,将n的值设置为逻辑处理器的数量。n的值与逻辑初期的数量相同,最多为8

-XX:ConcGCThreads=n
设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右

-XX:lnitiatingHeapOccupancyPercent=n
设置触发标记周期的java堆占用率阈值,默认占用率是整个java堆的45%

-XX:+PrintGCDetails
控制台打印GC信息

-Xloggc:F://xx/gc.log
日志文件的输出路径,会输出日志到F盘xx文件夹中gc.log文件

-XX:UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m

GC Easy可视工具

http://gceasy.io

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

类加载

通过字节码,我们了解了class文件的结构
通过运行数据区,我们了解了jvm内部的内存划分及结构
接下来,让我们看看,字节码怎么进入jvm的内存空间,各自进入那个空间,以及怎么跑起来
在这里插入图片描述

1、加载

1.1、概述

类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态数据结构转化为方法区中运行的数据结构,并且在堆内存中生成一个java.lang.Class对象作为访问方法区数据结构的入口。
在这里插入图片描述
注意:

  • 加载的字节码来源,不一定非得是class文件,可以是符合字节码规范的任意地方,甚至二进制流等
  • 从字节码到内存,是由加载器(ClassLoader)完成的,下面我们详细 看一下加载器相关内容

1.2、系统加载器

jvm提供了3个系统加载器,分别是Bootstrp loader、ExtClassLoader、AppClassLoader

这三个加载器互相成父子继承关系
在这里插入图片描述
1)Bootstrp loader
Bootstrp加载器是用C++语言写的,它在Java虚拟机启动后初始化
它主要负责加载以下路径的文件:

  • %JAVA_HOME%/jre/lib/*.jar
  • %JAVA_HOME%/jre/classes/*
  • -Xbootclasspath参数指定的路径
System.out.println(System.getProperty("sun.boot.class.path" ));

2)ExtClassLoader
ExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader
ExtClassLoader主要加载:

  • %JAVA_HOME%/jre/lib/ext/*
  • ext下的所有classes目录
  • java.ext.dirs系统变量指定的路径中类库
System.getProperty("java.ext.dirs")

3)AppClassLoader
AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个
getSystemClassLoader方法,此方法返回的就是它。

  • 负责加载 -classpath 所指定的位置的类或者是jar文档
  • 也是Java程序默认的类加载器
System.getProperty("java.class.path")

4)验证
很简单,使用一段代码打印对应的property信息就可以查到当前三个类加
载器所加载的目录

public static void main(String[] args) { 
String[] bootstrap = 
System.getProperty("sun.boot.class.path").split(":"); 
String[] ext = 
System.getProperty("java.ext.dirs").split(":"); 
String[] app = 
System.getProperty("java.class.path").split(":"); 
System.out.println("bootstrap:"); 
for (String s : bootstrap) { 
System.out.println(s); 
}
System.out.println(); 
System.out.println("ext:"); 
for (String s : ext) { 
System.out.println(s); 
}
System.out.println(); 
//app是默认加载器,注意启动控制台的 -classpath 选项 
System.out.println("app:"); 
for (String s : app) { 
System.out.println(s); 
} 
} 

1.3、自定义加载器

除了上面的系统提供的3种loader,jvm允许自己定义类加载器,典型的在tomcat上:

在这里插入图片描述

1.4、双亲委派

1)概述
在这里插入图片描述
类加载器加载某个类的时候,因为有多个加载器,甚至可以有各种自定义的,他们呈父子继承关系。
这给人一种印象,子类的加载会覆盖父类,其实恰恰相反!
与普通类继承属性不同,类加载器会优先调父类的load方法,如果父类能加载,直接用父类的,否则最后一步才是自己尝试加载,从源代码上可以验证。

ClassLoader.loadClass()方法:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 首先,检测是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
            //如果没有加载,开始按如下规则执行:
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    //重点!父加载器不为空则调用父加载器的 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                    //父加载器为空则调用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                //父加载器没有找到,则调用findclass,自己 查找并加载
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

2)为什么这么设计呢?
避免重复加载、 避免核心类篡改
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java。
API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class
即便是父类没加载,也会优先让父类去加载特定系统目录里的class,你获取到的依然是jvm内的核心类,而不是你胡乱改写的。这样便可以防止核心API库被随意篡改。

2、验证

加载完成后,class里定义的类结构就进入了内存的方法区。
而接下来,验证是连接阶段的第一步。实际上,验证和上面的加载是交互进行的(比如class文件格式验证)。
而之所以把验证放在加载的后面,是因为除了基本的class文件格式,还需要其他很多验证,我们逐个来看:

2.1、文件格式验证

这个好理解,就是验证加载的字节码是不是符合规范

  • 是不是CAFEBABYE开头
  • 主次版本号是否在当前jvm虚拟机可运行的范围内
  • 常量池类型对不对
  • 有没有其他不可识别的信息
  • ……等

总之,要满足合法的字节码约束

2.2、元数据验证

到java语法级别了。这个阶段主要验证属性、字段、类关系、方法等是否合

  • 是否有父类?除了Object其他类必须有
  • 是否继承了不该被继承的类,比如final
  • 是不是抽象类,是的话,方法都完备了没
  • 字段有没问题?是不是覆盖了父类里的final
  • ……等
    总之,经过这个阶段,你的类对象结构是ok的了

2.3、字节码验证

最复杂的一个阶段。
等等,字节码前面不是验证过了吗?咋还要验证?
上面的验证是基本字节表格式验证。而这里主要验证class里定义的方法,看方法内部的code是否合法。

  • 类型转换是不是有问题?
  • 指令是否跳到了方法外的字节码上?
  • ……

经过本阶段,可以确保你的代码执行时,不会发生大的意外
注意!不是完全不会发生。比如你写了一段代码,jvm只会知道你的方法执行时符合系统规则。
它也不知道你会不会执行很长很长时间导致系统卡死

2.4、符号引用验证

最后一个阶段。
这个阶段也好理解,我们上面的字节码解读时,知道字节码里有的是直接引用,有的是指向了其他的字节码地址。
而符号引用验证的就是,这些引用的对应的内容是否合法。

  • utf8里记了某个类的名字,这个类存在不?
  • 方法或字段引用,这些方法在对应的类里存在不存在?
  • 类、字段、方法等上面的可见性是否合法
  • ……

3、准备

这个阶段为class中定义的各种类变量分配内存,并赋初始值。
所做的事情好理解,但是要注意几点:

3.1、变量类型

注意是类变量,也就是类里的静态变量,而不是new的那些实例变量。new的在下面的初始化阶段

  • 类变量 = 静态变量
  • 实例变量 = 实例化new出来的那些

3.2、存储位置

理论上这些值都在方法区里,但是注意,方法区本身就是一个逻辑概念。
1.6里,在永久代
1.8以后,静态类变量如果是一个对象,其实它在堆里。这个上面我们讲方法区的时候验证过

3.3、初始化值

这个值进入了内存,那到底内存里放的value是啥?
注意!
即便是static变量,它在这个阶段初始化进内存的依然是它的初始值!
而不是你想要什么就是什么。
看下面两个实例

//普通类变量:在准备阶段为它开了内存空间,但是它的value是int的初始 
值,也就是 0//而真正的123赋值,是在类构造器,也就是下面的初始化阶段 
public static int a = 123; 
//final修饰的类变量,编译成字节码后,是一个ConstantValue类型 
//这种类型,在准备阶段,直接给定值123,后期也没有二次初始化一说 
public static final int b = 123;

4、解析

解析阶段开始解析类之间的关系,需要关联的类被加载。
这涉及到:

  • 类或接口的解析:类相关的父子继承,实现的接口都有哪些类型?
  • 字段的解析:字段对应的类型?
  • 方法的解析:方法的参数、返回值、关联了哪些类型
  • 接口方法的解析:接口上的类型?

经过解析后,当前class里的方法字段父子继承等对象级别的关系解析完成。
这些操作上相关的类信息也被加载。

5、初始化

5.1、概述

最后一个步骤,经过这个步骤后,类信息完全进入了jvm内存,直到它被垃圾回收器回收。
前面几个阶段都是虚拟机来搞定的。我们也干涉不了,从代码上只能遵从它的语法要求。
而这个阶段,是赋值,才是我们应用程序中编写的有主导权的地方

在准备阶段,jvm已经初始化了对应的内存空间,final也有了自己的值。但是其他类变量,是在这里赋值完成的。
也就是我们说的:

public static int a = 123;

这行代码的123才真正赋值完成

5.2、两个初始化

1)类变量与实例变量的区分
注意一件事情!
这里所说的初始化是一个class类加载到内存的过程,所谓的初始化值得是
类里定义的类变量。也就是静态变量。
这个初始化要和new一个类区分开来。new的是实例变量,是在执行阶段才创建的。

2)实例变量创建的过程
当我们在方法里写了一段代码,执行过程中,要new一个类的时候,会发生以下事情:

  • 在方法区中找到对应类型的类信息
  • 在当前方法栈帧的本地变量表中放置一个reference指针
  • 在堆中开辟一块空间,放这个对象的实例
  • 将指针指向堆里对象的地址,完工!
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值