JVM调优篇-08
1.jvm 调优实用工具?
-
jconsole 工具
-
VisualVM 工具
-
监控应用程序的 CPU、GC、堆。方法区和线程信息(jstack 和 jstat 的功能)
-
dump 文件以及分析(jmap 和 jhat 的功能)
-
方法级的程序性能分析,可以找出被调用最多,运行时间最长的方法
-
离线程序快照:收集程序运行时配置、线程 dump。内存 dump 等信息建立一个快照,并可以将快照发送给开发者进行 bug 反馈。
-
插件化处理,有无限扩展可能
-
2.打印 gc 日志的命令?
#打印gc日志的命令
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/Users/lizhengqiang/Documents/gc.log
找到 gc.log 文件,刚开始没有发生 GC,所以文件是空的,等到发生 GC 后打开
3.内存泄漏和内存溢出?
内存泄漏:是指创建的对象已经没有用处,正常情况下应该会被垃圾收集器回收,但是由于该对象仍然被其他对象进行了无效引用,导致不能够被垃圾收集器及时清理,这种现象称之为内存泄漏。内存泄漏会导致内存堆积,最终发生内存溢出,导致 OOM。
内存溢出:java 堆用于存储对象实例,只要不断的创建实例,并保证 GC roots 到对象是可达的,避免被回收,对象数量达到堆的最大容量时就会出现内存溢出异常.
出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
//设置jvm参数 VM Args:-Xms20m-Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class Jvm_01_HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
内存溢出分三种情况
OutOfMemoryError: PermGen space
Permanent Generation space 这个区域主要用来保存加来的 Class 的一些信息,在程序运行期间属于永久占用的,Java 的 GC 不会对他进行释放,所以如果启动的程序加载的信息比较大,超出了这个空间的大小,就会发生溢出错误;
解决的办法:
增加空间分配——增加 java 虚拟机中的 XX:PermSize 和 XX:MaxPermSize 参数的大小,其中 XX:PermSize 是初始永久保存区域大小,XX:MaxPermSize 是最大永久保存区域大小。
OutOfMemoryError:Java heap space
heap 是 Java 内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC 又来不及释放的时候,就会发生溢出错误。Java 中对象的创建是可控的,但是对象的回收是由 GC 自动的,一般来说,当已存在对象没有引用(即不可达)的时候,GC 就会定时的来回收对象,释放空间。但是因为程序的设计问题,导致对象可达但是又没有用(即前文提到的内存泄露),当这种情况越来越多的时候,问题就来了。
针对这个问题,我们需要做一下两点:
1、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
2、增加 Java 虚拟机中 Xms(初始堆大小)和 Xmx(最大堆大小)参数的大小。
StackOverFlowError
stack 是 Java 内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。针对这个问题,除了修改配置参数-Xss 参数增加线程栈大小之外,优化程序是尤其重要。
4.什么是对象逃逸?
什么是对象逃逸?对象逃逸优化有哪几种?
逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
#在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析。
优化有三种:栈上分配;标量替换;锁消除(或称同步消除)。
栈上分配(Stack Allocations):在 Java 虚拟机中, Java 堆上分配创建对象的内存空间几乎是 Java 程序员都知道的常识, Java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。
标量替换(Scalar Replacement):若一个数据已经无法再分解成更小的数据来表示了, Java 虚拟机中的原始数据类型(int 、 long 等数值类型及 reference 类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate), Java 中的对象就是典型的聚合量。如果把一个 Java 对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
同步消除(Synchronization Elimination):线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个 StringBuffer.append()方法中都有一个同步块,锁就是 sb 对象在 concatString()方法内部。也就是 sb 的所有引用都永远不会逃逸到 concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉 。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略。虚拟机观察变量 sb,经过逃逸分析后会发现它的动态作用域被限制所有的同步措施而直接执行。
5.什么是锁粗化?
6.有过 jvm 调优经验吗?
JVM 调优情况十分复杂,各种情况都可能导致垃圾回收不能够达到预想的效果。对于场景问题,可以从如下几个大方向进行设计:
MinorGC
频繁
- MinorGC 是针对新生代进行回收的,每次在 MGC 存活下来的对象,会移动到 Survivor1 区。
- 大访问压力下, MGC 频繁一些是正常的,只要 MGC 延迟不导致停顿时间过长或者引发 FGC
- 可以适当的增大 Eden 空间大小,降低频繁程度,同时要保证,空间增大对垃圾回收时间产生的停顿时间增长也是可以接受的。
Full GC
- 如果 MinorGC 频繁,且容易引发 Full GC 。
- 每次 MGC 存活的对象的大小,是否能够全部移动到 S1 区,如果 S1 区大小< MGC 存活的对象大小,这批对象会直接进入老年代。
- 这批对象的年龄才 1 岁,很有可能再多等 1 次 MGC 就能被回收了,可是却进入了老年代,只能等到 Full GC 进行回收,很可怕。这种情况下,应该在系统压测的情况下,实时监控 MGC 存活的对象大小,并合理调整 eden 和 s 区的大小以及比例。
- 还有一种情况会导致对象在未达到 15 岁之前,直接进入老年代,就是 S1 区的对象,相同年龄的对象所占总空间大小>s1 区空间大小的一半,所以为了应对这种情况,对于 S 区的大小的调整就要考虑:尽量保证峰值状态下, S1 区的对象所占空间能够在 MGC 的过程中,相同对象年龄所占空间不大于 S1 区空间的一半,因此对于 S1 空间大小的调整,也是十分重要的。
大对象
创建频繁,导致 Full GC 频繁。- 对于大对象, JVM 专门有参数进行控制,-XX: PretenureSizeThreshold 。超过这个参数值的对象,会直接进入老年代,只能等到 full GC 进行回收,所以在系统压测过程中,要重点监测大对象的产生。
- 如果能够优化对象大小,则进行代码层面的优化,优化如:根据业务需求看是否可以将该大对象设置为单例模式下的对象,或者该大对象是否可以进行拆分使用,或者如果大对象确定使用完成后,将该对象赋值为 null,方便垃圾回收。
- 如果代码层面无法优化,则需要考虑
- 调高-XX: PretenureSizeThreshold 参数的大小,使对象有机会在 eden 区创建,有机会经历 MGC 以被回收。但是这个参数的调整要结合 MGC 过程中 Eden 区的大小是否能够承载,包括 S1 区的大小承载问题。
- 这是最不希望发生的情况,如果必须要进入老年代,也要尽量保证,该对象确实是长时间使用的对象,放入老年代的总对象创建量不会造成老年代的内存空间迅速长满发生 Full GC,在这种情况下,可以通过定时脚本,在业务系统不繁忙情况下,主动触发 full gc。
- 内存泄漏导致的 MGC 和 FGC 频繁,最终引发 oom 。
- 纯代码级别导致的 MGC 和 FGC 频繁。
- 如果是这种情况,那就只能对代码进行大范围的调整,这种情况就非常多了,而且会很糟糕。
- 如大循环体中的 new 对象,未使用合理容器进行对象托管导致对象创建频繁,不合理的数据结构使用等等。
MGC 与 FGC 停顿时间长导致影响用户体验。其实对于停顿时间长的问题无非就两种情况:
- a:gc 真实回收过程时间长,即 realtime 时间长。这种时间长大部分是因为内存过大导致,导致从标记到清理的过程中需要对很大的空间进行操作,导致停顿时间长。
- b:gc 真实回收时间 real time 并不长,但是 user time(用户态执行时间)和 systime (核心态执行时间)时间长,导致从客户角度来看,停顿时间过长。
对于 a 情况,要考虑减少堆内存大小,包括新生代和老年代,比如之前使用 16G 的堆内存,可以考虑将 16G 内存拆分为 4 个 4G 的内存区域,可以单台机器部署 JVM 逻辑集群,也可以为了降低 GC 回收时间进行 4 节点的分布式部署,这里的分布式部署是为了降低 GC 垃圾回收时间。
对于 b 情况,要考虑线程是否及时达到了安全点,通过-XX:+PrintSafepointStatistics 和-XX: PrintSafepointStatisticsCount=1 去查看安全点日志,如果有长时间未达到安全点的线程,再通过参数-XX:+SafepointTimeout 和-XX: SafepointTimeoutDelay=2000 两个参数来找到大于 2000ms 到达安全点的线程,这里的 2000ms 可以根据情况自己设置,然后对代码进行针对的调整。除了安全点问题,也有可能是操作系统本身负载比较高,导致处理速度过慢,线程达到安全点时间长,因此需要同时检测操作系统自身的运行情况。
7.JVM 两种常见异常?
StackOverFlowError
: 如果 Java 虚拟机栈容量不能动态扩展,而此时线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError
: 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展的时候,无法申请到足够的内存(Java 虚拟机堆中没有空闲内存,垃圾回收器也没办法提供更多内存)
8.堆栈溢出异常?
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemorvError 异常。
-
单线程 用-Xss 减少栈内存容量,来模拟 StackOverflowError 异常.
-
多线程,-Xss 设置大一些,模拟 OutOfMemoryError 异常.
9.方法区运行时常量池异常?
String.intern()是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。在 JDK1.6 及之前的版本中,由于常量池分配在永久代内,我们可以通过-Xx:PermSize 和-
XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量.
public class Jvm_04_RuntimeConstantPooloOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免FullGC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生00M了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
OutOfMemoryError 后面跟随的提示信息是“Perm Gen space”
10.本机直接内存异常?
DirectMemory 容量可通过-XX:MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样,直接通过反射获取 Unsafe 实例进行内存分配(Unsafe 类的 getUnsafe))方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有 rtjar 中的类才能使用 Unsafe 的功能)。因为,虽然使用 DirectByteBuffer 分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是 unsafeallocateMemorv)。
public class Jvm_05_DirectMemory0OM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
- 点赞
- 收藏
- 关注作者
评论(0)