JVM 内存管理

JVM 内存管理

参考文档

1. 堆和栈

JVM内存划分:

  1. 寄存器(程序计数器PC拿到指令地址,放入指令寄存器IR中,cpu执行指令)
  2. 本地方法区(本地方法栈):主要是调用native方法
  3. 方法区:记录类的信息,常量等

    这个方法区在jdk8之前是叫永久代,存储一些类的信息,方法的信息等类似元数据信息一样的东西,如果程序加载的类过多,也可能会导致OOM。JDK8 把这个区域挪出来,不再保存在虚拟机中,而是本地内存,叫做元空间,这样就不局域于jvm可使用的系统内存。

  4. 栈内存(stack)
    • 由编译器自动分配,存放函数的参数值,局部变量的值(定义在方法中的都是局部变量,方法外的是全局变量,for循环内部也是局部变量)
    • 先加载函数才能进行局部变量的定义,所以方法先进栈,再定义变量,变量离开作用域后释放,生命周期都很短
  5. 堆内存(heap,不是数据结构中的堆)
    • 由程序猿分配释放,如果程序猿不释放,程序结束时由GC回收
    • 存储的是数组和对象,凡是new的都在堆中,实体(对象)里面封装了多个数据,一个数据消失,实体不会消失,还可以用,所以堆不会随时释放,会由GC不定时回收

int[] arr = new int[3];
主函数进栈 -> 在栈中定义一个 arr 变量 -> 在堆里通过new开辟一个空间,这个空间会产生一个地址,这个地址下的所有所有会进行初始化 -> 把内存的地址赋值给 arr
int[] arr = null; arr不做任何指向,null的作用就是取消引用数据类型的指向

1.1 堆

堆又分为

  • 新生代
    • 新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成
    • 新建的对象都是由新生代分配内存
    • 新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例
  • 老年代
    • 用于存放新生代中经过多次垃圾回收仍然存活的对象

堆结构

堆结构

1.2 栈

每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果

当一个方法执行结束的时候,方法会出栈,方法内部的变量也会跟着出栈,当局部变量出栈之后,堆中new出来的内存空间就会变的不可达,当GC回收的时候就会去回收那些不可达的区域

2. GC

文档1
文档2
文档3

怎么确定哪些对象应该回收?引用计数方法和可达性分析
什么时候回收?cpu空闲自动回收;堆内存满了;主动调用 system.gc()
如何回收?四种垃圾回收算法

2.1 垃圾回收算法

  1. 引用计数法:有引用就+1,引用失效就-1,等于0的时候清除。缺点:很难解决对象之间互相循环引用的问题。
  2. 标记清除法:先标记哪一些需要回收,再清除被标记的对象。缺点:容易产生内存碎片,碎片太多的话后续过程中为大对象分配空间时无法找到足够的空间从而会提前触发一次垃圾回收动作
  3. 复制算法:两部分内存,每次只使用一块内存。回收时将a1内存中还存活的对象复制到a2,然后清空整个a1内存。优点:在复制过去的时候相当于有一次整理的动作,不会产生内存碎片,运行高效。缺点:会使可用内存缩减到原来的一半。
  4. 标记整理法:先标记,然后把存货的对象移动到一端整理,最后清理掉端边界以外的内存。优点:不产生内存碎片。缺点:成本高

2.2 新生代和老年代的GC算法

  1. 新生代GC
    新生代通常存活时间较短,因此基于复制算法来进行回收。新生代内存空间划分为 Eden + S1 + S2(8:1:1),当进行回收的时候,把Eden和S1存活的对象复制到S2上,然后清理掉Eden和S1。
  2. 老年代GC
    老年代对象存活的时间比较长,比较稳定。因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗

2.3 如何是老年代?

  1. 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden和S1S2区产生大量的内存拷贝
  2. 长期存活的对象进入老年代。对象每经过一次YGC年龄都会+1,默认阈值是15,达到阈值的对象进入老年代。阈值可以通过 -XX:MaxTenuringThreshold 设置
  3. 动态判断对象的年龄。如果Survivor区中存在相同年龄的对象,年龄从小到大进行累加,当加入某个年龄的对象后,累加和超过了Survivor区域大小* TargetSurvivorRatio(默认50%)的时候,从这个年龄往上的所有对象都会挪到老年代。例:年龄1占了33%,年龄2占了33%,年龄3占了34%,因为年龄1+年龄2累加超过了50%,那么年龄2和年龄3都挪到老年代。

