深入理解JAVA虚拟机读书笔记----垃圾收集器与内存分配策略

来源:互联网 发布:单片机流水灯不亮 编辑:程序博客网 时间:2024/06/05 02:47

GC的主要对象:JAVA堆和方法区

判断GC是否存活的方法:

1、引用技术算法

说明:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
特点:简单高效
实际:大多数虚拟机不会使用这个,主要原因是它很难解决对象之间相互循环引用的问题

2、可达性分析(Reachability Analysis)

说明:基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
在Java语言里,可作为GC Roots对象的包括如下几种:

a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象

用的较多

3、强软弱虚

强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

4、finalize直接不用

5、回收方法区

方法区回收的性价比很低,主要收集两部分内容:废弃常量的无用的类。一般不考虑这个区的回收问题,但是如果项目中用了太多的反射、动态代理、CGLib等ByteCode框架、动态生成JSP、OSGi等频繁自定义ClassLoader的场景则需要虚拟机具备回收方法区的功能,保证永久代不溢出。

垃圾收集算法

1、标记-清除算法

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
特点:效率不高,内存空间碎片太多

2、复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
特点:简单高效,内存缩小一半,新生代适宜采用

3、标记-整理算法

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
特点:效率一般,内存效率高,老年代适用

4、分代收集算法

新生代:复制算法
老年代:标记-清除算法 或者 标记-整理算法

HotSpot的算法实现

枚举根节点:可达性分析从GC Roots节点找出引用链,但大型应用太多了,要逐个检查引用必然会浪费太多的时间。HotSpot用OopMap这个数据结构来空间换时间,保证了速度。
安全点:程序执行时并非在所有地方都能停下来开始GC,只有到达安全点才能暂停。
安全区域:借用安全点的概念,指在一段代码片段中,引用关系不会发生变法,在这个区域中的任意地方GC都是安全的。

垃圾收集器

介绍

垃圾收集器.jpg
1、Serial收集器曾经是虚拟机新生代收集的唯一选择,是一个单线程的收集器,在进行收集垃圾时,必须stop the world,它是虚拟机运行在Client模式下的默认新生代收集器。

2、Serial Old是Serial收集器的老年代版本,同样是单线程收集器,使用标记整理算法。

3、ParNew收集器是Serial收集器的多线程版本,许多运行在Server模式下的虚拟机中首选的新生代收集器,除Serial外,只有它能与CMS收集器配合工作。

4、Parallel Scavenge收集器也是新生代收集器,使用复制算法又是并行的多线程收集器,它的目标是达到一个可控制的运行用户代码跟(运行用户代码+垃圾收集时间)的百分比值。

5、Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。

6、Concurrent Mark Sweep 收集器是一种以获得最短回收停顿时间为目标的收集器,基于标记清除算法。过程如下:初始标记,并发标记,重新标记,并发清除,优点是并发收集,低停顿,缺点是对CPU资源非常敏感,无法处理浮动垃圾,收集结束会产生大量空间碎片。

7、G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。

组合

Serial/Serial Old
-XX:+UseSerialGC:强制使用该GC组合
-XX:+PrintGCApplicationStoppedTime:查看STW(Stop The World)时间
CPU核数<2,物理内存<2G的机器(简单来讲,单CPU,新生代空间较小且对STW时间要求不高的情况下使用)

ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已
在单CPU情况下没有Serial/Serial Old速度快(因为ParNew多线程需要切换),在多CPU情况下又没有之后的三种组合快(因为Serial Old是单GC线程),所以使用其实不多。
-XX:+UseParallelGC:强制使用该组合

ParNew/CMS:当下比较高效的组合,注重用户体验组合
-XX:+UseConcMarkSweepGC:使用该GC组合
-XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收
-XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW
-XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理
-XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4

Parallel Scavenge/Parallel Old:自动管理的组合,注重吞吐量组合
-XX:+UseParallelOldGC:使用该GC组合
-XX:GCTimeRatio:直接设置吞吐量大小,假设设为19,则允许的最大GC时间占总时间的1/(1 +19),默认值为99,即1/(1+99)
-XX:MaxGCPauseMillis:最大GC停顿时间,该参数并非越小越好
-XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC自适应调节策略),而我们需要设置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio两个参数就好(当然-Xms也指定上与-Xmx相同就好)

G1:最先进的收集器,但是需要JDK1.7update14以上
G1可单独回收整个空间,未来想要替代CMS
-XX:+UseG1GC :使用G1收集器

设置垃圾收集器组合

运行 java -XX:+PrintCommandLineFlags -version 然后看参数,和上面的组合参数对照一下即可
这块还是总结下吧:
-XX:+UseSerialGC:Serial/Serial Old组合
-XX:+UseParallelGC:ParNew/Serial Old组合
-XX:+UseConcMarkSweepGC:ParNew/CMS组合
-XX:+UseParallelOldGC:Parallel Scavenge/Parallel Old组合
-XX:+UseG1GC :G1收集器

理解GC日志

例子

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]  100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 

