JVM

225 阅读12分钟

1、JVM常配置的参数

  1. -Xms 堆初始内存
  2. -Xmx 堆最大内存
  3. -XX:+UseG1GC/CMS 垃圾回收器
  4. -XX:+DisableExplicitGC 禁止显示GC
  5. -XX:MaxDirectMemorySize 设置最大堆外内存,默认是-xmx-survivor,也就是基本上和-xmx大小相等
  6. -Xss:每个线程的堆栈大小,默认1M
  7. -Xmn: 年轻代大小(eden区+2 survivor)
  8. -XX:newRatio: 4 年轻代与老年代1:4
  9. -XX:survivorRatio: 8Eden区与survivor大小比值

java整个进程占用的内存分类:

  1. 堆内存
  2. metaspace(堆内) JDK8使用metaspace来替代了permsize:永久代大小
  3. 堆外内存使用
  4. 线程栈空间

2、内存泄漏

内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象已经不再需要,但由于长生命周期对象持有它的引用而导致不能被回收。以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

3、内存溢出

内存溢出有以下几种常见的情况:
1、java.lang.OutOfMemoryError: PermGen space (持久代溢出)
我们知道jvm通过持久带实现了java虚拟机规范中的方法区,而运行时常量池就是保存在方法区中的,因此发生这种溢出可能是运行时常量池溢出,或是由于程序中使用了大量的jar或class,使得方法区中保存的class对象没有被及时回收或者class信息占用的内存超过了配置的大小。

2、java.lang.OutOfMemoryError: Java heap space (堆溢出)
发生这种溢出的原因一般是创建的对象太多,在进行垃圾回收之前对象数量达到了最大堆的容量限制。解决这个区域异常的方法一般是通过内存映像分析工具对Dump出来的堆转储快照进行分析,看到底是内存溢出还是内存泄漏。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,定位出泄漏代码的位置,修改程序或算法;如果不存在泄漏,就是说内存中的对象确实都还必须存活,那就应该检查虚拟机的堆参数-Xmx(最大堆大小)和-Xms(初始堆大小),与机器物理内存对比看是否可以调大。

3、虚拟机栈和本地方法栈溢出
(1)、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError,可以通过jvm参数 -Xss调小栈空间大小
(2)、如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

4、对象gc年龄存在哪?gc年龄最大是多少?

因为Object Header(头信息)采用4个bit位来保存年龄,4个bit位能表示的最大数就是15,头信息64位占16个字节,其中Mark Word占8个字节,对象年龄占4个比特位。

5、垃圾收集器

5.1、G1

5.1.1、特点

  1. 并发与并行
  2. 分代收集
  3. 空间整合,基于标记-整理算法,解决了内存碎片的问题。
  4. 可以建立可预测的停顿模型
  5. 将整个java堆内存模型划分为多个大小相等的Region,使得年轻代和老年代不再物理隔离开来

5.1.2、优点

停顿时间短; 用户可以指定最大停顿时间; 不会产生内存碎片:G1 的内存布局并不是固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域 (Region),G1 从整体来看是基于“标记-整理”算法实现的收集器,但从局部 (两个Region 之间)上看又是基于“标记-复制”算法实现,不会像 CMS (“标记-清除”算法) 那样产生内存碎片。

5.1.3、缺点

G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。

5.1.4、总结

相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比 CMS 要高。
从经验上来说,在小内存应用上 CMS 的表现大概率会优于G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间。

5.2、ZGC

ZGC是从JDK11中引入的一种新的支持弹性伸缩和低延迟垃圾收集器,ZGC可以工作在KB~TB的内存之下,作为一种并发的垃圾收集器,ZGC保证应用延迟不会超过10毫秒(即便在堆内存很大的情况下),在JDK11中是以实验阶段的特性被发布出来的,到JDK13时,ZGC可以支持到16TB的堆内存,并且可以将未提交的内存归还给操作系统。

Z垃圾收集器(ZGC)是可伸缩的低延迟垃圾收集器。ZGC可以同时执行所有昂贵的工作,而不会将应用程序线程的执行停止超过10ms,这使得它适合于要求低延迟和/或使用非常大堆(数TB)的应用程序。

Z垃圾收集器可作为实验功能使用,并通过命令行选项启用 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。

设置堆大小 ZGC最重要的调整选项是设置最大堆大小(-Xmx)。由于ZGC是并发收集器,因此必须选择一个最大堆大小,以便:1)堆可以容纳您的应用程序的活动集,以及2)堆中有足够的空间以允许在GC处于运行状态时为分配提供服务运行。需要多少空间非常取决于分配率和应用程序的实时设置大小。通常,给ZGC的内存越多越好。但是同时,浪费内存是不可取的,因此,这全都在于在内存使用和GC需要运行的频率之间找到平衡。

设置并发GC线程数 您可能要看的第二个调整选项是设置并发GC线程数(-XX:ConcGCThreads)。ZGC具有启发式功能,可以自动选择此数字。这种启发式方法通常效果很好,但根据应用程序的特性,可能需要对其进行调整。此选项从根本上决定了应该给GC多少CPU时间。给它太多,GC将占用应用程序太多的CPU时间。给它太少,应用程序分配垃圾的速度可能比GC收集垃圾的速度快。

6、堆外内存

