java内存区域
运行时数据区域
- 程序计数器
当前线程私有
- java虚拟机栈
当前线程私有
- 本地方法栈
虚拟机使用本地方法
- Java heap
对象实例存放的地方
- 方法区
它是各个线程共享的内存区域
JDK8 之前
使用永久代会导致如果类及方法信息如果增加
为什么要使用元空间取代永久代的实现
- 字符串存在永久代中
容易出现性能问题和内存溢出, 。 - 类及方法的信息等比较难确定其大小
因此对于永久代的大小指定比较困难, 太小容易出现永久代溢出, 太大则容易导致老年代溢出, 。 - 永久代会为 GC 带来不必要的复杂度
并且回收效率偏低, 。 - 将 HotSpot 与 JRockit 合二为一
。
- 运行时常量池
运行时常量池是方法区的一部分
- 直接内存
直接内存并不是虚拟机运行时数据区的一部分
比如NIO
- OutOfMemory
-
堆内存异常
-
虚拟机栈异常
-
本地方法栈
public void stackLeak() { stackLeak(); }
-
方法区和运行时常量池溢出
String::intern 是一个本地方法 它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串, 则返回代表池中这个字符串的String对象的引用, 否则, 会将此String对象包含的字符串添加到常量池中, 并且返回此String对象的引用, 。
Java里面的String对象到底神奇在什么地方- 运行时常量池溢出
int i = 0; List<String> strList = new ArrayList<>(); while(true){ strList.add(String.valueOf(i++).intern()); }
- 方法区溢出
使用动态代理 比如CGLib直接操作字节码运行时生成大量的动态类, 当增强的类越多, 就需要越大的方法区以保证动态生成的新类型可以载入内存, , public class RuntimeConstantPoolOOM { static class OOMObject { } public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, args); } }); enhancer.create(); } } }
在经常运行时生成大量动态类的应用场景中
- 可以设定元空间最大值
-XX:MaxMetaspaceSize: - 设定元空间初始空间大小
-XX:MetaspaceSize:
以上代码虽然创建的临时对象应该被回收
shallow size and retained size
- Dominator by retain size
这里有个对Dominator tree更加直观的描述支配树体现了对象实例间的支配关系
- 对象A的子树
所有被对象A支配的对象集合( 表示对象A的保留集(retained set)) 即深堆, 。 - 如果对象A支配对象B
那么对象A的直接支配者也支配对象B, 。 - 支配树的边与对象引用图的边不直接对应
。
如下图所示:左图表示对象引用图 右图表示左图所对应的支配树, 对象A和B由根对象直接支配。 由于在到对象C的路径中, 可以经过A, 也可以经过B, 因此对象C的直接支配者也是根对象, 对象F与对象 D相互引用。 因为到对象F的所有路径必然经过对象D, 因此, 对象D是对象F的直接支配者, 而到对象D的所有路径中。 必然经过对象c, 即使是从对象F到对象D的引用, 从根节点出发, 也是经过对象c的,所以, 对象D的直接支配者为对象C, 。
- 本机直接内存溢出
直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定
private static final int _1MB = 1024 * 1024;
public static void directMemoryOOM() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
由直接内存导致的内存溢出
垃圾收集器与内存分配策略
- 可达性分析算法
在Java技术体系里
- 在虚拟机栈中引用的对象
譬如当前正在运行的方法所使用到的参数, 局部变量、 临时变量、
public class Test {
}
private void method() {
Integer temp = new Test();
temp = null;
}
temp 作为临时变量<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>引用了new Test(), 从而不会被回收<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当temp = null 时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>原来的new Test()就会被回收
- 在方法区中类静态属性引用的对象<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>譬如Java类的引用类型静态变量
public class Test {
p rivate static Test s;
public static void main(String[] args) {
Test temp = new Test();
temp.s = new Test();
temp = null;
}
}
s 作为类的静态属性<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>被赋值new Test();从而这个new Test()不会被回收<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>但是这个temp作为临时变量<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>则它之前被赋值的new test()就会被回收
- 在方法区中常量引用的对象
譬如字符串常量池里的引用(字符串常量池), public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; String s3 = "xxx"; }
- 在本地方法栈中JNI引用的对象
所谓本地方法就是一个 java 调用非 java 代码的接口
当调用 Java 方法时
- Java虚拟机内部的引用
如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointerException, OutOfMemoryError)等、 还有系统类加载器, 。 - 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean
JVMTI中注册的回调、 本地代码缓存等、
除了这些固定的GC Roots集合以外
引用(直观理解)
- 强应用
只要强引用关系还在: 垃圾收集器就永远不会回收掉被引用的对象, 。 - 软引用
只被软引用关联着的对象: 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围之中进行第二次回收, 如果这次回收还没有足够的内存, 才会抛出内存溢出异常, 。 - 弱引用
只被弱引用关联的对象只能生存到下一次垃圾收集发生为止: 。 - 虚引用
一个对象是否有虚引用的存在: 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例, 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 。
分代收集理论
当前的垃圾收集器
- 弱分代假说
绝大多数对象都是朝生夕灭的: - 强分代假说
熬过越多次垃圾收集过程的对象就越难以消亡:
这两个分代假说共同奠定了多款常用的垃圾收集器的一致设计原则
基础的垃圾回收器
-
标记-清除算法
首先标记出所有需要回收的对象 在标记完成后, 统一回收掉所有被标记的对象, 。
缺点: - 执行效率不稳定
如果堆中大部分是需要被回收的, 则会导致标记和清除两个过程的执行效率都随对象数量增长而降低, - 内存空间碎片问题
- 执行效率不稳定
-
标记复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题 标记-复制算法将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块, 当这一块的内存用完了。 就将还存活着的对象复制到另一块上面, 然后再把已使用过的内存空间一次清理掉, 。
IBM有一项研究表明 新生代中的对象有98%熬不过第一轮收集, 因此不需要按照1:1的比例来划分, Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间, HotSpot设定的比例是8:2, 每次分配内存只使用Eden和其中一块Survivor空间, 发生垃圾收集时。 将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上, 然后直接清理掉Eden和已用过的那块Survivor空间, 。 缺点
: - 如果大量对象存活
则会导致大量的内存复制开销, - 内存利用率只有一半
- 如果大量对象存活
-
标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作 效率将会降低, 标记-整理算法是让所有存活对象都向内存空间一端移动。 然后直接清理掉边界以外的内存, 。
缺点: - 当有大量存活对象时
则移动对象就会降低回收效率,
- 当有大量存活对象时
垃圾收集器
-
Serial收集器
单线程工作的收集器 它"单线程"的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作, 更重要的是强调在它进行垃圾收集时, 必须暂停其他所有工作线程, 直到它收集结束, 。
虽然是很早出现的收集器 但是有着优于其他收集器的地方, 那就是简单高效, 对于内存资源受限的环境, 它是所有收集器里额外内存消耗最小的, 对于单核处理器或处理器核心数较少的环境来说; Serial收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率, 。 -
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本 除了同时使用多条线程进行垃圾收集之外, 其余的行为包括Serial收集器可用的所有控制参数, 收集算法、 Stop The World、 对象分配策略、 回收策略等都与Serial收集器完全一致、 。
-
Parallel Scavenge 收集器
Parallel Scavenge收集器又称为吞吐量优先收集器 和ParNew收集器类似, 是一个新生代收集器, 同样是使用标记-复制算法的并行多线程收集器。 它的目标是达到一个可控制的吞吐量。 吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:。
$$吞吐量=\cfrac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间}$$
高吞吐量就可以最高效率地利用处理器资源 尽快完成程序的运算任务, 有一些参数可以控制GC停顿的时间。 -XX:MaxGCPauseMills: 还可以通过-XX:GCTimeRatio来控制GC消耗时间所占的比例, 从而来控制吞吐量, 此外还可以通过-XX:+UseAdaptiveSizePolicy, 让JVM来动态调整, 但是过分的设置高吞吐量。 可能会导致新生代频繁的回收, 新生代内存变小, 来降至提高吞吐量, 。 -
Serial Old收集器
Serial Old收集器是Serial收集器的老年版本 -
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本 支持多线程并行收集, 基于标记-整理算法实现, 。
-
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器 整个过程分为四个步骤:。 - 初始标记
- 并发标记
- 重新标记
- 并发清除
由于是和用户线程一起并发执行
-
Garbage First收集器
G1( )
G1开创的基于Region的堆内存布局能够控制消耗在垃圾收集器上的时间 G1不再根据分代收集。 要么是整个新生代, 要么是这个老年代, 再要么就是整个Java堆(Full GC), G1可以面向堆内存任何部分来组成回收集进行回收, 衡量标准不再是它属于哪个分代, 而是哪块内存中存放的垃圾数量最多, 回收受益最大, 这就是G1收集器的Mixed GC模式, G1把连续的堆划分为多个大小相等的region。 每一个region可以根据需要扮演不同的空间, 如Eden, Survivor、 老年代、 以及存储大对象的Humongous区域, 。
用户可以指定期望的停顿时间 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡, 从G1开始。 最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率, 而不追求一次把整个Java堆全部清理干净, 。
G1是基于标记整理算法的 从而不会产生内存碎片, 但无论是内存占用, 还是运行时的额外执行负载都要比CMS要高, 。 -
低延迟收集器
在内存占用 吞吐量和延迟三项指标里, 延迟的重要性日益凸显, 随着硬件性能的提升, 吞吐量会变高, 内存会逐渐扩大, 从而回收大内存就会耗费更多的时间, 以低延迟出发的收集器。 Shenandoah: ZGC收集器、 。
GC 日志分析
- GC 日志参数
:
- -verbose:gc :输出gc日志信息
默认输出到标准输出, - -XX:+PrintGC :等同于-verbose:gc表示打开简化的GC日志
- -XX:+PrintGCDetails :在发生垃圾回收时打印内存回收详细的日志并在进程退出- 时输出当前内存各区域分配情况
- -XX:+PrintGCTimeStamps :输出GC发生时的时间戳
- -XX:+PrintGCDateStamps :输出GC发生时的时间戳
以日期的形式( 如- 2021-10-04T21:53:59.234+0800), - -XX:+PrintHeapAtGC :每一次GC前和GC后
都打印堆信息, - -Xloggc:
:把GC日志写入到一个文件中去 而不是打印到标准输出中, - -XX:+TraceClassLoading : 监控类的加载
- -XX:+PrintGCApplicationStoppedTime: 打印GC时线程的停顿时间
- -XX:+PrintGCApplicationConcurrentTime : 垃圾收集之前打印出应用未中- 断的执行时间
- -XX:+PrintReferenceGC : 记录回收了多少种不同引用类型的引用
- -XX:+PrintTenuringDistribution : 让JVM在每次MinorGC后打印出当前使用- 的Survivor中对象的年龄分布
- -XX:+UseGCLogFileRotation : 启用GC日志文件的自动转储
- -XX:NumberOfGClogFiles=1 : GC日志文件的循环数目
- -XX:GCLogFileSize=1M : 控制GC日志文件的大小
- GC 日志分类
针对HotSpot VM的实现 它里面的GC按照回收区域又分为两大种类型:一种是部分收集, Partial GC( ) 一种是整堆收集(Full GC), - 部分收集:不是完整收集整个Java堆的垃圾收集
其中又分为:。 - 新生代收集
Minor GC / Young GC( :只是新生代) Eden\S0,S1( 的垃圾收集) - 老年代收集
Major GC / Old GC( :只是老年代的垃圾收集) 。 - 目前
只有CMS GC会有单独收集老年代的行为, 。 - 注意
很多时候Major GC会和Full GC混淆使用, 需要具体分辨是老年代回收还是整堆回收, 。
- 目前
- 新生代收集
- 混合收集
Mixed GC):收集整个新生代以及部分老年代的垃圾收集( 。 - 目前,只有G1 GC会有这种行为
- 整堆收集
Full GC):收集整个java堆和方法区的垃圾收集( 。
- 部分收集:不是完整收集整个Java堆的垃圾收集
GC日志格式的规律一般都是:
- GC前内存占用->GC后内存占用
该区域内存总大小)( - [PSYoungGen: 5986K->696K(8704K)] 5986K->704K(9216K)
- 中括号内:GC回收前年轻代堆大小
回收后大小, (年轻代堆总大小, ) - 括号外:GC回收前年轻代和老年代大小
回收后大小, (年轻代和老年代总大小),
----------------------Minor GC----------------------------
2021-09-09T14:44:04.813+0800: 0.163:
[GC (Allocation Failure) 2021-09-09T14:44:04.813+0800: 0.163:
[DefNew: 139776K->17472K(157248K), 0.0164545 secs] 139776K->45787K(506816K), 0.0165501 secs]
[Times: user=0.00 sys=0.02, real=0.02 secs] 2021-09-09T14:44:04.853+0800: 0.203:
[GC (Allocation Failure) 2021-09-09T14:44:04.853+0800: 0.203:
[DefNew: 157248K->17471K(157248K), 0.0192998 secs] 185563K->84401K(506816K), 0.0193485 secs]
[Times: user=0.02 sys=0.01, real=0.02 secs]
-----------------------Full GC----------------------------
2021-09-09T14:44:05.240+0800: 0.589:
[GC (Allocation Failure) 2021-09-09T14:44:05.240+0800: 0.589:
[DefNew: 157148K->157148K(157248K), 0.0000289 secs]2021-09-09T14:44:05.240+0800: 0.589:
[Tenured: 341758K->308956K(349568K), 0.0459961 secs] 498907K->308956K(506816K),
[Metaspace: 2608K->2608K(1056768K)], 0.0460956 secs]
[Times: user=0.05 sys=0.00, real=0.05secs]
2021-09-09T14:44:04.813-GC事件开始的时间点
GC - 用来区分Minor GC还是Full GC的标志
DefNew表示垃圾收集器的名称
(157248K) 表示年轻代的总空间大小
139776K->45787K 表示在垃圾收集前后整个堆内存的使用情况
0.0165501 secs - GC事件的持续时间
[Times: user=0.00 sys=0.02, real=0.02 secs] 表示此次GC事件的持续时间
- user
进程执行用户态代码所耗费的处理器时间: - sys
进程执行核心态代码所耗费的时间: - real
执行动作从开始到结束耗费的时钟时间:
前面两个是处理器时间
虚拟机性能监控和故障处理工具
- Jps
可以列出正在运行的虚拟机进程 并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID, 。
基本语法为:jps [options] [hostid]- -q:仅仅显示LVMID (local virtual machine id)
即本地虚拟机唯一id, 不显示主类的名称等。 - -1:输出应用程序主类的全类名或如果进程执行的是jar包
则输出jar完整路径, - -m:输出虚拟机进程启动时传递给主类main()的参数
- -v:列出虚拟机进程启动时的JVM参数
比如: -Xms20m -Xmx50m是启动程序指定的jvm参数。 。 - 说明:以上参数可以综合使用
。
- -q:仅仅显示LVMID (local virtual machine id)
- 虚拟机统计信息监视工具
jstat(VM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具 它可以显示本地或者远程虚拟机进程中的类装载。 内存、 垃圾收集、 JIT编译等运行数据、 。
-class:显示ClassLoader的相关信息:类的装载 卸载数量、 总空间、 类装载所消耗的时间等、 - -t
前面显示Timestamp一列: - -h3
每隔三次输出标题: - 9000
进程号: - 1000
每隔1000毫米输出一次: - 10
一共输出10次:
- -t
垃圾回收相关的:
- -gc:显示与GC相关的堆信息
包括Eden区。 两个Survivor区、 老年代、 永久代等的容量、 己用空间、 GC时间合计等信息、 。 - -gccapacity:显示内容与-gc基本相同
但输出主要关注Java堆各个区域使用到的最大, 最小空间、 。 - -gcutil:显示内容与-gc基本相同
但输出主要关注已使用空间占总空间的百分比, 。 - -gccause:与-gcutil功能一样
但是会额外输出导致最后一次或当前正在发生的GC产生的原因, 。 - -gcnew:显示新生代Gc状况
- -gcnewcapacity:显示内容与-gcnew基本相同
输出主要关注使用到的最大, 最小空间、 - -geold:显示老年代GC状况
- -gcoldcapacity:显示内容与-gcold基本相同
输出主要关注使用到的最大, 最小空间、 - -gcpermcapacity:显示永久代使用到的最大
最小空间、 。
- Jmap
java内存映像工具:
jmap(JVM Memory Map):作用一方面是获取dump文件 堆转储快照文件( 二进制文件, ) 它还可以获取目标Java进程的内存相关信息, 包括Java堆各区域的使用情况, 堆中对象的统计信息、 类加载信息等、 。 - jmap [option]
- jmap [option] <executable
- jmap [option] [server_id@]
- jmap [option]
- -dump
生成Java堆转储快照: dump文件: - 特别的: -dump:live只保存堆中的存活对象
- -heap
输出整个堆空间的详细信息: 包括GC的使用, 堆配置信息、 以及内存的使用信息等, - -histo
输出堆中对象的统计信息: 包括类, 实例数量和合计容量、 - 特别的:-histo:live只统计堆中的存活对象
- -permstat
以ClassLoader为统计口径输出永久代的内存状态信息: - 仅linux/solaris平台有效
- -finalizerinfo
显示在F-Queue中等待Finalizer线程执行finalize方法的对象: - 仅linux/solaris平台有效
- -F
当虚拟机进程对-dump选项没有任何响应时: 可使用此选项强制执行生成dump文件, - 仅linux/solaris平台有效
- -h |-help
jmap工具使用的帮助命令: - -J
传递参数给jmap启动的jvm:
- Jstack: java 堆栈跟踪工具
生成线程快照的作用:可用于定位线程出现长时间停顿的原因 如线程间死锁, 死循环、 请求外部资源导致的长时间等待等问题、 这些都是导致线程长时间停顿的常见原因。 当线程出现停顿时。 就可以用jstack显示各个线程调用的堆栈情况, 。
option参数
- -F
当正常输出的请求不被响应时: 强制输出线程堆栈, - -m
除堆栈外: 显示关于锁的附加信息, - -h
如果调用到本地方法的话: 可以显示C/C++的堆栈,
- Jcmd
它是一个多功能的工具 可以用来实现前面除了jstat之外所有命令的功能, 比如:用它来导出堆。 内存使用、 查看Java进程、 导出线程信息、 执行GC、 JVM运行时间等、
可视化故障处理工具
- JConsole
- VisualVM
Monitor 显示CPU Classes、 Heap、 Metaspace、 Threads的Usage、
对进程进行heap dump进行分析
dump的结果可以看到class实例的个数
- mat
对Object的引用关系分析更加准确 - Jprofile
判断是不是 GC 引发的问题?
到底是结果
- 时序分析
先发生的事件是根因的概率更大: 通过监控手段分析各个指标的异常时间点, 还原事件时间线, 如先观察到 CPU 负载高, 要有足够的时间 Gap( ) 那么整个问题影响链就可能是, CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> 线程Block增多 -> RT 上涨: 。 - 概率分析
使用统计概率学: 结合历史问题的经验进行推断, 由近到远按类型分析, 如过往慢查的问题比较多, 那么整个问题影响链就可能是, 慢查询增多 -> GC 耗时增大 -> CPU 负载高 -> 线程 Block 增多 -> RT上涨: 。 - 实验分析
通过故障演练等方式对问题现场进行模拟: 触发其中部分条件, 一个或多个( ) 观察是否会发生问题, 如只触发线程 Block 就会发生问题, 那么整个问题影响链就可能是, 线程Block增多 -> CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> RT 上涨: 。 - 反证分析
对其中某一表象进行反证分析: 即判断表象的发不发生跟结果是否有相关性, 例如我们从整个集群的角度观察到某些节点慢查和 CPU 都正常, 但也出了问题, 那么整个问题影响链就可能是, GC 耗时增大 -> 线程 Block 增多 -> RT 上涨: 。
不同的根因 后续的分析方法是完全不同的, 如果是 CPU 负载高那可能需要用火焰图看下热点。 如果是慢查询增多那可能需要看下 DB 情况、 如果是线程 Block 引起那可能需要看下锁竞争的情况、 最后如果各个表象证明都没有问题, 那可能 GC 确实存在问题, 可以继续分析 GC 问题了, 。
总结
我们可以根据进程的内存使用特点
在分析是不是GC导致应用出现问题时
Reference: