JVM相关

目录

基础

1 System.gc()

2 强引用、软引用

​3 堆外内存

4 类加载

5 类加载JVM参数调优

6 GCroots

GC算法与调优

1 Minor GC:

2 Major GC:

3 垃圾收集器 

Serial, SerialOld

ParNew

Parallel, Parallel Old

CMS 

G1

收集器搭配推荐

问题

指针碰撞、空闲列表、TLAB是什么关系

Young 区与Old区内存比例划分

System.gc是否要禁用


基础

1 System.gc()

这个方法执行后是立即回收内存吗?答:程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行;因为这个命令只是建议JVM安排GC运行, 还有可能完全被拒绝。 GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化

2 强引用、软引用


3 堆外内存

  • 堆内存完全由JVM负责分配和释放;
  • 使用堆外内存,就是为了能直接分配和释放内存,提高效率。
  • JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。
  •  NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心(这个参数作用是禁止代码中显示调用GC),存在潜在的内存泄露风险
  • 我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。
  • Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。
  • 使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。堆外内存的好处是:(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;(2)理论上能减少GC暂停时间;(3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

4 类加载

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到APP ClassLoaderExtention ClassLoaderBootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是说当发现这个类没有的时候会先去让自己的父类去加载,父类没有再让儿子去加载。面试题:自己写一个String类会被加载嘛?答:不能,因为java.lang.String已经被Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。

  • 双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
  • 为什么使用双亲委派模型:使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 nt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱

5 类加载JVM参数调优

JVM调优总结 -Xms -Xmx -Xmn -Xss

GCroots

常见可作为 GC Root 的对象有:

  • 执行上下文:JVM 栈中参数、局部变量、临时变量等引用的堆对象

  • 全局引用:方法区中类的静态引用、常量引用所指向的对象

GC算法与调优

1 Minor GC:

  • eden区满时,触发MinorGC,即申请一个对象时,发现eden区不够用,则触发一次MinorGC(优化:扩容新生代)
  • 在cms算法中,young gc的实现过程?先找出根对象,如Java栈中引用的对象、静态变量引用的对象和系统词典中引用的对象等待,把这些对象标记成活跃对象,并复制到to区,接着遍历这些活跃对象中引用的对象并标记,找出老年代对象在eden区有引用关系的对象并标记,最后把这些标记的对象复制到to,在复制过程还要判断活跃对象的gc年龄是否已经达到阈值,如果已经达到阈值,就直接晋升到老年代,YGC结束之后把from和to的引用互换。

2 Major GC:

  • CMS在重标记(Remark)阶段,Remark阶段是Stop-The-World,即在执行垃圾回收时,Java应用程序中除了垃圾回收器线程之外其他所有线程都被挂起,意味着在此期间,用户正常工作的线程全部被暂停下来,这是低延时服务不能接受的。新生代对象持有老年代中对象的引用,这种情况称为“跨代引用”。因它的存在,Remark阶段必须扫描整个堆来判断对象是否存活,包括图中灰色的不可达对象。新生代中对象的特点是“朝生夕灭”,这样如果Remark前执行一次Minor GC,大部分对象就会被回收。CMS就采用了这样的方式
  • 触发条件 (1)调用System.gc时,系统建议执行Full GC,但是不必然执行;(2)老年代空间不足;(3)方法区空间(Perm空间)不足;(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存;(5)由Eden区、From区向To区复制时,对象大小>To内存,把该对象转存到老年代,但老年代可用内存<该对象大小; (6)  CMS GC时出现promotion failed和concurrent mode failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止CMS,直接进行Serial Old GC);

3 垃圾收集器 

Serial, SerialOld

原理:内存不足触发 GC 后会暂停所有用户线程,单线程地在新生代中标记复制,在老年代中标记整理,收集完毕后恢复用户线程

优点:全程 STW 简单高效

缺点:STW 时长与堆对象数量成正相关,且 GC 线程只能用到 1 core 无法加速

场景:单核 CPU 且可用内存少(如百兆级),JDK1.3 之前的唯一选择

图片

ParNew

原理:多线程并行版的 Serial 实现,能有效减少 STW 时长;线程数默认与核数相同,可配置

场景:JDK7 之前搭配老年代的 CMS 回收器使用

图片

Parallel, Parallel Old

垃圾回收有两个通常不可兼得的目标

  • 低延迟:STW 时长短,响应快;允许高频、短暂 GC,比如调小新生代空间,加快收集延迟(吞吐下降)

  • 高吞吐量:用户线程耗时 /(用户线程耗时 + GC 线程耗时)高,GC 总时间低;允许低频、单次长时间 GC,(延迟增加)

原理:与 ParNew 类似都是并行回收,主要增加了 3 个选项(倾向于提高吞吐量)

  • -XX:MaxGCPauseTime:控制最大延迟

  • -XX:GCTimeRatio:控制吞吐(默认 99%)

  • -XX:+UseAdaptiveSizePolicy :启用自适应策略,自动调整 Eden 与 2 个 Survivor 区的内存占比-XX:SurvivorRatio,老年代晋升阈值 -XX:PretenureSizeThreshold


CMS 

  • CMS垃圾回收器设计目的:为了避免「老年代 GC」出现「长时间」的卡顿(Stop The World)
  • CMS垃圾回收器回收过程:初始标记、并发标记、并发预处理、重新标记和并发清除。初始标记以及重新标记这两个阶段会Stop The World
  • CMS垃圾回收器的弊端:1、会产生内存碎片&&需要空间预留(CMS可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用);2、停顿时间是不可预知的
  • 如果CMS运行过程中预留的空间不够用了,会报错(Concurrent Mode Failur e),这时会启动 Serial Old垃圾收集器进行老年代的垃圾回收,会导致停顿的时间很长

G1

  • 将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合

  • 可以设置一个STW的时间(-XX:MaxGCPauseMillis设置G1收集过程目标停顿时间,默认值200ms)

  • 分区,分小区后,对这些小区回收就会比较容易控制收集的时间

图片

G1之前回收器
堆内存划分方式多个等大的 region, 各 region 分代角色并不固定,按需在 Eden, Survivor, Old 间切换固定大小、固定数量的分代区域
回收目标回收价值高的 region 动态组成的回收集合新生代、整个堆内存

  • 跨代引用:各 region 除了用卡表标记各卡页是否为 dirty 之外,还用哈希表记录了各卡页正在被哪些 region 引用,通过这种“双向指针”机制,能直接找到 Old 区,避免了全量扫描(G1 自身内存开销大头)
  • 一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:H-obj直接分配到了old gen,防止了反复拷贝移动。H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

收集器搭配推荐

  • parallel Scavenge + parallel old:注重吞吐量的应用

  • PerNew + CMS:并发收集、低停顿注重停顿时间的应用,强交互环境,但CMS不可预测停顿时间,所以才有G1

  • G1:基本不用配置,低停顿,用于大容量的堆。但他牺牲了应用程序的吞吐量和部分堆空间

  • 所以当你觉得配置 CMS 太难了,或你的堆在 2 G 以上,或你想要显式的指定停顿时间那么你可以使用 G1,否则使用 CMS

问题

指针碰撞、空闲列表、TLAB是什么关系

原文:https://www.zhihu.com/question/476948066/answer/2036463259

 

 简单的实现是将释放的对象内存加入 FreeList,下次分配对象的时候,优先从 FreeList 中寻找合适的内存大小进行分配,之后再在主内存中撞针分配。这样虽然一定程度上解决了问题,但是目前大多数应用是多线程的,所以内存分配是多线程的,都从主内存中分配,CAS 更新重试过于频繁导致效率低下。目前的应用,一般根据不同业务区分了不同的线程池,在这种情况下,一般每个线程分配内存的特性是比较稳定的。这里的比较稳定指的是,每次分配对象的大小,每轮 GC 分配区间内的分配对象的个数以及总大小。所以,我们可以考虑每个线程分配内存后,就将这块内存保留起来,用于下次分配,这样就不用每次从主内存中分配了。如果能估算每轮 GC 内每个线程使用的内存大小,则可以提前分配好内存给线程,这样就更能提高分配效率。这种内存分配的实现方式,在 JVM 中就是 TLAB (Thread Local Allocate Buffer)。

Young 区与Old区内存比例划分

  • IO 交互型:互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好。

  • MEM 计算型:主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。

  • 如果是 Young/Eden 区过小,我们可以在总的 Heap 内存不变的情况下适当增大 Young 区,具体怎么增加?一般情况下 Old 的大小应当为活跃对象(可以观察监控每次老年代gc后剩余的对象占用的大小)的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区

System.gc是否要禁用

  • 保留 System.gc:此处补充一个知识点,CMS GC 共分为 Background 和 Foreground 两种模式,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但 Foreground Collector 却有很大的差异,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 一样的 LISP2 算法,其使用 Mark-Compact 来做 Full GC,一般称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的 Young 区和 Old 区以及 MetaSpace。由上面的算法章节中我们知道 compact 的代价是巨大的,那么使用 Foreground Collector 时将会带来非常长的 STW。如果在应用程序中 System.gc 被频繁调用,那就非常危险了。
  • 去掉 System.gc:如果禁用掉的话就会带来另外一个内存泄漏问题,此时就需要说一下 DirectByteBuffer,它有着零拷贝等特点,被 Netty 等各种 NIO 框架使用,会使用到堆外内存。堆内存由 JVM 自己管理,堆外内存必须要手动释放,DirectByteBuffer 没有 Finalizer,它的 Native Memory 的清理工作是通过 sun.misc.Cleaner 自动完成的,是一种基于 PhantomReference 的清理工具,比普通的 Finalizer 轻量些。为 DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory,下面为代码实现:HotSpot VM 只会在 Old GC 的时候才会对 Old 中的对象做 Reference Processing,而在 Young GC 时只会对 Young 里的对象做 Reference Processing。Young 中的 DirectByteBuffer 对象会在 Young GC 时被处理,也就是说,做 CMS GC 的话会对 Old 做 Reference Processing,进而能触发 Cleaner 对已死的 DirectByteBuffer 对象做清理工作。但如果很长一段时间里没做过 GC 或者只做了 Young GC 的话则不会在 Old 触发 Cleaner 的工作,那么就可能让本来已经死亡,但已经晋升到 Old 的 DirectByteBuffer 关联的 Native Memory 得不到及时释放。这几个实现特征使得依赖于 System.gc 触发 GC 来保证 DirectByteMemory 的清理工作能及时完成。如果打开了 -XX:+DisableExplicitGC,清理工作就可能得不到及时完成,于是就有发生 Direct Memory 的 OOM。

CMS 发生收集器退化

主要有以下几种情况

  •   晋升失败(Promotion Failed):顾名思义,晋升失败就是指在进行 Young GC 时,Survivor 放不下,对象只能放入 Old,但此时 Old 也放不下。直觉上乍一看这种情况可能会经常发生,但其实因为有 concurrentMarkSweepThread 和担保机制的存在,发生的条件是很苛刻的,除非是短时间将 Old 区的剩余空间迅速填满,例如上文中说的动态年龄判断导致的过早晋升(见下文的增量收集担保失败)。
  •   内存碎片:另外还有一种情况就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。

CMS减少收集器退化策略:

  •   内存碎片:通过配置 -XX:UseCMSCompactAtFullCollection=true 来控制 Full GC的过程中是否进行空间的整理(默认开启,注意是Full GC,不是普通CMS GC),以及 -XX: CMSFullGCsBeforeCompaction=n 来控制多少次 Full GC 后进行一次压缩。
  •   增量收集:降低触发 CMS GC 的阈值,即参数 -XX:CMSInitiatingOccupancyFraction 的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减少 Old 区空间的使用大小,另外需要使用 -XX:+UseCMSInitiatingOccupancyOnly 来配合使用,不然 JVM 仅在第一次使用设定值,后续则自动调整。
  •   浮动垃圾:视情况控制每次晋升对象的大小,或者缩短每次 CMS GC 的时间,必要时可调节 NewRatio 的值。另外就是使用 -XX:+CMSScavengeBeforeRemark 在过程中提前触发一次 Young GC,防止后续晋升过多对象。

G1收集器的优点

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片(cms会产生,但其实cms也可配置多少次fullgc后进行一次整理)。

  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值