前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

JVM

原创
作者头像
hhss
修改2021-02-16 11:24:08
6140
修改2021-02-16 11:24:08
举报

厚积薄发,打好基础。

重学Java系列之深入理解JVM虚拟机开篇:JVM介绍与知识脉络梳理

重学Java系列之深入理解JVM虚拟机1:JVM内存的结构与永久代的消失

重学Java系列之深入理解JVM虚拟机2:垃圾回收器详解

重学Java系列之深入理解JVM虚拟机3:Java class介绍与解析实践

重学Java系列之深入理解JVM虚拟机4:虚拟机字节码执行引擎

重学Java系列之深入理解JVM虚拟机5:深入理解JVM类加载机制

重学Java系列之深入理解JVM虚拟机6:JNDI,OSGI,Tomcat类加载器实现

重学Java系列之深入理解JVM虚拟机7:Java的编译期优化与运行期优化

重学Java系列之深入理解JVM虚拟机8:JVM监控工具与诊断实践

重学Java系列之深入理解JVM虚拟机9:JVM常用参数以及调优实践

重学Java系列之深入理解JVM虚拟机10:JVM性能管理神器VisualVM介绍与实战

重学Java系列之深入理解JVM虚拟机11:再谈四种引用及GC实践

补充

对于 JVM 方面的知识的巩固与其在网上看一些零零碎碎的文章不如啃一下这本书。《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 2 版)》,在看每一章节的时候看到不懂的要配合网上的文章理解,并且需要看几篇文章理解。

参考大佬面经:https://www.jianshu.com/p/a61f012e84d5

复习整体规划:

运行时数据区域---OOM异常(内存泄漏,内存溢出)---垃圾回收(what?when?how?)---类加载(双亲委派)

what:什么样的对象需要回收?引用计算法、可达性分析算法。

how:考察几个算法:分代收集、标记清除(内存泄漏)、标记整理、复制。

when:minor GC和Full GC什么时候触发?

1. 运行时数据区域(内存模型)(必考)

堆:在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存。(线程共享) 方法区:可认为是年老代;存储运行时常量,存储已经被虚拟机加载的类信息,final常量、静态变量、编译器即时编译的代码等。(线程共享) 虚拟机栈:一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。 局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。(线程私有) 本地方法栈:唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。(线程私有) 程序计数器:当前线程的行号指示器(线程私有) 直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

2. 内存分配机制与垃圾回收机制(必考)

https://blog.csdn.net/u012501054/article/details/84503286 这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String等),然后在栈上分配,在栈上分配的很少见,我们这里不考虑。 Java内存分配和回收机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。 年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。 年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)。 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。 GC机制的基本算法是:分代收集

3. 垃圾回收算法(必考)

引用计数法(redis就是用的这个):无法解决循环依赖的问题,如果几个对象存在循环依赖,那么垃圾收集器就永远不会回收它们。 可达性分析算法 :GCRoots(GCRoots可以简单记忆为,如果被删就一定会影响程序运行的对象,比如有虚拟机栈/本地方法栈中的引用对象,synchronized持有的对象,方法区中的静态对象、常量) https://blog.csdn.net/luzhensmart/article/details/81431212 不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程(标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链) 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

​ 但是我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。 这样就需要用到四个引用中的软引用了。 强引用:会爆出OOM也不会被回收 软引用:在内存不够的时候被回收 弱引用:每次GC都会被回收(ThreadLocal中内部类Map的key就是) 虚引用:可以用来跟踪GC,对象准备被回收时发现他还有虚引用,会把这个虚引用加入一个引用队列,可以观察这个队列中虚引用是否存在来判断对象是否被回收了。 Java垃圾回收器会优先清理可达性强度低的对象(可达性由上面的四种引用决定) === 为什么需要使用软引用? 首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。 这时我们通常会有两种程序实现方式: 一种是: 把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终; 另一种是: 当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。 很显然,第一种实现方法将造成大量的内存浪费. 而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。 我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。

四个垃圾回收算法

-- 垃圾回收器经典算法: 标记清除(内存泄漏) 标记整理 复制(一般不用在老年代,太耗时,且浪费空间) -- GC机制的基本算法是: 分代收集: 新生代:复制算法 老年代:标记清楚、标记整理

