Skip to content

Instantly share code, notes, and snippets.

@zhanzhenchao
Last active June 2, 2020 08:13
Show Gist options
  • Save zhanzhenchao/943dbec269c3f5f46e09 to your computer and use it in GitHub Desktop.
Save zhanzhenchao/943dbec269c3f5f46e09 to your computer and use it in GitHub Desktop.
jvm

Java内存分布

不同的年代采用不同的垃圾回收算法,使之有效。

年轻代采用“Mark-Copy”算法进行垃圾回收

  • Eden区满了,会触发minor gc,所有存活的对象会拷贝到Survivor区域,年龄增加“1”,然后清空Eden区。

  • 再次触发minor gc,会将Eden和Survivor 1 区域中存活的对象移到Survivor 2中,年龄加”1”,然后清空Eden和Survivor 1 区域。

  • 再次触发minor gc,年龄最够大(JVM设置)机会升级到Tenured区域。

老年代Tenured区域上的垃圾回收

  1. 再次触发minor gc,又有对象升级到Tenured区域,但是Tenured区域没有空间容纳新的对象,就会触发年老代上的垃圾回收,称为major gc。

  2. major gc取决于使用哪种垃圾回收算法:

  • Parallel Scavenge(PS)
  • Concurrent Mark Sweep(CMS)

Parallel Scavenge(PS)

采用标记-清除算法和标记-压缩算法:

  • 发生major gc后,会采用标记-清除算法回收所有非可到达对象。

  • 因为步骤1容易产生内存碎片,所以再采用标记-压缩算法来消除内存碎片。

Concurrent Mark Sweep(CMS)

采用标记-清除算法(与PS不同,有4个阶段),应用程序是可以并行运行的:

  1. Initial Mark 阶段 - 短暂的Stop-The-World,标记根对象的第一层孩子节点。

  2. Concurrent Mark 阶段 - 以标记的节点为根对象,重新开始标记Tenured区域的可达对象。程序在此阶段是继续运行的。

  3. Remark 阶段 - 短暂的Stop-The-World,确保新加入Tenured对象有被标记到,采用多线程加速。

  4. Concurrent Sweep阶段 - 清除。

PS和CMS的对比


Garbage First(G1)垃圾收集器

但是很不幸的是,CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是由于它没有Compact阶段,它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器 - G1垃圾收集器。它会在未来逐步替换掉CMS垃圾收集器。

G1垃圾收集器和CMS垃圾收集器有几点不同。首先,最大的不同是内存的组织方式变了。Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region - 每个region从1M到32M不等。

一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。

在G1垃圾收集器中,年轻代的垃圾回收过程跟PS垃圾收集器和CMS垃圾收集器差不多,新对象的分配还是在Eden region中,当所有Eden region的大小超过某个值时,触发minor gc,回收Eden region和Survivor region上的非可达对象,同时升级存活的可达对象到对应的Survivor region和Tenured region上。对象从Survivor region升级到Tenured region依然是取决于对象的年龄。

对于年老代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:

  1. Initial Mark阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。

  2. Concurrent Mark阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,那就是,如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。

  3. Remark阶段 - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。

  4. Clean up/Copy阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:

从上可以看到,由于Initial Mark阶段和Clean up/Copy阶段都是跟minor gc同时发生的,相比于CMS,G1暂停应用程序的时间更少,从而提高了垃圾回收的效率。

Java内存分布

观察JVM的使用率以及极限容量,可以看出PC,S0C+S1C+EC, OC会不会总是使用率很高,并且容量以及使用量是否总是接近极限容量(意味着总是GC)。如果是这样的话,就应该考虑增加硬件,修改JVM参数或者调整代码的质量。注意EU在Web应用总是缓慢增长和容器实时创建对象有关。以下是配置JVM的参数:

堆内存分配 = (S0C + S1C + EC)2/3 + old1/3

Name Explanation Default
-Xms 初始分配的堆内存 1/64
-Xmx 最大分配的堆内存 1/4
  • 空余堆内存小于40%时,JVM就会增大到-Xmx的最大限制
  • 空余堆内存大于70%时,JVM就会减少到-Xms的最小限制
  • young(2/3),old(1/3) 因此服务器一般设置-Xms和-Xmx相等,避免每次GC后调整堆的大小。如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。