2.4 什么时候触发GC?

  1. 新对象生成,并在Eden申请空间失败时,会触发minorGC即YGC
  2. 老年代被写满;持久代被写满;system.gc() 被显示调用;会触发full gc。

2.5 垃圾回收器

垃圾回收器

  1. serial
    采取复制算法,用于新生代,单线程收集器,所以在他工作时会产生StopTheWorld。单线程情况下效率更高,比如用于GUI小程序
    可以与CMS垃圾回收器一起搭配工作
  2. ParNew
    采取复制算法,用于新生代,是Serial的多线程版本,多个GC线程同时工作,但是也会产生StopTheWorld,因此不能和工作线程并行。
    可以与CMS垃圾回收器一起搭配工作
  3. Parallel Scavenge
    采取复制算法,用于新生代,和ParNew一样,所以也会产生STW,多线程收集器,他是吞吐量优先的收集器,提供了很多参数来调节吞吐量。
    可以与 Serial Old , Parallel Old 垃圾回收器一起搭配工作
  4. Serial Old
    采取标记整理算法,用于老年代,单线程收集器,所以在他工作时会产生StopTheWorld。单线程情况下效率更高,比如用于GUI小程序
  5. Parallel Old
    采取标记整理算法,用于老年代,Parallel Scavenge收集器的老年代版本,吞吐量优先。
  6. CMS
    采取标记清除算法,老年代并行收集器,号称以最短STW时间为目标的收集器,并发高、停顿低、STW时间短的优点。主流垃圾收集器之一。
  7. G1
    采取标记整理算法,并行收集器。对比CMS的好处之一就是不会产生内存碎片,此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。而且他的STW停顿时间是可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。

新生代都会产生 STW,老年代可能更倾向于 CMS

2.6 CMS 回收器

采用标记清除,老年代并行收集器。分为四个阶段

  1. 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。所以此阶段会产生STW,但时间很短。
  2. 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
  3. 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
  4. 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。

    会产生两次 STW

CMS

优点:

  1. 并发高
  2. 停顿短
  3. STW时间短

缺点:

  1. 对cpu资源敏感,并发标记会和用户进程一起占用cpu资源,竞争激烈的话会导致程序变慢
  2. 会产生浮动垃圾碎片,在最后一个阶段因为用户进程还在执行,这时候产生的垃圾只能下次再处理
  3. 内存碎片问题(因为是标记清除算法)。当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

针对这个内存碎片,有这个开关参数 -XX:+UseCMSCompactAtFullCollection 作用是在full gc 后会做一次碎片整理,但是会导致停顿时间增长,此时就需要另一个参数 -XX:CMSFullGCsBeforeCompaction 这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的

CMS 不等于Full GC,我们可以看到CMS分为多个阶段,只有stop the world的阶段被计算到了Full GC的次数和时间,而和业务线程并发的GC的次数和时间则不被认为是Full GC。CMS主要可以分为initial mark(stop the world), concurrent mark, remark(stop the world), concurrent sweep几个阶段,其中initial mark和remark会stop the world。

Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC。

2.7 G1 回收器

采用标记整理,是针对整个堆的垃圾回收器。分为四个阶段

  1. 初始标记:仅仅标记GCRoots能直接关联到的对象,且修改TAMS的值让下一阶段用户程序并发运行时能正确可用的Region中创建的新对象。速度很快,会STW。
  2. 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。不会STW。
  3. 最终标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。STW时间会比第一阶段稍微长点,但是远比并发标记短,效率也很高。
  4. 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

特点:

  • 并行与并发执行:利用多CPU的优势来缩短STW时间,在GC工作的时候,用户线程可以并行执行。
  • 分代收集:无需其他收集器配合,自己G1会进行分代收集。
  • 空间整合:不会像CMS那样产生内存碎片。
  • 可预测的停顿:可以手动控制一个长度为M毫秒的时间片段(可以用JVM参数 -XX:MaxGCPauseMillis指定),设置完后垃圾收集的时长不得超过这个(近实时)。

原理:
G1并不是简单的把堆内存分为新生代和老年代两部分,而是把整个堆划分为多个大小相等的独立区域(Region),新生代和老年代也是一部分不需要连续Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