● 最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
● “[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的。新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC (System)”。
● “[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,分别是新生代、老年代、永久带,这里显示的区域名称与使用的GC收集器是密切相关的。
● 后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。
● “0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[Times: user=0.01 sys=0.00, real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。

Minor GC、Major GC和Full GC

Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快。
Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
Full GC:指发生在老年代和新生代的GC,速度很慢,需要Stop The World。
http://www.importnew.com/15820.html 更详细的解释

jvm 内存分配和回收策略

一下实验用不同的垃圾收集器会有不同的结果,所以最好先了解自己的垃圾收集器类型,然后强制设定自己的垃圾收集器类型来看结果比较好

1、对象优先在eden分配

jvm给一个对象分配内存会先在eden区域分配,如果内存不足,会发起一次young GC.如果回收了之后,内存空间依然不够,就会通过担保机制提前把一些可以转移的对象分配到老年代中,以此保证eden中的内存足够给当前要分配的对象使用。

package com.leo.c03;/** * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * * 强制使用固定的垃圾收集器 * -XX:+UseSerialGC:Serial/Serial Old组合 -XX:+UseParallelGC:ParNew/Serial Old组合 -XX:+UseConcMarkSweepGC:ParNew/CMS组合 -XX:+UseParallelOldGC:Parallel Scavenge/Parallel Old组合 -XX:+UseG1GC :G1收集器 * @author xuexiaolei * @version 2017年09月06日 */public class EdenAllocationTest {    private static final int _1MB = 1024 * 1024;    public static void main(String[] args) {        byte[] allocation1 = new byte[2 * _1MB];//1        byte[] allocation2 = new byte[2 * _1MB];//2        byte[] allocation3 = new byte[2 * _1MB];//3        byte[] allocation4 = new byte[4 * _1MB];//4    }}

2、大对象直接进入老年代

大对象一般指那种很长的字符串以及数组。对于jvm来说,大对象不是一个好的现象,大对象的出现容易导致明明虚拟机内存还有不少空间就可能会提前触发GC(内存空间不连续的时候)。

设置-XX:PretenureSizeThreshold可以设置一个对象大于这个阀值的时候,被直接分配到老年代的区域中。

package com.leo.c03;/** *  VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 *          超过 3M 直接进入老年代 * * 强制使用固定的垃圾收集器 * -XX:+UseSerialGC:Serial/Serial Old组合 -XX:+UseParallelGC:ParNew/Serial Old组合 -XX:+UseConcMarkSweepGC:ParNew/CMS组合 -XX:+UseParallelOldGC:Parallel Scavenge/Parallel Old组合 -XX:+UseG1GC :G1收集器 * @author xuexiaolei * @version 2017年09月06日 */public class OldTest {    private static final int _1MB = 1024 * 1024;    public static void main(String[] args) {        byte[] allocation = new byte[4 * _1MB];    }}

3、长期存活的对象讲进入老年代

jvm采用了分带的概念,那么jvm是怎么区分对象具体在哪个代呢?
JVM给每个对象定义了一个年龄计数器,对象从eden出生后,经过一次young gc仍然存活,能被survivor容纳,就被移动到survivor中,年龄设置为1。之后每次这个对象在survivor中熬过一次young gc之后,年龄都会加1.年龄到了一定的阀值,会晋升到老年代中。我们可以设置JVM参数-XX:MaxTenuringThreshold设置晋升到老年代的对象年龄阀值。

/** * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 * -XX:+PrintTenuringDistribution * 强制使用固定的垃圾收集器 * -XX:+UseSerialGC:Serial/Serial Old组合 -XX:+UseParallelGC:ParNew/Serial Old组合 -XX:+UseConcMarkSweepGC:ParNew/CMS组合 -XX:+UseParallelOldGC:Parallel Scavenge/Parallel Old组合 -XX:+UseG1GC :G1收集器 */public static void testTenuringThreshold() {    byte[] allocation1, allocation2, allocation3;    allocation1 = new byte[_1MB / 4];    allocation2 = new byte[4 * _1MB];    allocation3 = new byte[4 * _1MB];    allocation3 = null;    allocation3 = new byte[4 * _1MB];}

4、动态对象年龄判定

如果在survivor中的相同年龄所有对象的大小总和大于survivor空间的一半,年龄大等于该年龄的对象就可以直接进入老年代,不需要等到达到年龄阀值。

    /**     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15     * -XX:+PrintTenuringDistribution     */    @SuppressWarnings("unused")    public static void testTenuringThreshold2() {        byte[] allocation1, allocation2, allocation3, allocation4;        allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大于survivo空间一半        allocation2 = new byte[_1MB / 4];        allocation3 = new byte[4 * _1MB];        allocation4 = new byte[4 * _1MB];        allocation4 = null;        allocation4 = new byte[4 * _1MB];

5、空间分配担保

在JVM进行young gc之前,先检查老年代最大可用连续空间是否大于年轻代所有对象总空间:如果条件成立,证明young gc是可以确保安全的;如果不成立,JVM会查看是否允许担保失败,如果允许就会进行young gc。如果不允许,就改为进行一次full gc。

这里我们说的是否允许担保失败我们可以这么理解:在survivor中的一些对象可能会在young gc之后进入老年代(这些对象的大小其实是可以在这次ygc前就知道的),但是如果老年代的空间不足够容纳这些要晋升的对象,这就担保失败了,这时候进行一次full gc会吧老年代中无用的对象清理,然后给新晋升的这些对象腾出空间。

    /**     * VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure     */    @SuppressWarnings("unused")    public static void testHandlePromotion() {        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;        allocation1 = new byte[2 * _1MB];        allocation2 = new byte[2 * _1MB];        allocation3 = new byte[2 * _1MB];        allocation1 = null;        allocation4 = new byte[2 * _1MB];        allocation5 = new byte[2 * _1MB];        allocation6 = new byte[2 * _1MB];        allocation4 = null;        allocation5 = null;        allocation6 = null;        allocation7 = new byte[2 * _1MB];    }
阅读全文
0 0
原创粉丝点击