视频学习:【狂神说Java】JVM快速入门篇
相关资料:
- 《深入理解Java虚拟机》第三版
- 漫画:什么是JVM的垃圾回收?
- 非常简单易懂,十分推荐
- bugstack虫洞栈——面经#27
- 实例查看回收过程
10. 实例验证GC
老是说GC,如何看到GC运行的实际效果?TALK IS CHEAP,SHOW ME THE CODE
举个例子:(例子来源:bugstack虫洞栈——面经#27)
测试代码:
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存, 以便能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) throws InterruptedException {
testGC();
}
public static void testGC() throws InterruptedException {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC, objA和objB是否能被回收?
//这里我们先采用 jvm 工具指令,jstat来监控。因为监控的过程需要我手敲代码,比较耗时,所以我们在调用testGC()前,睡眠会
// Thread.sleep(10000);
System.gc();
}
}
在启动的程序中,加入GC打印参数,观察GC变化结果。
-XX:+PrintGCDetails #打印每次gc的回收情况 程序运行结束后打印堆空间内存信息(包含内存溢出的情况)
-XX:+PrintHeapAtGC #打印每次gc前后的内存情况
-XX:+PrintGCTimeStamps #打印每次gc的间隔的时间戳 full gc为每次对新生代老年代以及整个空间做统一的回收 系统中应该尽量避免
-XX:+TraceClassLoading #打印类加载情况
-XX:+PrintClassHistogram #打印每个类的实例的内存占用情况
-Xloggc:/Users/xiaofuge/Desktop/logs/log.log #配合上面的使用将上面的日志打印到指定文件
-XX:HeapDumpOnOutOfMemoryError #发生内存溢出将堆信息转存起来 以便分析
-
-XX:+PrintGCDetails
[GC (System.gc()) [PSYoungGen: 6717K->712K(37888K)] 6717K->720K(123904K), 0.0020003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 712K->0K(37888K)] [ParOldGen: 8K->590K(86016K)] 720K->590K(123904K), [Metaspace: 3105K->3105K(1056768K)], 0.0048082 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap PSYoungGen total 37888K, used 983K [0x00000000d6200000, 0x00000000d8c00000, 0x0000000100000000) eden space 32768K, 3% used [0x00000000d6200000,0x00000000d62f5db8,0x00000000d8200000) from space 5120K, 0% used [0x00000000d8200000,0x00000000d8200000,0x00000000d8700000) to space 5120K, 0% used [0x00000000d8700000,0x00000000d8700000,0x00000000d8c00000) ParOldGen total 86016K, used 590K [0x0000000082600000, 0x0000000087a00000, 0x00000000d6200000) obje space 86016K, 0% used [0x0000000082600000,0x0000000082693820,0x0000000087a00000) Metaspace used 3128K, capacity 4496K, committed 4864K, reserved 1056768K class space used 343K, capacity 388K, committed 512K, reserved 1048576K
- 怎么看结果?
- PSYoungGen:GC日志中的PSYoungGen(PS是指Parallel Scavenge)为Eden+FromSpace,而整个YoungGeneration为Eden+FromSpace+ToSpace。
- ParOldGen:ParOldGen表示gc回收前后老年代的内存变化
- MetaSpace:JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
- 从运行结果可以看出内存回收日志,Full GC 进行了回收。
- 也可以看出JVM并不是依赖引用计数器的方式,判断对象是否存活。否则他们就不会被回收啦
- 怎么看结果?
11. GC主要算法
- 如何判断哪些垃圾是需要回收的?
- 有哪些重要的垃圾回收算法?
接下来一个个进行回答:这些问题的关键其实就是有关GC的四大算法
11.1 如何判断哪些垃圾是需要回收的:
引用计数算法
定义:
- 它通过记录对象被引用的次数从而判断该对象的重要程度;
- 如果该对象被其它对象引用,则它的引用计数加一,
- 如果删除对该对象的引用,那么它的引用计数就减一,
- 当该对象的引用计数为0时,那么该对象就会被回收。
评价:
- 从实现来看,引用计数器法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但是它的实现方案简单,判断效率高,是一个不错的算法。
- 也有一些比较出名的引用案例,比如:微软COM(Component Object Model) 技术、使用ActionScript 3的FlashPlayer、 Python语言等。
- 但是,在主流的Java虚拟机中并没有选用引用技术算法来管理内存,主要是因为这个简单的计数方式在处理一些相互依赖、循环引用等就会非常复杂。可能会存在不再使用但又不能回收的内存,造成内存泄漏
- 所以,Java虚拟机采用的是另一种方法来判断对象是否存活,它就是可达性分析算法。
可达性分析算法
Java、C#等主流语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
定义:
- 首先要确定一系列根对象(GC Roots),并从根对象为起点根据对象之间的引用关系搜索出一条引用链(Reference Chain);
- 在引用链的对象就存活,而不在引用链的对象就认定为可回收对象。
程序员小灰举了个十分生动的例子:
有一个比喻十分恰当:可达性分析算法就好比是在清洗葡萄串,我们可以从一根枝提起一大串葡萄,他们就像一串引用链,而没有和引用链相连的对象就像是散落在池子里的葡萄,可以回收。
根对象(GC Roots):
-
全局性引用,对方法区的静态对象、常量对象的引用
- 虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)
- 方法区中类静态属性引用的对象(static关键字声明的字段)
- 方法区中常量引用的对象,(也就是final关键字声明的字段)
-
执行上下文,对 Java方法栈帧中的局部对象引用、对 JNI handles 对象引用
- 本地方法栈中引用的对象(native方法)
- Java虚拟机内部的引用。(系统内部的东西当然能作为根了)
-
已启动且未停止的 Java 线程
11.2 如何进行垃圾回收:
判断了内存种哪些垃圾需要被回收之后接下来就要通知JVM进行垃圾回收了,在此我们会了解到几个重要的回收算法,”标记-*算法“(可以说十分见名之意了)
标记-清除算法(mark-sweep)
如图:简单来说,就是用可达性算法判断出哪些垃圾,并就地释放。
要点:
-
需要注意的是:所谓的清除,并不需要真正地把整个内存的字节进行清零操作,只需要把空闲对象的起始结束地址记录下来放入空闲列表里,表示这段内存是空闲的就行。
-
优缺点:
-
优点:速度快,只需要做个标记就能知道哪一块需要被回收,但是他的缺点也是致命的。
-
缺点
-
一是执行效率不稳定,
-
二是会涉及到内存碎片化的问题,如下图,有空间但是不连续导致无法存入,造成内存浪费
-
-
所谓标记复制算法和标记整理算法,都是对标记清除算法缺点的改进,所以才说标记清除算法是最基础的方式。
标记-整理算法(mark-compact))
如图:简单来说,用可达性算法找出需要回收的垃圾,回收并释放空间后把空间整理为连续存储的内存空间。
要点:
- 1974年,Edward Lueders 提出了标记-压缩算法,标记的过程和标记清除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端移动,然后在清理掉其他内存空间。
- 这种算法能够解决内存碎片化问题,但压缩算法的性能开销也不小。效率就低了。
- 标记-整理算法 不仅可以弥补 标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;
标记-复制算法(mark-copy)
如图:简单来说,用可达性算法找出需要回收的垃圾后,把非垃圾资源连续复制至to空间,实现资源释放。
要点:
- 这种方式是把内存区域分成两份,分别用两个指针 from 和 to 维护,并且只使用 from 指针指向的内存区域分配内存。
- 当发生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并交换 from 与 to 指针。
- 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半。
面试题:如何判断哪个是to区呢?一句话:谁空谁是to
12.进行垃圾回收
以上算法并不是单兵作战,而是会在JVM里分代协同回收。
上图所示,就是Java堆内存的划分。为什么需要这么划分区域呢?那是因为我们的java对象寿命都是不同的,有的可能需要长时间使用,而有的可能用完就可以丢去。于是我们可以根据其生命周期的不同特点,进行不同的垃圾回收策略。
总的来说,新生代的垃圾回收比较频繁,老年代很久才触发一次垃圾回收。
新生代处理的都是一些朝生夕死的对象,而老年代回收的是更有价值的,会长时间存活的对象。
举个很好理解的例子:新生代处理垃圾,就像是处理生活日用垃圾,而老年代处理的垃圾,更像是过年大扫除,家里实在太多垃圾了来一次重清理。大扫除清理的垃圾,都是在家中存放时间较长的,往往可能曾经很受用,如今退役了先放着过年再打扫清除掉。
大致步骤:
首先等新生代的伊甸园区满了之后用Minor GC进行垃圾回收,筛选一部分资源进入老年代,
等老年去空间满了之后,触发Full GC进行垃圾回收
-
新生代:
- 年轻代中使用的是Minor GC,采用的就是复制算法(mark-copy)
Minor GC 会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移动到Old generation中,也就是说,一旦收集后,Eden就是变成空的了
-
当对象在Eden(包括一个Survivor区域,这里假设是From区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳
- (上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden 和 From 区域中存活的对象),
- 则使用复制算法将这些仍然还活着的对象复制到另外一块Survivor区域(即 to 区域)中,然后清理所使用过的Eden 以及Survivor 区域(即form区域),并且将这些对象的年龄设置为1,
- 以后对象在Survivor区,每熬过一次MinorGC,就将这个对象的年龄 + 1,当这个对象的年龄达到某一个值的时候(默认是15岁,通过- XX:MaxTenuringThreshold 设定参数)这些对象就会成为老年代。
-XX:MaxTenuringThreshold
任期门槛=>设置对象在新生代中存活的次数面试题:如何判断哪个是to区呢?一句话:谁空谁是to
-
老年代:
- 老年代使用的是Full GC,采用的是清除(sweep)与整理(compact)算法
- 在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都像一端移动,然后直接清除边界 以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被 清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比 维护一个空闲列表显然少了许多开销。
- 标记、整理算法不仅可以弥补 标记、清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;
- 老年代使用的是Full GC,采用的是清除(sweep)与整理(compact)算法
小结:
-
内存效率:
- 复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)
-
内存整齐度:
- 复制算法 = 标记压缩算法 > 标记清除算法
-
内存利用率:
- 标记压缩算法 = 标记清除算法 > 复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所 提到的三个指标,标记压缩算法相对来说更平滑一些 , 但是效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记清除多了一个整理内存的过程。
难道就没有一种最优算法吗?猜猜看,下面还有
答案 : 无,没有最好的算法,只有最合适的算法 。
分代收集算法👍
年轻代:(Young Gen)
年轻代特点是区域相对老年代较小,对象存活低。
这种情况复制算法的回收整理,速度是最快的。
复制算法的效率只和当前存活对象大小有关,因而很适 用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代:(Tenure Gen)
老年代的特点是区域较大,对象存活率高!
这种情况,存在大量存活率高的对象,复制算法明显变得不合适。
一般是由标记清除或者是标记清除与标记整理的混合实现。Mark阶段的开销与存活对象的数量成正比,这点来说,对于老年代,标记清除或 者标记整理有一些不符,但可以通过多核多线程利用,对并发,并行的形式提标记效率。Sweep阶段的 开销与所管理里区域的大小相关,但Sweep “就地处决” 的 特点,回收的过程没有对象的移动。使其相对其他有对象移动步骤的回收算法,仍然是是效率最好的,但是需要解决内存碎片的问题。