4. Minor GC和Full GC触发条件

MinorGC:Eden区满 fullgc:大对象直接 到老年代,老年代空间不足,system.gc,minorgc后发现老年代大剩余空间大小小于平均每次从新生代进入老年代的值 Java GC机制:https://www.cnblogs.com/leeego-123/p/11298267.html JVM 内置的通用垃圾回收原则。堆内存划分为 Eden、Survivor(年轻代) , Tenured/Old (老年代)空间,永久代:对象生成后几乎不灭的对象(例如:加载过的类信息)。新生代和老年代都在java堆,永久代在方法区1.Minor GC 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC 2. Full GC Full GC 是清理整个堆空间—包括年轻代和老年代。 Major GC通常是跟full GC是等价的,收集整个GC堆。 对象优先分配在Eden区,Eden区不够再执行Minor GC。 大对象直接进入老年代,避免Eden和Survivor区之间发生大量内存拷贝。(新生代基本采用复制算法,年老代采用标记清除或标记整理)

5. GC中Stop the world(STW)

什么叫STW:在垃圾回收器进行回收之前,JVM会对内存中的对象进行一次可达性分析,也就是哪些是可回收的,哪些是不可回收的,但是在这个判断的过程中,要求JVM中的对象是不可变,也就是要求一个快照,所以在这个时候就会暂停所有的工作线程,也就是所说的Stop The World。 垃圾回收期什么时候会触发STW: CMS会在找GCroot时和第二次查找时STW ,查找完毕就结束STW开始清理垃圾 G1会在找GCRoot时和第二次查找时STW,需要等垃圾清理完才结束 https://blog.csdn.net/ladymorgana/article/details/82352100 https://www.jianshu.com/p/d686e108d15f GC之前还有STW这一步骤和知道OopMap以及安全点的存在即可。 补充:OOPMap和RememberSet OOP是栈中所存储的是引用的堆中的对象,可以快速枚举GCRoots RememberSet是为了加快新生代的GCRoots,他保存的是老年代中对象引用的新生代对象 此时真正的新生代的GCRoots为 “新生代GCroot+rememberSet里的对象” G1收集器将堆分为各个region,但是难免会有各个region互相引用的情况,所以G1也用到了RememberSet

6. 各垃圾回收器的特点及区别

在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现。 serial:单线程,复制算法,与其他的交互少的交互和上下文切换,快。 parnew:serial的多线程版本,只有这个能配合CMS scanvage:吞吐量优先(用户代码执行时间/用户代码+垃圾回收执行的时间) cms:老年代并发收集器,标记清除算法,停顿时间段,无法清理浮动垃圾,cpu敏感,线程数为(cpu数+3)/4,cpu少的用户效率较低 G1 :强化分区,弱化分代的概念,本质上是复制算法,能预测时间停顿

​ 搞懂CMS和G1:https://www.cnblogs.com/heyonggang/p/11718170.html CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的。 G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。

7. 双亲委派模型

类的加载过程:加载(以上的过程)--验证--准备--解析--初始化 双亲委派模型(一种层次关系):启动类加载器==扩展类加载器==应用程序加载器==自定义加载器 双亲委派模型的工作过程:如果一个类加载器收到加载请求,他首先不会自己加载,交给父加载器,每一层都是如此;最终都送到了启动类加载器,只有父加载器无法加载这个请求,才会让子加载器城市去加载。 双亲委派模型的作用:类先由父加载器加载,如果不能加载再由子类加载。通过类本身与加载器来确定类的唯一性,防止类被重复加载或者被修改核心的api。 Java提供了显示加载类的API:Class.forName(classname);

8. JDBC和双亲委派模型关系

