JVM浅的不能再浅析

303 阅读16分钟

若不说明,都是在jdk1.8环境下

jvm内存结构

pic1.png

按是否共享

  • 线程共享数据区:堆、方法区
  • 线程隔离数据区:程序计数器、本地方法栈、虚拟机栈

程序计数器:可以理解为当前线程所执行字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)

虚拟机栈本地方法栈:每个java方法在执行时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接等信息。只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

:存放对象实例

方法区1:它用于存储已被虚拟机加载的类型信息、常量2、静态变量3、即时编译器编译后的代码缓存等数据(《Java虚拟机规范》说是堆的一个逻辑分区,暂且认为他是放在和堆一个地方)

运行时常量池:属于方法区的一部分,存放编译后生成的各种字面量和符号引用(这也是class文件常量池的内容)

字符串常量池:jdk1.7之前属于方法区(永久代),之后到了堆。(有一个有意思的情况;String.intern(),作用将首次遇到的字符串实例复制到字符串常量池中,返回引用;在1.6中,会复制一份堆的实例到常量池中,在1.7中直接将堆中的引用记到常量池中)

jvm参数配置

-Xms20M // 初始堆大小
-Xmx20M // 最大堆大小
-Xmn10M // 年轻代大小
-XX:SurvivorRatio=8 // 设置eden比例 默认8:1:1, 4 就是 4:1:1
-XX:+PrintGCDetails // 打印详细的gc日志
-verbose:gc
-XX:+PrintHeapAtGC // 打印gc前后的日志

gc日志的分析

[GC (Allocation Failure) [PSYoungGen: 2813K->624K(4096K)] 4869K->4728K(19456K), 0.0015969 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 790K->0K(9216K)] [ParOldGen: 5128K->5794K(10240K)] 5919K->5794K(19456K), [Metaspace: 3308K->3308K(1056768K)], 0.0139242 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

GC: minor gc,Allocation Failure:eden去分配内存失败触发,[PSYoungGen: 2813K->624K(4096K)]:年轻代gc前2813K,gc后624K,总大小4096K;4869K->4728K(19456K):gc前堆大小,gc后堆大小,堆总大小
Full GC :整个大gc,Ergonomics 调优,提高效率而触发

Heap
 PSYoungGen      total 9216K, used 1106K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 13% used [0x00000007bf600000,0x00000007bf714930,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 5793K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 56% used [0x00000007bec00000,0x00000007bf1a87f0,0x00000007bf600000)
 Metaspace       used 3313K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 365K, capacity 388K, committed 512K, reserved 1048576K

PSYoungGen 年轻代,使用 Parallel Scavenge收集器, 又分eden,from, to,from,to又称Survivor0,Survivor1; 上面的输出表示,PSYoungGen大小9216k,已使用1106k,eden大小8192k,使用了13%,0x00000是16进制内存地址

ParOldGen 老年代,一块相对较大的内存 上面的输出表示,ParOldGen大小10240k,已使用56%

Metaspace 元空间,1.8新增,存在本地内存

各区域垃圾回收算法

如何判断对象是否可回收?

有2种方法

  1. 对象引用计数器 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。【java未使用】
  2. 可达性算法 通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的

eden区 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。 效率不稳定 碎片化严重 需要停顿用户线程来标记清理可回收对象【stop the world】

survivor区 标记-复制算法:它将可用内存按容量划分为大小相等的两块(所以有from和to),每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 空间浪费一半 对象存活很高时,效率降低(原因是需要进行较多的复制)
循环一定次数还存活的对象可进入老年代

老年代 标记-整理算法:标记过程同“标记-清理”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

gc 回收流程

minor gc 和 full gc

对象创建分配在eden区,当eden区满会触发minor gc 过程如下:

  1. 把存活对象拷贝到survivor中一个区,eden区清空,如果大于survivor,会直接存到老年代这里是有判断的是否大于老年代剩余大小,否则full gc)
  2. 如果survivor满了,把存活的对象复制到另一个survivor,清空之前的,如果大于survivor,会直接存到老年代(这里是有判断的是否大于老年代剩余大小,否则full gc)
  3. 当再次gc时,扫描eden和survivorfrom存活的复制到survivorto,将eden和survivorfrom清空,gc时如此反复,直到对象年龄到了15还存活搬到老年代。
  4. survivorto与survivorfrom互换,即survivorto变成下次拷贝的区域