非堆内存分配 = PC + Code cache

Name Explanation Default
-XX:PermSize 初始分配的非堆内存 1/64
-XX:MaxPermSize 最大分配的非堆内存 1/4
  • -XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。因为GC并不会在主程序运行的时候对PermGen space进行清理,如果是动态加载了很多Class,就很可能会出现PermGen space错误,这种错误常见在web服务器对JSP进行pre compile的时候,或者web服务器大量使用了第三方的jar包。

JVM内存限制 设置JVM的内存限制,和操作系统有关,具体要看多少位的系统预留多少内存给应用,当然如果是64bit的系统,暂时可以不用考虑系统的影响,只需要考虑内存的大小。另外如果有双核的CPU,可以使用-XX:+UseParallelGC参数,这个参数可以使GC的速度更加快。

jstat

接下来可以使用jstat来观察应用使用JVM时的情况。以下命令都是实时监控JVM状态:


查看JVM加载状况(很少用)

jstat -class pid
Column Description
Loaded 有多少个类加载
Bytes 有多少K字节加载
Unloaded 有多少个类没有加载
Bytes 有多少字节没有加载
Loaded 加载和未加载过程花费的时间

查看JVM编译状况(很少用)

jstat -compiler pid
Column Description
Compiled 有多少条编译任务
Failed 有多少条编译任务失败
Invalid 有多少条编译任务无效
Time 编译任务所花费的时间
FailedType 最后编译失败的类型
FailedMethod 最后编译失败的类名或方法名

查看容量和使用量(常用),可以查看平时的容量以及使用量情况

jstat -gc pid

Column Description
S0C 生存区S0(FROM)当前容量(KB)
S1C 生存区S1(TO)当前容量(KB)
S0U 生存区S0(FROM)当前使用量(KB)
S1U 生存区S1(TO)当前使用量(KB)
EC Eden区当前容量
EU Eden区当前使用量
OC 年老代当前容量
OU 年老代当前使用量
PC 永久代当前容量
PU 永久代当前使用量
YGC 年轻代GC次数
YGCT 年轻代GC耗时
FGC Full GC次数
FGCT Full GC耗时
GCT GC总耗时

查看容量(常用),查看当前容量,以及极限容量分配是否有问题。

jstat -gccapacity pid
Column Description
NGCMN young的初始化大小(KB)
NGCMX young的最大容量(KB)
NGC young当前容量(KB)
S0C 生存区S0(TO)当前容量(KB)
S1C 生存区S1(TO)当前容量(KB)
EC Eden当前容量
OGCMN Old初始化大小(KB)
OGCMX Old的最大容量(KB)
OGC Old当前新生成容量
OC Old当前容量
PGCMN 永久代初始化容量
PGCMX 永久代最大的容量
PGC 永久代当前新生成容量
PC 永久代当前容量
YGC young GC次数
FGC Full GC次数

查看使用率(常用),以下命令查看在2秒内(打印5次)的JVM使用率情况,可以判断JVM分配是否有问题。注意P的比率一直很高,是因为应用一开就决定了PermGen space的空间,只要不大于设定的极限值就行了,不过要注意如果PermGen space的容量太接近极限值,会产生java.lang.OutOfMemoryError: PermGen space的内存溢出错误。

jstat -gcutil pid 2000 5
Column Description
S0 S0已使用占用当前容量的百分比
S1 S1已使用占用当前容量的百分比
E ...百分比
O ...百分比
P ...百分比
YGC young GC次数
YGCT young GC耗时
FGC Full GC次数
FGCT Full GC耗时
GCT GC总耗时

其他命令

Commond Description
-gcnew pid 查看young的使用率
-gcnewcapacity pid 查看young的容量
-gcold pid 查看Old的使用率
-gcoldcapacity pid 查看Old的容量
-gcpermcapacity pid 查看pg的使用率
-printcompilation pid 查看热点编译class使用情况

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment