JDK1.8环境
public class JVMDemo {
public static void main(String[] args) {
/*
JVM中相关的参数:
-Xms 初始堆内存大小,默认为物理内存的1/64 12G时,196M
-Xmx 最大分配的堆内存,默认为物理内存的1/4 12G时,3136M
-XX:+PrintGCDetails 输出详细的GC处理日志
*/
// 获取Runtime实例
Runtime runtime = Runtime.getRuntime();
// 获取默认最大分配内存
long maxMemory = runtime.maxMemory(); // 单位:字节
// 获取JVM中的内存问题
long totalMemory = runtime.totalMemory(); // 单位:字节
// 打印
System.out.println("maxMemory = " + maxMemory / (1024.0 * 1024) + "MB");
System.out.println("totalMemory = " + totalMemory / (1024.0 * 1024) + "MB");
// 不断产生垃圾
String str = "GCGCGGC";
while (true) {
str += str + new Random().nextInt(888888) + new Random().nextInt(999999);
}
}
}
运行环境的设置如下:
// Metaspace 元数据区
// 本机物理内存:12G
JVM参数:-Xmx1024m -Xms1024m -XX:+PrintGCDetails
-Xmx: JVM的初始堆内存,默认为物理内存的1/64
-Xms: 最大分配的堆内存,默认为物理内存的1/4
-Xmx1024m -Xms1024m -XX:+PrintGCDetails
在IDEA的VM Options中配置:
运行结果如下:
maxMemory = 981.5MB
totalMemory = 981.5MB
[GC (Allocation Failure) [PSYoungGen: 221069K->35670K(305664K)] 221069K->87828K(1005056K), 0.0250783 secs] [Times: user=0.05 sys=0.03, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 249327K->35606K(305664K)] 440552K->226831K(1005056K), 0.0150943 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 185038K->776K(305664K)] 793463K->678734K(1005056K), 0.0275678 secs] [Times: user=0.16 sys=0.02, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 776K->0K(305664K)] [ParOldGen: 677958K->348374K(699392K)] 678734K->348374K(1005056K), [Metaspace: 3499K->3499K(1056768K)], 0.0463971 secs] [Times: user=0.31 sys=0.03, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 144231K->32K(305664K)] 631672K->626540K(1005056K), 0.0182470 secs] [Times: user=0.20 sys=0.00, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(305664K)] [ParOldGen: 626508K->278841K(699392K)] 626540K->278841K(1005056K), [Metaspace: 3499K->3499K(1056768K)], 0.0343299 secs] [Times: user=0.20 sys=0.00, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(262656K)] 556975K->556975K(962048K), 0.0018310 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(262656K)] [ParOldGen: 556975K->417908K(699392K)] 556975K->417908K(962048K), [Metaspace: 3499K->3499K(1056768K)], 0.0489941 secs] [Times: user=0.33 sys=0.00, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(302592K)] 417908K->417908K(1001984K), 0.0012516 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(302592K)] [ParOldGen: 417908K->417888K(699392K)] 417908K->417888K(1001984K), [Metaspace: 3499K->3499K(1056768K)], 0.0564153 secs] [Times: user=0.33 sys=0.00, real=0.06 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at cn.yeats.JVMDemo.main(JVMDemo.java:35)
Heap
PSYoungGen total 302592K, used 7584K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 254464K, 2% used [0x00000000eab00000,0x00000000eb268250,0x00000000fa380000)
from space 48128K, 0% used [0x00000000fd100000,0x00000000fd100000,0x0000000100000000)
to space 46592K, 0% used [0x00000000fa380000,0x00000000fa380000,0x00000000fd100000)
ParOldGen total 699392K, used 417888K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 59% used [0x00000000c0000000,0x00000000d98181f8,0x00000000eab00000)
Metaspace used 3530K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 386K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 1
注意从“Heap”往下的部分!
通常有以下原因:
JVM堆内存设置不足
-Xms与-Xmx设置太小
程序中创建了大量对象,并且长时间没有被回收(驻留在堆内存中)
这里的对象指:GC链能够到达GC ROOTS顶点的对象
,也就是活跃对象,有用的对象
。并非指无用的垃圾对象。
-Xms JVM堆内存初始大小,默认为物理内存的1/64。
-Xmx JVM最大堆内存,默认为物理内存的1/4。
最大堆内存,通常会设置为物理的 `1/2~3/4`,但还是要根据实际情况而定。
注意:
什么样的对象,能够经过15轮GC过程,还依然存活在JVM堆内存中?通常就是些 池对象
存活
是指对于GC ROOTS顶点集,其GC链仍然可达。
JVM内存不足时,就需要执行垃圾清理过程,即GC过程。
以JDK1.7为例。
GC过程主要针对的是JVM堆内存,对于JDK1.7的Perm区(JDK1.8的Metaspace区)几乎不存在GC过程。
商业版的JVM,各区域的内存大小:
Eden : S0 : S1 = 8 : 1 : 1
JVM规范中,规定了Eden区与Survior区域大小是1:1,并且要求S0:S1=1:1
GC过程:
新生对象,放入Eden区
注意:通常来说,新生对象是存入Eden区的,但是如果生成的对象是大对象
,对象的描述最终是一个字符串,大对象就是指这一串字符的长度比较长,如几千。(QQQQ: 具体的数字,还有待探究!)
Eden区内存不足时,开始执行GC过程(复制清除算法,MinorGC)
把Eden区中有用的对象向S0区复制,然后将Eden区中留下的无用对象全部清除。
S0区内存不足时,执行GC过程(复制清除算法)
把S0区中有用的对象向S1区复制,然后将S0区中留下的无用对象全部清除。
S1区内存不足时,执行GC过程(复制清除算法)
此时,进入S0区与S1区间的切换阶段。
把S1区中有用的对象向S0区复制,然后将S1区中留下的无用对象全部清除。
之后,如果存在S0或者S1区的内存不足时,就针对这两个区域执行GC过程(复制清除算法)。
当对象年龄
达到15
时,如果需要再次切换区域,则将对象称入到年老区
。
full GC
过程,清理年老区。可以参考一下Obejct类中的finalize()方法的注释:
/**Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* A subclass overrides the {@code finalize} method to dispose of
* system resources or to perform other cleanup.
(1) 引用计数算法; (2)可达性算法
JDK1.2及之前,可以认为“没有引用指向的对象,是垃圾”。 采用的是“引用计数算法”
引用计数算法
给每个对象定义一个引用计数器count,当有一个引用指向该对象时,count就加1,当有一个引用不再指向该对象时,count减1。最终判断该对象是否为"垃圾",只需判断其引用计数器count是否大于0。
引用计数算法,在循环引用
中会有问题:明明一个对象已经是”垃圾“了(比如:obj = null;),但它的”引用计算器“可能不为0,这时JVM就不会将它当作垃圾进行回收。
// 循环引用的代码示例
public class Demo{
public static void main(String[] args){
// 定义一个对象,默认为null
public Demo instance;
// 创建两个对象
Demo d1 = new Demo();
Demo d2 = new Demo();
// 循环引用
d1.instance = d2; // d1指向d2
d2.instance = d1; // d2指向d1
// 设置对象为空,即置其为"垃圾"
d1 = null;
d2 = null;
}
}
其内存图:
JDK1.2之后,”没有引用可达的对象,是垃圾“。 采用的是”可达性算法“
由JVM选择 GC ROOTS(GC根节点,多个)顶点,其他对象去指向GC ROOTS顶点,如果能够到达GC ROOTS顶点,该对象就不是"垃圾",否则就是"垃圾"。
两个概念:
GC ROOTS GC顶点
什么样的内容,可以作为”GC ROOTS顶点“?(《深入理解JAVA虚拟机》P65)
1. 虚拟机栈(栈桢中本地变量表所指向的对象)
2. 方法区中类的静态属性所指向的对象
3. 方法区的常量
4. native方法中引用的对象
GC链
从对象出发,指向GC ROOTS的路由,称为”GC链“。
举例:还是以上面的代码为例
如果只断掉指向d1节点与d2节点其中的一条路,那么两个对象因为存在有循环引用,它们仍然都可以到达GC ROOTS。
按引用力度由高到低: 强引用 -> 软引用 -> 弱引用 -> 虚引用
Q1: 什么叫“引用力度”?
Q2: 四种引用,各自是什么意思?
强引用 Strong Reference
就是指普通的对象引用,如:Person p = new Person();
软引用 Soft Reference
当JVM内存足够时,该对象不会受到任何影响,发生GC过程,它也不会受到影响;
当JVM内存不足时,这个对象就会被自动回收。
弱引用 Weak Reference
只要发生GC过程,无论JVM内存是否充足,该对象都会被回收。
虚引用 Phantom Reference
为每一个对象设置虚引用的唯一目的就是能在这个对象被GC回收时收到一个系统通知,告知程序,此对象已被Garbage Collector回收了。
标记-清除
算法算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:
复制-清除
算法为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
标记-整理
算法根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。
当前虚拟机的垃圾手机都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清楚”或“标记-整理”算法进行垃圾收集。
延伸面试问题: HotSpot为什么要分为新生代和老年代?
根据上面的对分代收集算法的介绍回答。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非了挑选出一个最好的收集器。因为知道现在位置还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?
-XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:
G1收集器的运作大致分为以下几个步骤:
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
合**:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
G1收集器的运作大致分为以下几个步骤:
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。