6.1、介绍

  1. 堆外内存的产生:堆内内存的老年代,存放了缓存数据后,内存占用率过大,带来的频繁的GC,以及操做系统对堆内内存不可知的问题,java虚拟机开辟出了堆外内存(off-heap memory)。堆外内存意味着把一些对象的实例分配在Java虚拟机堆内内存之外的内存区域,这些内存直接受操做系统(而不是虚拟机)管理。这样作的结果就是能保持一个较小的堆,以减小垃圾收集对应用的影响。 图片

  2. 堆外内存回收:堆外内存的回收是通过system.gc()来的,依赖于目前的gc机制。通常是通过DirectByteBuffer对象来分配堆外内存,gc的时候就是判断这个对象是否被引用,来决定是否回收。 java 在NIO 包中提供了ByteBuffer类,对堆外内存进行访问。下图为NIO包中ByteBuffer的层次继承关系spa

这里写图片描述

6.2、堆外内存参数配置

  1. -XX:InitialCodeCacheSize=64M
  2. -XX:CodeCacheExpansionSize=1M
  3. -XX:CodeCacheMinimumFreeSpace=1M
  4. -XX:ReservedCodeCacheSize=200M
  5. -XX:MinMetaspaceExpansion=1M
  6. -XX:MaxMetaspaceExpansion=8M
  7. -XX:MaxDirectMemorySize=96M
  8. -XX:CompressedClassSpaceSize=256M

 6.3、问题排查

6.3.1、首先确认堆占用

1、用jmap,jmap 查看heap内存使用情况

jmap -heap pid

可以查看到MetaspaceSize,CompressedClassSpaceSize,MaxMetaSize
jmap和jdk版本有关系,有些jdk版本会查看不到内存信息,可以使用jstat来查看统计信息

2、jstat 收集统计信息

jstat -gc pid 1000
S0C/S0U            S1C/S1U           EC/EU    CCSC/CCSU                  YGC/YGCT          FGC/FCGT         GCT
survivor0容量和使用 survivor1容量和使用 Eden     jdk8是meta,以前应该是PC,PC   young gc次数和耗时  full gc次数和耗时   total gc时间

如果能排除掉heap的问题,就要分析堆外内存情况了。

6.3.2、分析堆外情况

6.3.2.1、使用NMT(native memory tracking)

在JVM参数中添加 -XX:NativeMemoryTracking=[off | summary | detail],比如-XX:NativeMemoryTracking=detail

6.3.2.2、在JVM运行过程中,使用jcmd获取相关信息

jcmd pid VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB],比如(jcmd pid VM.native_memory detail),baseline个基准,之后会输出diff参数,来和这个基线版本进行比较,可以得到两次的内存差。

NMT报告会显示内存使用情况,如下:

类别                  含义
Java Heap        堆大小
Thread              线程
Thread Stack    线程栈

NMT可以得到线程栈大小,排除栈空间影响。

6.3.2.3、pmap查看进程内存地址空间

可以结合pmap和nmt得到内存地址空间,分析堆外占用情况了,接下来需要做的就是分析堆外内存的内容了。

pmap -x pid | sort xx
6.3.2.4、gdb dump查看内存空间内容
(gdb) dump binary memory ./file BEGIN_ADDRESS END_ADDRESS

将内存内容dump到文件中,就可以查看到文件中的内容了。但是这种方式不直观,所以可以使用gperf工具。它是google的,使用gperf2.5即可(blog.csdn.net/unix21/arti…) 另外一个注意点就是虽然heap文件只有1M,但是可以分析出堆外内存的大小。不过在实际使用过程中,gperf并没有分析出实际的堆外内存情况,通过pmap可以看出堆外内存占用有几个G,但是gperf始终只有200M。

6.4、堆外内存的优缺点

优势 :

  1. 能够很方便的自主开辟很大的内存空间,对大内存的伸缩性很好
  2. 减小垃圾回收带来的系统停顿时间
  3. 直接受操做系统控制,能够直接被其余进程和设备访问,减小了本来从虚拟机复制的过程
  4. 特别适合那些分配次数少,读写操做很频繁的场景

缺点 :

  1. 容易出现内存泄漏,而且很难排查
  2. 堆外内存的数据结构不直观,当存储结构复杂的对象时,会浪费大量的时间对其进行串行化。

6.5、堆外内存与堆内内存联系

虽然堆外内存自己不受垃圾回收算法的管辖,可是由于其是由ByteBuffer所创造出来的,所以这个buffer自身做为一个实例化的对象,其自身的信息(例如堆外内存在主存中的起始地址等信息)必须存储在堆内内存中,具体状况以下图所示。

这里写图片描述

当在堆内内存中存放的buffer对象实例被垃圾回收算法回收掉的时候,这个buffer对应的堆外内存区域同时也就被释放掉了。

6.6、总结

从实践经验来看,除了java堆和永久代之外,我们注意到下面这些区域也会占用较多的内存,这里所有的内存总和会受到操作系统进程最大内存的限制:

  1. Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct buffer memory。
  2. 线程堆栈:可通过-Xss调整大小内存不足时抛出StackoverflowErroe(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。
  3. Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB的内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。
  4. JNI代码:如果代码中使用JNI调用本地库,那么本地库使用内存也不在堆中
  5. 虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。