3. JVM 调优

  1. 一个程序默认的最大堆大小是系统内存的1/4,最小堆大小是系统内存的1/64

    free -h 查看服务器内存大小
    jcmd pid help 查看可选的option列表
    jcmd pid VM.flags 查看当前进程的jvm参数信息
    -XX:MaxHeapSize=memory的四分之一

  2. 如果一个服务器内存32G,运行了100个程序,每一个程序的最大堆大小都是8G,最小堆

  3. 一般什么时候去调整 JVM 参数

  • 需要大的吞吐量
  • 需要低时延
    总结来说,原因就是需要快速的响应需求接口,但是如果 GC 的时间特别长,那么就需要去调整堆大小、年轻代大小、老年代大小
  1. 查看 GC 信息
    jstat -gc pid 查看进程在此刻的GC相关信息,主要看最后几个参数
    YGC: 次数; YGCT: 时间(秒); FGC: 次数; FGCT: 时间(秒); GCT: 等于前面两个时间之和

  2. 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
    => 所以,一般最小堆大小和最大堆大小设置为相等,避免每次GC后要去调整堆的大小

3.1 一些JVM参数

  1. 堆设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -Xms:初始(最小)堆大小
    -Xmx:最大堆大小
    -Xmn:新生代大小 默认是最小堆的1/3,即年轻代和老年代的比例默认是 1:2
    -XX:NewRatio:设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
    -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:为3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5

    -XX:MaxTenuringThreshold:设置转入老年代的存活次数。如果是0,则直接跳过新生代进入老年代
    -XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
    -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)
  2. 收集器设置

    1
    2
    3
    4
    5
    6
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParNewGC

    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行老年代收集器
    -XX:+UseConcMarkSweepGC:设置并发收集器
  3. 垃圾回收统计信息

    1
    2
    3
    4
    5
    6
    7
    8
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps 打印从启动到发生gc这段时间的一个秒数
    -XX:+PrintGCDateStamps 会打印详细的时间信息
    -Xloggc:filename
    -XX:GCLogFileSize=256M
    -XX:+UseGCLogFileRotation
    -XX:NumberOfGCLogFiles=5
  4. 并行收集器设置

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

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

3.3 栈溢出

栈溢出一般是因为存在循环递归的情况,有几种优化

  1. -Xss 这是每个栈可使用的内存大小,即栈大小,如果这个值过小,可能导致栈溢出;如果过大,可能会影响到创建栈的数量,在多线程的情况下,依旧可能溢出
  2. 递归的优化 => 尾递归,参考斐波那契的写法。程序会自动优化尾递归,每次循环最后一个栈帧会被替换,而不会多开辟空间。需要有辅助记录的变量。相当于在执行到 base case 的时候,之前的答案也传递过来了,那么就直接 return 就行
1
2
3
4
5
6
7
8
9
10
public int F(int n)
{
return n < 2 ? 1 : F(n - 1) + F(n - 2);
}

优化后
public static int F(int n,int a1,int a2)
{
return n == 0 ? a1 : F(n - 1, a2, a1 + a2);
}

3.4 HBase JVM

regionServer 向 zk 发送心跳,zk有一个maxSessionTimeout,client自己也会传一个时间,不过zk本身会去做一个计算,来得到一个心跳时间,如果在心跳时间内 rs 没有发送心跳,那么就会认为 rs 下线了。

rs 下线前是在 full gc,而时间过长,导致gc时间超过了心跳时间。

24G
-Xmn配置6G, 4-5s一次 minor gc, 耗时300-400ms
-Xmn配置2G, 2s左右一次 minor gc, 耗时110ms左右
-Xmn配置1G, 1s左右一次或两次 minor gc, 耗时70ms左右

还有一种情况,接口获取的大多是明细数据,会导致数据存放在blockcache中,在数据量大的时候,同时去访问,会导致内存不够。此时需要去调大堆大小。

blockcache 有两种策略,堆内LruBlockCache和堆外BucketCache

3.5 Java 程序

memory 32G
程序 堆最大8g 最小512M
9521次 146.591秒 60次/秒
-Xmn配置1G, 1s左右一次或两次 minor gc, 耗时70ms左右

memory 16G
程序 堆最大4g 最小256M
1294次 9.388秒 138次/秒 刚运行没多久就这么多次

2g
357次 3.383秒 1天时间内回收了357次,full gc出现2次

-Xmn6g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=15 -XX:CMSInitiatingOccupancyFraction=70 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data1/log/hbase/gc-hbase.log -XX:ReservedCodeCacheSize=256m -XX:GCLogFileSize=256M -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5

nohup java -Xms2048m -Xmx2048m -Xmn712m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/data/label_engine/logs/gc.log -XX:GCLogFileSize=256M -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -jar label-0.0.1-SNAPSHOT.jar –server.port=8082 > /dev/null 2>&1 &