full gc 的条件

  1. 当老年代内存满了触发
  2. 调用system.gc();
  3. minor gc 有对象移到老年代,且大于老年代剩余空间

oom的情况

  1. full gc后仍没有空间存放对象

过程如下: Full GC清理整个heap区,包括Yong区和Tenured区 通过标记-整理算法清除死掉的内存

引用网上的一幅图(侵删)

gc.png

另外担保机制在JDK1.5以及之前版本中默认是关闭的,需要通过HandlePromotionFailure手动指定,JDK1.6之后就默认开启。这里我们使用的是JDK1.8,所以不用再手动去开启担保机制。

实操

虚拟机参数配置

-Xms20M
-Xmx20M
-Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-verbose:gc
-XX:+PrintHeapAtGC

我们配置堆大小20M,新生代10M,eden:from: to = 8 : 1 : 1

空载运行(空跑main方法)

Heap
PSYoungGen      total 9216K, used 2859K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 34% used [0x00000007bf600000,0x00000007bf8cacb0,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen       total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
Metaspace       used 3163K, capacity 4496K, committed 4864K, reserved 1056768K
class space    used 346K, capacity 388K, committed 512K, reserved 1048576K

这里已经使用了2859K了

触发gc

代码

    int size = 1024 * 1024;
    System.out.println("0000-2M");
    byte[] temp1 = new byte[2 * size];
    System.out.println("1111-2M");
    byte[] temp2 = new byte[2 * size];
    System.out.println("2222-1M");
    byte[] temp3 = new byte[size];
    System.out.println("3333-1M");
    byte[] temp4 = new byte[size]; // 触发gc

日志

0000-2M
1111-2M
2222-1M
3333-1M
{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 9216K, used 8182K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 99% used [0x00000007bf600000,0x00000007bfdfdb80,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K
[GC (Allocation Failure) [PSYoungGen: 8182K->758K(9216K)] 8182K->5887K(19456K), 0.0039671 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 9216K, used 758K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfe00000)
  from space 1024K, 74% used [0x00000007bfe00000,0x00000007bfebdbf0,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 5128K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 50% used [0x00000007bec00000,0x00000007bf102030,0x00000007bf600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K
}
{Heap before GC invocations=2 (full 1):
 PSYoungGen      total 9216K, used 758K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfe00000)
  from space 1024K, 74% used [0x00000007bfe00000,0x00000007bfebdbf0,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 5128K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 50% used [0x00000007bec00000,0x00000007bf102030,0x00000007bf600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K
[Full GC (Ergonomics) [PSYoungGen: 758K->0K(9216K)] [ParOldGen: 5128K->5767K(10240K)] 5887K->5767K(19456K), [Metaspace: 3262K->3262K(1056768K)], 0.0108672 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 9216K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 5767K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 56% used [0x00000007bec00000,0x00000007bf1a1e80,0x00000007bf600000)
 Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K
}
Heap
 PSYoungGen      total 9216K, used 1190K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 14% used [0x00000007bf600000,0x00000007bf729918,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 5767K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 56% used [0x00000007bec00000,0x00000007bf1a1e80,0x00000007bf600000)
 Metaspace       used 3299K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K
-edenfromto新生代老年代回收
第一次minor gc前8182K008182K0-
第一次minor gc后/3333-1M074%0758K5128K2295K
full gc后00005767K119K
最终1190K001190K5767K-

0000-2222没有gc,直到3333创建,eden空间不足,触发minor gc检查机制,新生代大小大于老年代剩余大小 或者 历次新生代平均大小小于老年代剩余大小,直接minor gc,发现还有5.8M左右,to放不下,判断老年代剩余大小10M大于,存入老年代。
之后发生full gc ? 这里存疑?// TODO
full gc 后把3333-1M存入eden区,完成。

参考文献

  1. 深入理解java虚拟机-周志明
  2. 图解GC流程
  3. Full GC (Ergonomics) 原因和实验

Footnotes

  1. jdk1.7及之前hotspot虚拟机对方法区的实现为永久代,jdk1.7把字符串常量池、静态变量等从方法区移除;jdk1.8移除了永久代,用本地内存实现的元空间代替,也就是对方法区的实现为元空间

  2. jdk1.7之前方法区常量包括字符串常量池、运行时常量池、;jdk1.7把字符串常量池移到堆中。

  3. jdk1.7之后静态变量从方法区移除