
JVM底层原理与应用
文章平均质量分 93
JVM底层原理与应用
东阳马生架构
回归初心,保持好奇心,享受那些"成功解题"的喜悦~
展开
-
G1原理—10.如何优化G1中的FGC
这么大量的消息,不会直接全量推送,而是生成推送消息,发送到MQ,然后通过一个消费者去消费这些消息,慢慢把千万级别的消息推送给用户。弥留空间,就是为了提升FGC的效率而设计的。假如因为垃圾回收的速率跟不上系统产生垃圾的速度而造成频繁的FGC,那么就可以适当调低这个参数,尽快开启MGC,通过提升MGC的频率来避免JVM内堆积过多的垃圾对象。调小该参数可以提升并发标记的次数,让并发标记触发的更加频繁一点,从而让重新标记更短一点,以此来提升垃圾回收的速率,避免垃圾回收速度跟不上垃圾产生速度,最终造成FGC。原创 2025-01-17 23:37:07 · 948 阅读 · 0 评论 -
G1原理—9.如何优化G1中的MGC
检查了JVM的GC日志,分析dump文件发现有大量的大对象,并且是缓存补偿操作下的Job持有的集合占用了大量的内存。另外一个缓存更新的业务点是:有很多商品是批量上架的,尤其是自营商品,会直接批量上架一批商品。这里要补充一个细节:就是更新商品的缓存,是按照店铺来加锁的,因为这样做不至于在Redis中产生大量的分布式锁。那就是因为这个缓存同步服务里的一些更新操作,获取到了锁,去更新这个商铺的商品缓存,然而更新的时间比较长,导致商品系统查询数据库 -> 更新缓存这个操作被阻塞了,最终导致接口请求时间非常长。原创 2025-01-16 23:38:12 · 986 阅读 · 0 评论 -
G1原理—8.如何优化G1中的YGC
直到有一次特殊的交易窗口,因为这次交易窗口涉及到的商家比较多,活动也比较多,达到了5w日活。打开大盘的人数大大增加,导致产生了8K+的QPS,订单查询的QPS也接近了8000左右。因为报表系统也是集群化部署,单台也就3K-5K之间的QPS,所以整体的性能是完全够用的。这样的设置能够直接让系统在启动初期就能达到一个比较高的QPS,即大量的普通查询请求都能直接进入TLAB分配,且允许TLAB自动调整。PLAB和TLAB有同样的效果,PLAB也是用来提升对象分配的效率的,只不过产生的时机不同。原创 2025-01-15 21:57:35 · 900 阅读 · 0 评论 -
G1原理—7.G1的GC日志分析解读
因为对象如果超过了Region大小,是一定会被当作大对象单独存储的,所以要把这个对象的大小设置的合理一点。因为对象如果超过了Region大小一半,是一定会被当作大对象单独存储的,所以要把这个对象的大小设置的合理一点。注意:refills的次数就是填充TLAB的次数,可理解为申请新的TLAB的次数。从多次GC的结果来看,新生代GC的情况相对比较稳定,每次的时长都在1ms左右(除了第一次时间比较长),并且由于我们的代码是没有对象置为null的操作的,所以基本上所有的对象都能存活下来晋升到老年代。原创 2025-01-14 22:24:10 · 1348 阅读 · 0 评论 -
G1原理—6.G1垃圾回收过程之Full GC
如下图示,GC Roots引用了Obj这个对象,而Obj3这个对象对Obj0的引用又记录在这个Region对应的RSet中,所以不需要找到Obj3这个GC Roots就可对Obj0做好标记,判断是否存活。当G1在任何一个过程发现一个对象的指针指向自己,就可以认为它是需要恢复对象头的,然后G1就可以去恢复这个对象的对象头。此时2K的这个对象想要尝试放到第一个Region里肯定是不会成功的,所以只能进入第二个Region中,并且这个2K的对象的起始位置,就是第二个Region内存的开始地址。原创 2025-01-13 21:05:28 · 1042 阅读 · 0 评论 -
G1原理—5.G1垃圾回收过程之Mixed GC
在YGC中,当发现需要开启并发标记,当记录下被GC Roots直接引用的老年代对象后,此时除了可以对引用了老年代对象的GC Roots做一些特殊处理之外,还可以把Survivor对象 + GC Roots引用的老年代对象 + 老年代RSet,作为Mixed GC时执行并发标记的起始点。由于YGC会找出GC Roots引用的新生代对象以及RSet引用的新生代对象,所以YGC后所有被GC Roots直接引用 + 间接引用的存活对象都在S区了,并且老年代引用的新生代存活对象也全部在S区了。如果大于,则继续进行;原创 2025-01-12 22:24:42 · 1133 阅读 · 0 评论 -
G1原理—4.G1垃圾回收的过程之Young GC
此时YGC线程最多就是进行少部分的收尾工作,但现在YGC线程还需要大量的时间去处理DCQ消息,那么就说明:要么这几个DCQS的阈值设置得过大了、要么Refine线程太少了。那么老年代引用的新生代对象所在Region的RSet此时还没有修改,因此需要把这个旧的RSet进行清理,然后建立一个新生代对象所在的新Region的RSet。比如,在新生代GC后,会有存活对象进入老年代。如果新生代空间足够大,加上G1会自己动态调整新生代分区的数量,那么就是YGC太频繁导致YGC的时间占程序运行时间比例超过10%。原创 2025-01-11 19:45:32 · 1339 阅读 · 0 评论 -
G1原理—3.G1是如何提升垃圾回收效率
比如发现老年代有一块512字节的空间里的对象引用了新生代的一个对象,那么记忆集Rset只需要直接记录这个512字节的空间在卡表里的位置即可。一旦发现老年代的对象引用了一个新生代的Region中的对象,那么G1就会在这个新生代的Region的记忆集中维护一个key-value对。但是新的问题来了:此时一个位图占用的内存过大了,例如一个Region的大小是1M。由于新生代和老年代GC的阶段是不同的,当只需要回收新生代时,如果还是按照可达性分析算法,那么就会把老年代也都全标记一遍,而此时并不回收老年代。原创 2025-01-10 21:24:03 · 1190 阅读 · 0 评论 -
G1原理—2.G1是如何提升分配对象效率
如果对象在线程1的TLAB分配,压缩后出现在线程2的TLAB里面,那此时该对象应该由谁管理,所以压缩肯定是不合理的。由于TLAB是一个很小的空间,而且对象的分配是按照连续内存来分配的,所以可以直接遍历整个TLAB,然后找到第一个没有被使用的内存位置。进行堆加锁分配一个新的TLAB时:如果堆加锁分配一个新TLAB成功,就在Region上分配一个新的TLAB(堆加锁分配TLAB成功)。因为TLAB大小分配好后,其大小就固定了,而对象的大小却是不规则的,所以很有可能会出现对象放不进TLAB的情况。原创 2025-01-09 23:07:51 · 775 阅读 · 0 评论 -
G1原理—1.G1回收器的分区机制
假如堆内存最大最设置为Xms = 128G,最小值设置为Xmx = 128G,则RegionSize = max((32G + 128G) / 2 / 2048, 1M) = 32M,并且由于G1垃圾回收器会自动计算分区个数,所以分区个数的范围在32G / 32M = 1024 ~ 128G / 32M = 4096之间。假设设置xms = 32G,xmx = 128G,则每个Region分区的大小为32M,Region分区个数动态变化范围从1024到4096个。所以直接简单粗暴的求平均是不合适的。原创 2025-01-08 21:55:55 · 1060 阅读 · 0 评论 -
JVM实战—13.OOM的生产案例
接着就可以向Jetty监听的9090端口发送请求,Jetty会把请求转交给我们用的SpringMVC之类的框架,而SpringMVC之类的框架再去调用我们写好的Controller之类的代码。接着开始分析生产现场的内存快照,在分析MAT时发现了一个问题:因为有大量的XXClass.process()方法递归执行,每个XXClass.process()方法中都创建了大量的char数组,最后因为XXClass.process()方法又多次递归调用,也就导致了大量的char[]数组耗尽了内存。原创 2025-01-07 22:20:21 · 1271 阅读 · 0 评论 -
JVM实战—12.OOM的定位和解决
接着又是一次FGC。其实栈内存溢出跟堆内存是没有关系的,因为它的本质是一个线程的栈中压入了过多调用方法的栈桢。如果有监控平台,就可以接入系统异常的监控和报警,可以设置当系统出现OOM异常,就发送报警给对应的开发人员。比如可以让JVM在OOM时dump一份内存快照,事后只要分析这个内存快照,就可以知道是哪些对象导致OOM的了。通过异常信息可以直接定位出是Metaspace区域发生异常,然后分析GC日志就可以知道Metaspace发生溢出的全过程,接着再使用MAT分析内存快照,就知道是哪个类太多导致异常。原创 2025-01-06 19:28:34 · 1182 阅读 · 0 评论 -
JVM实战—11.OOM的原因和模拟以及案例
通常会设置每个线程的栈内存就是1M,假设一个JVM进程内一共有1000个线程,这些线程包括:JVM的后台线程 + 系统依赖的第三方组件的后台线程 + 系统核心工作线。每次Eden区占满后,大量存活的对象必须转入老年代,而且老年代里的这些对象还无法释放,最终老年代一定会被占满。既然动态生成的类是Car的子类,那么该类也有Car的run()方法,于是通过如下代码对动态生成的类的run()方法进行改动。与此同时,数据计算系统还在不停加载数据到内存里计算,这必然会导致内存里的数据越来越多。原创 2025-01-05 21:23:10 · 1071 阅读 · 0 评论 -
JVM实战—10.MAT的使用和JVM优化总结
在JDK 1.6时:"Hello World"这个字符串底层是基于一个数组来存放那些字符的,比如[H,e,l,l,o,,W,o,r,l,d]这样的数组,然后切割出来的"Hello"字符串它不会对应一个新的数组,而是直接映射到原来那个字符串的数组,采用偏移量表明自己是对应原始数组中的哪些元素,比如"Hello"可能对应[H,e,l,l,o,W,o,r,l,d]数组中0~4位置的元素。在接下来的一个步骤,务必要注意:如果是线上导出来的dump内存快照,很多时候可能都是几个G的。原创 2025-01-04 20:30:01 · 1520 阅读 · 0 评论 -
JVM实战—9.线上FGC的几种案例
比如老年代有2G内存,其中1.5G是连续可用的,0.5G是内存碎片。这些类就是类似GeneratedSerializationConstructorAccessor的类,这样下次再执行反射时,就可以直接调用这些类的方法,这属于JVM底层的一个优化机制。因为-XX:SoftRefLRUPolicyMSPerMB=0,导致YGC时回收了调用反射时JVM创建的大部分软引用对象(在堆中),导致下一次调用反射又继续创建类和Class,而Class被放在元空间,从而导致元空间很快满了,于是就触发FGC。原创 2025-01-03 23:35:37 · 1424 阅读 · 0 评论 -
JVM实战—8.如何分析jstat统计来定位GC
比如:Eden区的总容量、已经使用的容量、剩余的空间容量,两个Survivor区的总容量、已经使用的容量、剩余的空间容量,老年代的总容量、已经使用的容量、剩余的容量。所以YGC其实是很快的,即使回收800M的对象,也就10毫秒左右。因此通过这个模拟程序的运行,我们可以使用jstat分析出以下信息的:新生代对象增长的速率、YGC的触发频率、YGC的耗时、每次YGC后有多少对象是存活的、每次YGC后有多少对象进入了老年代、老年代对象增长的速率、FGC的触发频率、FGC的耗时。原创 2025-01-02 23:27:52 · 1078 阅读 · 0 评论 -
JVM实战—7.如何模拟GC场景并阅读GC日志
这行代码一旦运行,就会在JVM的Eden区内放入一个1M的对象,同时在main线程的虚拟机栈中会压入一个main()方法的栈帧。这时Eden区明显已经不能放下这个数组了,因为Eden区总共4M,里面已经放入3个1M的数组,剩余空间只有1M。因为这次GC时,会回收掉上图中的2个2M的数组和1个128K的数组,然后留下一个2M的数组和1个未知的500K的对象作为存活对象。此时希望在Eden区再次分配一个2M的数组,由于此时Eden区里已有3个2M数组和1个128K数组,大小都超过6M了。原创 2025-01-01 22:41:27 · 1257 阅读 · 0 评论 -
JVM实战—6.频繁YGC和频繁FGC的后果
老年代被占满后就会触发老年代的GC,也会把这种GC也称为Full GC,但有人会觉得老年代的GC不能叫Full GC。所以如果10多秒就触发一次YGC,导致的后果就是:可能可以回收掉的垃圾也就几百M,有1G的对象可能都是无法回收的。于是就会导致每隔10多秒,就有1G对象进入老年代,而老年代也就1G。那么G1就会基于它的Region内存划分原理,在运行一段时间后,就只针对比如2G内存的Region进行垃圾回收,此时只需停顿20ms,然后回收掉2G内存空间,腾出内存后,接着继续让系统运行。原创 2024-12-31 22:57:11 · 1540 阅读 · 0 评论 -
JVM实战—5.G1垃圾回收器的原理和调优
比如2048个Region中有1200个Region都是属于新生代的了,里面的Eden占了1000个Region,每个Survivor占了100个Region,而且Eden中的Region都占满了对象。于是这些存活下来的对象就会全部进入老年代,或者存活下来的对象比较多,达到S区的50%,触发动态年龄判定规则,那么也会导致下一次新生代GC的存活对象快速进入老年代。我们应该要合理分析系统的内存压力,然后合理优化JVM的参数,尽可能降低JVM GC的频率,同时降低JVM GC导致的系统停顿的时间。原创 2024-12-30 23:49:59 · 1415 阅读 · 0 评论 -
JVM实战—4.JVM垃圾回收器的原理和调优
因为对这个参数的考虑必须结合系统的运行模型,如果躲过15次GC都经过几分钟了,也就是一个对象几分钟都不能回收,说明这个对象肯定是要长期存活的核心组件(使用了类似@Service注解),那么这个对象就应该进入老年代。最多有经验的工程师在系统上线前,通过前面案例介绍的方法:估算一下系统的内存压力以及垃圾回收的运行模型,然后合理设置一下内存各个区域大小,尽量避免太多对象进入到老年代。上图的垃圾对象(新的)就是在并发清理期间,先被系统分配在新生代,然后触发一次YGC,一些对象进入了老年代,短时间内又没被引用了。原创 2024-12-29 21:53:28 · 1518 阅读 · 0 评论 -
JVM实战—3.JVM垃圾回收的算法和全流程
在新生代的内存区域会回收大量垃圾对象,保留一些被引用的存活对象。但由于内存碎片太多,虽然所有的内存碎片加起来有很大的一块内存,但因这些内存都是分割的,所以导致没有完整的内存空间来分配新对象。而ParNew垃圾回收器针对新生代采用的是复制算法来进行垃圾回收,这时垃圾回收器会先把Eden区中的存活对象标记出来,全部转移到S1区,再一次性清空Eden区中的垃圾对象。复制算法的缺点其实非常的明显:假设给新生代1G的内存空间,那么只有512M的内存空间是可以用的,另外512M的内存空间是一直要放在那里空着的。原创 2024-12-27 23:07:28 · 1246 阅读 · 0 评论 -
JVM实战—2.JVM内存设置与对象分配流转
假设一台机器每秒创建100个500字节的支付订单对象,扩大20倍后,那么每秒创建出的被栈内存的局部变量引用的对象,大概占1M内存空间。但上述仅仅是针对一个支付订单对象来分析的,实际上如果扩大20倍来对完整支付系统的预估后:一台机器每秒处理100个订单,每秒就会占据1M左右的内存空间。然后堆内存还分为新生代和老年代,老年代需要放置系统的一些长期存活的对象,也要占几百M的内存空间,那么这样下来新生代可能只剩下几百M的内存了。如果准备上线一个新系统,如何根据这个系统预估的业务量和访问量,去推算系统每秒的并发量。原创 2024-12-26 23:09:31 · 942 阅读 · 0 评论 -
JVM实战—1.Java代码的运行原理
比如要加载一个"lib"目录下的类,根据双亲委派机制loadClass()代码:应用程序类加载器初次加载后,它的父类加载器也完成对该类的加载,以后其他应用程序类加载器再次加载该类,就可通过父类加载器加载。这样做的一个显而易见的好处就是:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,可保证例如Object类在程序的各种类加载器环境中都是同一个类。当我们的应用程序类加载器需要加载一个类的时候,它会委派给自己的父类加载器去加载,最终传导到启动类加载器去加载。原创 2024-12-25 23:13:21 · 732 阅读 · 0 评论 -
JVM简介—3.JVM的执行子系统
控制转移指令之条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge等。对于任意一个类,都必须由加载它的类加载器和这个类本身,一同确立其在Java虚拟机中的唯一性。这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,这种行为已经违背了双亲委派模型的一般性原则。原创 2024-12-24 21:44:34 · 1184 阅读 · 0 评论 -
JVM简介—2.垃圾回收器和内存分配策略
当这个参数打开后,就不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。因为不移动对象虽然会使得收集器的效率提升了,但因内存分配和访问相比垃圾收集频率高得多,这部分的耗时增加后,总吞吐量仍然是下降的。原创 2024-12-23 20:53:00 · 1384 阅读 · 0 评论 -
JVM简介—1.Java内存区域
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法进行指针碰撞了,此时虚拟机就必须要维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。如果Java堆中的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放另一边,并且中间放一个指针作为已用内存和空闲内存的分界点的指示器,分配内存时就把该指针向空闲空间那边移动一段与对象大小相等的距离,这种分配方式称为指针碰撞。原创 2024-12-22 23:30:07 · 1151 阅读 · 0 评论