简单来说就是JDK的库里有数据库连接的接口,而具体实现是在各个数据库的jar包中,又因为最高级的那个加载器默认只加载最基础的jar包,所以只能用其他加载器去加载数据库的jar包,此时双亲委派模型已经被破坏(可能说的比较乱,这块我也不太理解,tomcat比较好懂一点) 补充:tomcat与双亲委派模型 tomcat中能加载多个项目,为了防止多个项目不同jar包冲突,就不可能让父加载器都去加载这些,只能用子加载器加载各个项目的jar包。而父级加载器加载tomcat本身所需的jar包来确保安全。 tomcat还实现了jsp的热部署,这个也是通过类加载器实现的 我们都知道jsp本质上是servlet,他被类加载器加载后,如果被修改了,此时类名还是一样,类加载器还是会从方法区直接读取已经存在的“缓存”来加载,这样我们就无法实现热部署了。那么怎么让这个“缓存“失效呢?就是用自己的一个jsp类加载器,每个加载完成之后就卸载掉,每次加载都会去读取最新的。如果此时使用双亲委派的话,需要把父类加载器卸载,tomcat直接挂啦。

9. JVM锁优化和锁膨胀过程

锁消除: 不会发生竞争的情况下JVM会把锁消除 锁粗化: 比如简单for循环内的synchronized会放到for循环外 偏向锁: 对象头MarkWord01,还保存有持有的线程ID,这个MarKWord与无锁状态是一样的,每次线程进来只要比较每次进来用CAS的方式把线程id设置成自己的,然后直接运行即可。 轻量级锁: 对象头MarkWord00,由偏向锁膨胀而来,先通过cas设置线程id,设置失败,说明已经有其他线程拿到偏向锁了,开始膨胀,刚才那个拿到那个偏向锁的线程会在自己栈帧中创建一块区域保存对象的MarkWord信息,然后用CAS指向对象的MarkWord区域。设置成功就相当于获得了轻量级锁 重量级锁: 在轻量级锁CAS多次失败后会膨胀成重量级锁,此时其他线程过来的时候会直接挂起。唤醒需要由内核态转换到用户态,比较耗时。 自旋锁,就是一直尝试获取锁,建立在别的线程获取锁占用时间比较短的认知上。

重点:JVM调优

一、堆设置

-Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2, 一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n:设置持久代大小

二、收集器设置

-XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器

三、垃圾回收统计信息

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename 输出gc日志的参数

四、并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

五、并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

六、减少fullGC

什么是fullGC?

通常意义上而言指的是一次特殊GC的行为描述,这次GC会回收整个堆的内存,包含老年代,新生代,metaspace等。 但是实际情况中,我们主要看的是gc.log日志,其中也会发现在部分gc日志头中也有Full GC字眼,此处表示含义是在这次GC的全过程中,都是STW的状态,也就是说在这次GC的全过程中所有用户线程都是处于暂停的状态。

造成fullGC的可能场景有哪些?

https://blog.csdn.net/wangshuminjava/article/details/80907129

1、System.gc()方法的调用

2、老年代代空间不足

3、永生区空间不足

4、堆中分配很大的对象

5、统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间

6、CMS GC时出现promotion failed和concurrent mode failure

JVM触发Full GC的条件是什么呢?下面先介绍下java一些知识。 JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)。

上图是java运行时数据存储模型,包括栈,堆,方法区等等。程序内存数据一般都在堆中进行管理。Java堆又分为新生代和老年代,新生代可以分为Eden、Survivor。新建的对象会存放到新生代中,当多次垃圾回收后,仍然存活的对象会转移到老年代中。因此对象存活路径为:Eden->Survivor->Old Generation。

当老年代空间不足时,这时就会发生Full GC。明白了JVM内存管理机制,下面就开始进行优化。

如何去排查这个问题?

1)https://www.cnblogs.com/w-y-c-m/p/9919717.html

通过jmap -dump:format=b,file=temp.dump 5280 dump文件,然后下载到本地通过jvisualvm分析对象的引用链的方式来定位具体频繁创建对象的地方,dump文件下载下来有5G多,整个导入过程都花了10多分钟。想查看所占空间较多对象的引用链,直接OOM了,dump对象太大了。这时候就换了种思路,查看占用空间比较大的一系列对象,看能不能找出什么端倪。

发现排第一的chart[]对象里面,存在一些metrics监控的具体指标的相关内容,定位到出问题的代码处解决问题。

2)https://blog.csdn.net/zhangfengaiwuyan/article/details/89380008

既然参数调整没法解决问题,那么只能深层次分析内存中到底是哪些对象占用了如此大的内存。这时,需要使用到几个java内存分析工具:jmap,MAT等。

