很抱歉好久没更新博文了,最近想着好好再把JVM的知识系统性的归纳及整理一下,于是有了这篇。总结一下:JVM调优看起来高大上,但只是Java程序优化的最后一颗子弹,学习JVM的重点在其内存模型及GC算法及种类中,这样在遇到各类Java程序中有更加深刻的见解及了解
JVisualVM工具的使用
JVM及GC的结构原理与调优
JVM可以看做抽象的计算机,编译器将Java文件编译为Java.class(字节码)文件,JVM对字节码文件进行解释。
JVM和Java没有必然的联系,它只与class文件格式关联,任何语言只要能编译成符合规范的字节码文件,都能被JVM运行。JVM是跨语言的平台
JVM常见的实现
我们平时常说的JVM通常是Java虚拟机的具体实现。例如最知名的hotspot。
HotSport VM
最初由Sun研发,后来Oracle收购后在JDK8时期(2014),HotSport移除了永久代。
BEA JRockit / IBM J9 VM
与之前的HotSport合称三大商用虚拟机,分别由BEA及IBM研发。很多大公司的JVM是自研的,他们通过向Oracle购买版权,或基于OpenJDK改进来(如Alibaba,Twitter等)
JDK&JRE&JVM
JDK
JDK包含JRE,JDK是提供给Java开发者使用的
常用的如jar.exe用来打包,javac.exe用来编译等工具
JRE
JRE包含一些主要的lib,JVM组件及其他组件,用于运行JavaApplication等
JVM
JVM可以理解为一个虚拟出来的计算机,具备着计算机的基本运算方式,将Java字节码文件运行
JVM的内存
JVM定义了若干种程序运行时的数据区
JVM将会包括以下几个运行时的数据区域
程序计数器
计数器也被称为PC寄存器,占据一块较小的内存空间
它可以看做线程所执行的字节码的行号指示器,JVM的概念模型中,字节码解释器工作就是通过改变这个计数器的值来获取下一个需要执行的字节码指令,他是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等功能,都需要使用这个计数器来实现及完成。
每一个线程都有自己的程序计数器。在任意时刻,一个JVM线程只会执行一个方法的代码,这个被线程执行的方法被称为当前方法
若这个方法不是本地的,那寄存器就会将保存JVM正在执行的字节码指令的地址,如果这个方法是本地的,寄存器会将该值保存为undefined
JVM栈
JVM栈与计数器同样也为线程私有,栈的生命周期与线程相同,栈描述的是Java方法执行的线程内存模型,即它存储着有关对象在堆中的地址及信息。每个方法被执行的时候,JVM都会同步创建一个栈帧
用于存储局部变量,方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在JVM栈中从入栈到出栈的过程
把Java内存区域划分可以粗略地划分为堆内存和栈内存,这个栈就是JVM栈,在更多情况下只是JVM栈中局部变量表的部分。
局部变量表
存放了JVM中基本的基本数据类型,对象引用或对象指针,也可能是指向一个代表独享的句柄或者其他与此对象相关的位置,或是return Value的类型(指向另一个方法字节码指令的地址)。
JVM栈可能出现以下异常
- 如果线程请求分配的栈内存超过JVM栈允许的最大容量时,JVM会抛出一个栈溢出异常(Stack Overflow)
- 若JVM栈可以进行动态扩展,但是无法再申请到更大的内存去创建,则JVM会抛出一个OOM异常
本地方法栈
本地方法栈(Native Method Stacks)与JVM栈所发挥的作用是类似的,其区别是JVM栈为JVM执行method服务,而NMS是为虚拟机使用到的本地方法服务
NMS可能出现的异常
- 同JVM栈一样栈溢出异常
- 同JVM栈一样OOM异常
Java堆
Java堆(Java Heap)是JVM内存最大的,JVM堆是被所有线程所共享的,JVM启动时候创建。此区域内的唯一目的是存放对象实例,Java中几乎所有对象在这里分配内存。
堆也是GC的内存管理区域,因此也被称为GC堆。从GC角度来看,现在大部分GC使用过基于分代收集理论来设计的,所以在JVM堆中经常会出现新生代
,老年代
,永久代
,Eden空间
,From Surivor空间
等名词,需要注意的是这些区域划分只是部分GC的设计风格,而非JVM中的具体内存划分,更非JVM规范中的划分。
无论从什么角度,无论如何划分,都不会改变堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将J堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
方法区
与堆一样,是一个线程共享的内存区域,它主要用于JVM加载的类型信息,常量,静态变量,编译器后代码缓存。JVM规范中常常把方法区描述为堆的逻辑部分,但方法区常有一个别名为非堆(Non-Heap)
JVM规范对方法区相对宽松,甚至GC在这里也很少进行垃圾回收,尽管进入方法区不等于永久代。
若方法区无法分配新的内存需求时,会抛出OOM异常
请注意:不是任何JVM拥有永久代
运行时常量池
方法区的一部分,用于存放类的版本,方法,接口,字段,符号引用等。
JVM规范对于运行时常量池并没有做任何细节要求,运行时常量池最主要的一个特征既是具备动态性。即并非只存储编译时的常量,也可通过将Class文件预置,来将Class文件中的常量存储在常量池中,最常用的就是String类的intern()方法。
自然若内存空间已满,且无法申请到新的内存空间,常量池会抛出OOM异常。
直接内存
并不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域。
JDK1.4中加入了NIO类,同时引入了关于基于Buffer及通道的I/O方式,它可以使用本地存储库直接分配堆外内存,避免了Java堆和本地堆来回复制数据导致的性能损耗及内存开销。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OOM异常。
JDK内存区域的变迁
jdk1.6/1.7/1.8内存区域的变化
我们这里就简述HotSpot的内存区域的变迁
-
JDK1.6时期
-
JDK1.7
-
JDK1.8时,彻底干掉了方法区,将直接内存中划出一块区域作为元空间,常量池全部移到元空间
为什么替换掉方法区
- 方法区容易出现内存溢出问题
- 极少数方法在方法区中会在不同JVM中出现不同的结果及表现
- Oracle收购BEA后将,BEAJVM中的特性移植到Hotspot时,对两者JVM的方法去实现面临诸多困难
探究JVM对象
单纯从Java对象层面,新建一个对象,可以通过new,反射,复制,反序列化等等来实现,我们这次来探究一下JVM中对象及对象的创建过程
new在JVM
JVM每遇到一个new
指令时
-
检查该指令的参数是否能在常量池中定义到一个类的引用
-
检查该引用类是否被加载,解析,初始化过。如果没有,则通过类加载器加载对应引用类
-
类加载器检查通过后,接下来JVM会为该新生对象分配对应的内存
内存分配有两种方式
- 指针碰撞:假设堆内存是绝对规整的,所有被使用过的内存被放在一边,没有使用的在另一边,中间放置一个分界点指针,那所谓分配内存就是将那个指针根据新分配内存的大下向未使用内存划分对应的距离及内存位置
- 空闲列表: 堆内存不为规整的,已被使用的和未使用的交错在内存空间中,那就无法使用简单的指针碰撞了,JVM必须维护一个表用来记录标记其中堆内存的空闲区域,分配对象时修改对应的列表,这种分配被称为空闲列表
请注意堆的规整是由不同的GC来决定的
-
内存分配完毕后,JVM将分配的对应对象空间初始化为0,当然这里不包括对象头
-
最后根据对象头来设置该对象内存的实例信息,元数据信息,对象的HashCode,GC的等级,等
请注意以上JVM通过new关键字创建对象时候,会出现线程安全问题
解决方案有两种:
- 一种是对分配内存空间进行线程信息同步
- 将分配的内存划分为共享内存的上一次共享区,或被称为线程共享区,也可以称为一二级缓存
根据上部分内容,从JVM来看,new对象已经被完成了,但是从Java程序上,还会执行对应的构造函数及成员变量的属性和值的初始化,这样看,才可以说一个对象被new
出来了
对象的内存布局
在HotSpot中,独享可以在堆内存中的存储布局可以划分为三个部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
这里就放两个图
需要注意的是如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
对象的访问和定位
在JVM中对对象的访问经常有两种方式,一种为句柄,一种为直接指针
一般若要使用句柄来访问对象,则在JVM堆中会划分一部分内存来存储句柄,也被称为句柄池,通过访问句柄池,我们可以找到对应的对象类型数据及对象实例数据
若要使用直接指针的话,他的流程首先是访问该对象的实例数据,实例数据中存在着到对象的类型的直接指针。
两种方式的各个优点:
- 句柄池访问,是在GC经常移动对象,这样只需要修改句柄池中的对应地址即可
- 直接指针减少了上下文切换导致的时间开销
在HotSpot中对象的访问使用的是直接指针
GC
GC即所谓的垃圾回收,简单说就是要干三件事。
- 哪些内存需要回收
- 什么时候回收
- 如何回收
在Java的内存区域中
- 寄存器,JVM,本地方法栈三个区域随线程创建而创建,随线程死亡而消失。栈帧随着方法的执行,有条不紊的执行着入栈出栈的操作,所以在这几个区域势必会由各类线程决定其内存空间的占用,线程结束,内存自然释放
- 堆和方法区中有着极强的不确定性:一个接口的多个实现类需要的内存不具有一致性,一个方法执行的不同条件分支所需的内存也不同,只有处于运行期间,我们才知道程序究竟会创建多少个对象,至于创建后的对象,其内存空间大小不可预测且是动态的,所以这两部分区域正是GC的重点关照区域
回收对象判定
- 引用计数算法
为每一个需GC管理的对象中添加一个计数器,每当其他对象或方法对其进行引用的时候,该计数器进行加值,若引用失效,则计数器减一。任何时候该对象计数器为0时,该对象将会被GC回收且不可再被使用
该判定方法需要支付额外的内存空间及计算时间,当然其最大的问题就是无法解决循环使用问题
- 可达分析算法
目前JVM主流的GC采取的算法,该算法将一系列GC主节点作为其初始存活对象合集(GC Root Set),从节点出发,去嗅探GC Root Set存活对象中的引用对象,并将其加入到GC Root Set中,我们成该过程为标记(mark),所有未被探测到对象均是死亡且需要回收的。
Java中的引用
通过以上两种算法,我们需要明白GC回收对象基本上都需要使用对象引用来进行判断
Java引用中有四种
- 强引用
- 软引用
- 弱引用
- 虚引用
强引用为最传统的引用,指在程序代码中普遍存在的引用赋值,基本上与存活对象确立强引用关系,则GC不会对其进行回收。
Object obj = new Object();
软引用是指一些还有用但非必须的对象,在系统发生OOM之前,会把这些对象进行GC,如果GC后,还没有足够的内存空间,则会抛出OOM异常
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
// 强引用对象滞空,保留软引用
obj = null;
弱引用用来描述非必须对象,但其强度比软引用更弱一些,一般在下次GC时候,会将该引用对象进行GC,无论是否在OOM之前。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
WeakReference reference = new WeakReference(obj, queue);
// 强引用对象滞空,保留软引用
obj = null;
虚引用也被称为“幽灵引用”,最弱的引用关系。一个对象是否有虚引用,完全不会对其生存时间构成影响,同时也无法通过虚引用来获取一个对象实例,设置虚引用的目的性就是使该对象被GC时候,获得一个系统通知
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(obj, queue);
// 强引用对象滞空,保留软引用
obj = null;
不可达 != 死亡
一个简单的比喻既是死囚也有可能被伸冤。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果对象在在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它就”逃过一劫“;但是如果没有抓住这个机会,那么对象就真的要被回收了。
垃圾收集理论及算法
分代收集理论
其建立在两个分代假说上:
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
用通俗的话总结:大部分污渍很容易擦干净,多次擦都没擦干净的无责越来越难擦干净。
基于这个理论,收集器将Java堆划分出不同的区域,然后将回收对象按照年龄分配到不同的区域存储。
具体来讲,就是把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域,新生代存放存活时间短的对象,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
对于新生代的对象,可以只关注如何保留少量存活而不是去标记那些大量将要被回收的对象;
对于老年代,可以降低垃圾收集频率,同时更加关注那些要消亡的对象。
为了降低垃圾回收的代价,在新生代和老年代采用了不同的垃圾收集算法。
基于分代,产生了一些垃圾收集的类型划分:
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
垃圾收集算法
标记-清除算法
如字所示,该算法分为两个阶段:
- 标记: 标记出所需回收对象
- 清除: 回收所有被标记的对象
该算法算垃圾回收算法中比较普遍的算法,但其出现两个缺点:
- 执行效率不稳定,如果需要回收大量的对象,那其标记的开销及上下文切换也是很大的,这项会导致其效率及性能的降低
- 回收后内存空间碎片化问题,该算法会导致大量不连续的内存碎片,空间碎片太多会增加GC的难度及不可回收对象的风险
标记-清除算法主要应用在老年代的垃圾回收中
标记-复制算法
标记-复制算法解决了标记-清除算法执行效率的问题
其过程就是将内存划分为大小相等的两块,在程序一般运行的时候,在一块区域,当执行GC后,就将存活后的对象复制到另一块上,同时清理掉运行中块区域的内存
该算法存在着一个明显的缺点:一部分空间没有使用或空闲,导致空间的浪费
该算法主要应用在新生代,因新生代中存活对象较少,故复制开销较小
一般虚拟机的具体实现不会采用1:1的比例划分,以HotSpot为例,HotSpot虚拟机将内存分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。默认Eden和Survivor的大小比例是8∶1。
标记-整理算法
为了降低内存的消耗,引入的一种针对性的算法
其过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是将所有存活对象通过整理将所有存活对象移至该内存空间的整体一端,最后根据划分边界,将所有未存活对象清理
该算法主要用于老年代,在老年代这种大量对象存活的区域,移动对象是很大的负担,而且这种移动对象必须全程暂停程序的各个线程才能进行
根据上述垃圾回收算法,我们了解到了JVM中各类分类收集算法,新生代主要采用标记复制算法,老年代中主要采用标记清除和标记整理算法。
常见的GC
图中列出了七种垃圾收集器,连线表示可以配合使用,所在区域表示它是属于新生代收集器或是老年代收集器。
这里还标出了垃圾收集器采用的收集算法,G1收集器比较特殊,整体采用标记-整理算法,局部采用标记-复制算法
各个版本的JDK默认GC
监控工具
命令行
top
:显示系统整体资源使用情况
vmstat
:监控内存和CPU
iostat
:监控IO使用
JDK监控工具
jps:虚拟机进程查看
jps类似Linux下的ps,他会列出Java程序的进程
jps命令格式:
jps [options] [hostid]
# eg:
jps -l
jstat:虚拟机运行时信息查看
jstat [ option vmid [interval[s|ms] [count]] ]
# eg: 查看Java进程5728的ClassLodader的相关信息,每秒统计一次
jatat -class -t 5728 1000 2
# eg: 查看Java进程5728的GC相关的堆信息输出
jstat -gc 5728
jinfo:虚拟机配置查看
jinfo的作用是实时查看和调整虚拟机各项参数
jinfo [option] pid
# eg: 查看5728的新生对象晋升老年代最大年龄
jinfo -flag MaxtenuringThreshold 5728
jmap:内存映像(导出)
jmap一般用于导出dump文件
jmap [option] vmid
# eg: 使用jmap生成PID为5728的Java程序的对象统计信息,并输出到dump.txt中
jmap -histo 5728 > d:\dump.txt
# eg: jmap得到当前堆快照
jmap -dump:format=b,file=d:\heap.hprof 5728
jhat:堆存储快照文分析
经常与jmap搭配使用,类分析jmap生成的堆存储快照。jhat内置了一个微型的HTTP服务器,将dump文件分析后,可以显示在浏览器中.
jhat d:\heap.hprof
当显示Server is ready,可以在本地的localhost:7000看到对应的分析结果
jstack:Java堆栈跟踪
jstack一般用于生成当前时刻的线程快照
jstack命令格式:
jstack [option] vmid
# eg: jstack -l 5728
jcmd:多功能命令
在jdk1.7以后新增的命令,该命令可以实现以上命令初jstat以外所有的命令功能
例如,使用jcmd列出当前系统中的所有运行中JVM:
jcmd -l
可视化监控工具
JConsole
JConsole即Java Monitoring and Management Console,是一款基于JMX的可视化监控管理工具
Jconsole程序文娱JAVA_HOME的bin目录下,可以直接通过命令启动
我们可以直接通过新建对话框中,罗列所有的本地Java应用程序,选择需连接的程序即可
可视化调优及监控就这里简化以下贴一下原文地址:
https://zhuanlan.zhihu.com/p/363480830
JVM调优及总结
JVM调优听起来高大上,但只是Java性能优化的最后一颗子弹
我们一般是遇到性能问题,第一是优化程序,最后才可能是JVM调优
JVM的自动内存管理本来就是将开发人员将内存管理的泥沼中拉出来,即使不得不进行JVM优化,也绝不能拍脑门就去调参数,一定要全面监控,详细分析性能数据来进行调优。
JVM调优的时机
不得不考虑JVM调优的情况有哪些呢?
- 老年代内存持续上涨经常达到设置的最大内存值
- Full GC频繁
- GC停顿导致的时间过长(1S以上)
- 应用中经常出现OOM等异常
- 应用中使用本地缓存且占用大量内存空间
- 系统吞吐量与响应性能不高或下降
JVM调优的目标
吞吐量,延迟,内存占用三者类似CAP,构成了不可能三角,只能选择其中两个进行调优,不可三者兼得。
- 延迟:GC低停顿和GC低频率
- 低内存占用
- 高吞吐量
选择了其中两个,必然会以牺牲另一个为代价
下面展示一些JVM调优的量化目标参考实例:
- Heap内存使用率 <= 70%
- 老年代内存使用率 <= 70%
- avgpause <= 1s
- Full GC次数为0或,平均GC时间频率间隔(avg pause interval) >= 24h
请注意以上只是例子,每个需要进行调优的程序目标是不一样的
JVM调优的步骤
一般情况下,JVM调优通过以下步骤进行:
- 分析系统运行情况,分析GC日志及dump文件,判断是否需要优化,确定系统瓶颈
- 确定JVM调优量化目标
- 确定JVM调优参数
- 一次确定调优内存,延迟,吞吐量等指标
- 对比观察调优前后的差异
- 经过不断的分析和调整,找到适合该系统的最佳JVM参数配置
- 找到最合适的参数,将这些参数应用到所有服务器,并进行日志记录及监控
上述步骤中,某些步骤需要通过多次迭代完成的,一般是从满足程序的内存使用需求开始,之后是时间,最后为系统吞吐量,每一个步骤都是进行下一步步骤的基础,不可逆行
常用调优策略
选择合适的GC
- CPU单核,那么毫无疑问应该使用SerialGC
- CPU多核,关注吞吐量,那么可以选择PS+PO
- CPU多核,关注用户停顿时间,JDK1.6或1.7则选择CMS
- CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1
参数配置:
//设置Serial垃圾收集器(新生代)
开启:-XX:+UseSerialGC
//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
开启 -XX:+UseParallelOldGC
//CMS垃圾收集器(老年代)
开启 -XX:+UseConcMarkSweepGC
//设置G1垃圾收集器
开启 -XX:+UseG1GC
调整内存大小
GC频率非常频发,则表示堆内存分配空间可能太小。
请注意:
若GC频率非常频繁,但每次GC回收的对象很少,那么这个时候并不是内存分配过小,而可能导致内存泄露导致对象无法回收,从而进行频繁GC
参数配置:
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
设置符合预期的GC停顿时间
程序经常的卡顿,若内存分配合理,GC次数也比较合理,则可能是GC停顿时间的问题,若没有确切的停顿时间,则GC会以吞吐量为主。
请不要设置不切实际的停顿时间,否则可能导致多次GC
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
调整内存区域大小比率
某个区域GC频繁,其他全部正常,若对应的区域空间不足,导致需要频繁的GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率
请注意:
也许并非空间不足,而是因为内存泄露导致内存无法回收,从而导致GC频繁
参数配置:
//survivor区和Eden区大小比率
-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
调整对象升老年代的年龄
老年代GC频繁,每次GC回收很多对象,则可能新生代没经过几次GC存活周期后就进入到老年代了,这时候我们就需要设置新生代升老年代年龄值,来降低老年代存活的对象及GC频率
请注意:
增加了年龄之后,这些对象可能会经常在新生代被GC回收,这回导致新生代GC的频率增加,并且可能导致复制对象的时间开销变长
参数配置:
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
-XX:InitialTenuringThreshol=7
调整大对象的标准
老年代频繁GC,每次回收对象很多,且每个对象体积比较大,则可能大量的大对象分配到老年大,导致老年代容易被填满从而造成频繁GC,可设置对象直接进入老年代的标准
请注意:
这样会导致新生代GC的频率及时间的增加
配置参数:
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
调整GC的触发时间
CMS或G1经常Full GC,程序卡顿严重,因为CMS及G1是多线程GC,在GC的过程中,可能会产生新的对象,所以在GC的时候一般需要预留一部分内存空间来容纳新产生的对象,如果这个时候这部分内存空间不够容纳新产生的内存对象,则JVM会停止所有业务线程(STW)来保证GC的正常运行,故需要将GC的触发时间提前,以避免导致全部STW停滞而导致的业务卡顿
请注意:
提早触发GC会增加GC的频率
配置参数
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
调整JVM本地内存大小
GC频率,时间,回收的对象都正常,堆内存空间充足,但是出现OOM异常,在JDK1.8之后,一些永久代内存被移至堆外内存,这片内存也叫本地内存,若该区域出现内存不足则不会触发GC,只有在堆内存区域触发的时候会顺便把本地内存回收了,一旦本地内存分配不足则会出现OOM异常
请注意:
本地内存的异常除上述现象外,异常信息可能是OOM,direct buffer memory,除了调整本地内存大小外,也可以出现此异常时进行捕获,手动触发GC
配置参数:
XX:MaxDirectMemorySize
JVM调优案例
https://zhuanlan.zhihu.com/p/363961261
Q.E.D.