首先使用jmap工具,导出整个JVM 中内存信息。命令如下:

jmap -dump:format=b,file=文件名 [pid]

由于进程内存比较大,dump过程比较缓慢,耐心等待就好。大概等待20分钟后,终止导出来30G大小的dump文件。由于文件太大,一般jvisualvm分析工具难以加载。经过查阅资料,我是用MAT工具对dump文件进行分析。

./ParseHeapDump.sh m.hprof org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components。

m.hprof就是jvm的dump文件,在mat目录下会生成3份.zip结尾的报告和一些m.相关的文件,将生成的m.hprof相关的文件都下载到windows本地磁盘。由于dump文件比较大,以上过程进行比较缓慢,因此可以放到linux后台运行。以下是生成的报告内容。

在报告中,我发现RingBuff这个实例特别奇怪,只有一个实例,但是占用的空间非常大。为此,这时我开始查阅程序源代码。

通过走读源代码,了解到系统是基于Netty+Disruptor框架编写。Disruptor是一个开源框架,并发性能非常强悍。它使用了一个叫RingBuffer环形队列的数据结构,避免使用同步锁,因此性能非常高。

RingBuffer环形队列里面的数据不会释放,当队列满时会覆盖前面的数据。这样一来,随着系统的运行最终肯定有buffersize大小的数据长驻内存中,不会被垃圾回收器回收。

经过以上分析,我立即查看环境中RingBufferSize配置,果不其然,这个参数被设置的很大:6291456。意味着整个环形队列有6291456个槽位,而RingBuffer里面存放的是Cdr话单对象,这个对象里面有个HashMap,可能包含几十到上百个话单字段。如果一个话单对象有几百个字节,那么整个队列将占有几G甚至几十G的内存。怪不得老年代内存不够用,会频繁发生GC。

综合考虑性能测试目标,我将RingBufferSize设置为2048,经过近1亿数据量的测试,终于再没有出现Full GC的情况

3)https://www.pianshen.com/article/476856477/

一种实际情况:

1)jstat -gc 【pid】查看gc情况;

2)如果发现FullGC过多,通过 jmap -histo 【pid】查看堆中对象统计

问题解决

实际工作中,主要发现两个数据结构相关原因会导致FULL GC

1. LinkedBlockingQueue$Node

当数据量很大时,LinkedBlockingQueue会无限制存放数据,最终导致Allocation Failure的Full GC。由下图可知LinkedBlockingQueue的无参构造函数是一个无界队列,所以需要使用有参构造函数并合理设置数值来限制节点数量。

​。

2.线程池

使用Executors.newFixedThreadPool(nThreads)构造线程池处理消息,结果由于消息量很大,造成内存消耗过快,频繁FULL GC,其本质原因也是队列无限存放数据。

解决方案是构造一个阻塞的容量5000的任务队列,且在队列满的时候执行CalllerRunsPolicy的拒绝策略

代码语言:javascript
复制
new ThreadPoolExecutor(nThreads, nThreads,
                30, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5000),
                new ThreadPoolExecutor.CallerRunsPolicy());

七、美团GC优化技术文章

从实际案例聊聊Java应用的GC优化

疑点

1、四个算法,七个垃圾回收器

2、为什么复制算法适合年轻代;标记清楚,标记整理适合年老代?

3、减少频繁fullGC问题

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 补充
  • 1. 运行时数据区域(内存模型)(必考)
  • 2. 内存分配机制与垃圾回收机制(必考)
  • 3. 垃圾回收算法(必考)
  • 4. Minor GC和Full GC触发条件
  • 5. GC中Stop the world(STW)
  • 6. 各垃圾回收器的特点及区别
  • 7. 双亲委派模型
  • 8. JDBC和双亲委派模型关系
  • 9. JVM锁优化和锁膨胀过程
  • 重点:JVM调优
    • 一、堆设置
      • 二、收集器设置
        • 三、垃圾回收统计信息
          • 四、并行收集器设置
            • 五、并发收集器设置
              • 六、减少fullGC
                • 什么是fullGC?
                • 造成fullGC的可能场景有哪些?
                • 如何去排查这个问题?
            • 疑点
            相关产品与服务
            数据保险箱
            数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档