jvm学习第二天

0.垃圾回收概述

1.什么是垃圾,怎么判断?

1.1引用计数法

含义
顾名思义,此种算法会在每一个对象上记录这个对象被引用的次数,只要有任何一个对象引用了此对象,这个对象的计数器就+1,取消对这个对象的引用时,计数器就-1。任何一个时刻,如果该对象的计数器为0,那么这个对象就是可以回收的。
打个比方:

public static void method() {
    A a = new A();
}

public static void main(String[] args) {
    method();
}

main函数调用method方法,method方法中new了一个A的对象,赋值给局部变量a,此时堆内存中的对象A的实例的计数器就会+1。当方法结束时,局部变量会随之销毁,堆内存中的对象的计数器就会-1。
存在的问题
该算法存在两个问题: 1.无法处理循环引用的情况。 2.从上述的原理可知,堆内对象的每一次引用赋值和每一次引用清除,都伴随着加减法的操作,内部还需要额外维护一个计数器,会带来一定的性能开销。主流的商用JVM实现都没有采用这种方式。
考虑如下代码:

class A {
    private B b;
    public void setB(B b) {
        this.b = b;
    }
}

class B {
    private A a = new A();
    public void setA(A a) {
        this.a = a;
    }
}

public void method() {
    A a = new A();
    B b = new B();
    a.setB(b);
    b.setA(a);
}

内存图如下:

method方法中,执行完两个set后,method方法结束,图中两条红线引用消失,可以看到,留下两个对象在堆内存中循环引用,但此时已经没有地方在用他们了,造成内存泄漏。两个对象就凌乱在风中不知所措了。

1.2可达性分析法(java采用的这种)

为了解决引用计数方法导致的问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

要想理解可达性算法,首先要想明白几个问题:

1、什么是对象可达?
对象可达指的就是:双方存在直接或间接的引用关系。
根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。

2、GC Roots是什么?
垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。
然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。
那么,到底什么是GC Roots呢?
GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。
只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。

3、哪些对象可以作为GC Roots?
可以作为GC Roots的对象可以分为两大类:全局对象和执行上下文

4.一起来理解一下为什么这几类对象可以被作为GC Roots
(1)、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。

(2)、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

(3)、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。

(4)、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。

(5)、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。

5.编写代码并用Memory Analyzer查找根对象

先用jmap抓取运行中程序的内存情况转储为文件,然后再使用这个工具进行分析,这边建议亲亲您看视频哈

2.引用

什么是引用?
​众所周知,JAVA是一种面向对象的语言,在JAVA程序运行时,对象是存储在堆内存(Heap)中的,C/C++ 中是通过指针来访问所谓对象(结构体)的,而JAVA则是通过引用来访问对象,也就是说,引用指向了对象在堆内存中的地址,引用本身也占用内存,64 位的 JVM 中,引用所占内存大小为 8 个字节,通过指针压缩后占用4个字节。
​在 JDK 1.2 之前,JAVA 对引用的定义为:如果一个数据中存储的数值代表的是另外一块内存的起始地址,就称这块数据的内存代表着一个引用。
​在 JDK 1.2 之后,JAVA 引用的概念得到了扩充,引用被分为:强引用、软引用、弱引用、虚引用、终结器引用。
正常情况下我们平时基本上我们只用到强引用类型,而其他的引用类型我们也就在面试中,或者平日阅读类库或其他框架源码的时候才能见到。

2.1强引用

我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!
记住是存活着,不可能是你new一个对象就永远不会被GC回收。当一个普通对象没有其他引用关系,只要超过了引用的作用域或者显示的将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了。当然回收的时间是不一定的具体得看GC回收策略。

2.2软引用

软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的。

Object obj = new Object();
SoftReference softObj = new SoftReference(obj);
obj = null; //去除强引用

这样就是一个简单的软引用使用方法。通过get()方法获取对象。当JVM认为内存空间不足时,就回去试图回收软引用指向的对象,也就是说在JVM抛出OutOfMemoryError之前,会去清理软引用对象。软引用可以与引用队列(ReferenceQueue)联合使用。

2.2.1 引用队列

ReferenceQueue,当一个引用(软引用、弱引用)关联到了一个引用队列后,当这个引用所引用的对象要被垃圾回收时,就会将它加入到所关联的引用队列中,所以判断一个引用对象是否已经被回收的一个现象就是,这个对象的引用是否被加入到了它所关联的引用队列。

ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);

说到底,引用队列就是一个对引用的回收机制,当软引用或弱引用所包装的对象为 null 或被回收时,这个引用也就不在具有价值,引用队列就是清除掉这部分引用的一种回收机制

2.2.2 软引用应用

软引用一般用来实现内存敏感的缓存,如果有空闲内存就可以保留缓存,当内存不足时就清理掉,这样就保证使用缓存的同时不会耗尽内存。例如图片缓存框架中缓存图片就是通过软引用的。

2.3弱引用

弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象。

 Object obj = new Object();
 WeakReference<Object> weakObj = new WeakReference<Object>(obj);
 obj = null; //去除强引用

在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue 使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。

2.4虚引用

通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样,虚引用必须配合引用队列使用。

Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference<Object> phantomObj = new PhantomReference<Object>(obj , queue);
obj = null; //去除强引用

无法通过虚引用访问对象的任何属性或者函数。那就要问了要它有什么用?虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。比如说这个对象被回收之后发一个系统通知啊啥的。虚引用是必须配合ReferenceQueue 使用的,具体使用方法和上面提到软引用的一样。主要用来跟踪对象被垃圾回收的活动。例如:配合 ByteBuffer 使用,被引用对象回收时,会将虚引用cleaner入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

2.5终结器引用


大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
查看我另一篇文章:finalize方法使用介绍

3.垃圾回收算法

标记-清除算法、复制算法、标记-整理算法

3.1 标记清除

分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象
缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

3.2 标记整理

先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存。

3.3 复制

把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环。
缺点:内存使用率不高,只有原来的一半。

3.4finalize()二次标记

一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程。
第一次标记过程,通过可达性分析算法分析对象是否与GC Roots可达。经过第一次标记,并且被筛选为不可达的对象会进行第二次标记。
第二次标记过程,判断不可达对象是否有必要执行finalize方法。执行条件是当前对象的finalize方法被重写,并且还未被系统调用过。如果允许执行那么这个对象将会被放到一个叫F-Query的队列中,等待被执行。

注意:由于finalize由一个优先级比较低的Finalizer线程运行,所以该对象的的finalize方法不一定被执行,即使被执行了,也不保证finalize方法一定会执行完。如果对象第二次小规模标记,即finalize方法中拯救自己,只需要重新和引用链上的任一对象建立关联即可。

3.5总结

已经有了上面这三种垃圾回收算法,为什么需要分代垃圾回收嘞?
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同方式的垃圾回收算法收集,以便提高回收效率。

4.分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from 和 to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
    详细讲解视频

4.1 相关vm参数

描述 配置方式
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size ,这两个参数是等价的
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC,默认是打开的这个参数 -XX:+ScavengeBeforeFullGC

4.2 GC分析

第一阶段,未创建任何对象时
注意:需要设置下jvm的参数,和注释处相同,这样便于演示,第一个参数是堆初始化空间大小,第二个是堆最大空间,第三个是新生代大小,第四个是使用哪种垃圾收集器,第五六是打印gc时的详细日志

/**
 * author:wcc 进击的王小子
 */
public class GcTest {
    private static final int _512kb = 512*1024;
    private static final int _1MB = 1024*1024;
    private static final int _6MB = 6*1024*1024;
    private static final int _7MB = 7*1024*1024;
    private static final int _8MB = 8*1024*1024;
    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {

    }
}


这里分配了10m,由于from和to各要占用1m,所以eden space是8m,新生代总空间total为9m,因为to是用来存储复制过来的对象,from和to只有to使用到,故total为9m.

第二次,创建一个7M的对象后

可以看到发生了一次新生代GC,DefNew代表是发生在新生代的垃圾回收,1984k->667k(9216k)代表回收前后的内存占用,以及新生代的内存大小,1984k->667k(19456k)代表回收前后占用的内存以及堆内存的总大小。此时eden区已经占用90%.
第三次,再创建一个512k的对象后


可以看到还是只触发第二次这个步骤中的所导致的垃圾回收,即7M这个对象,接着放入512k对象时,空间还是够的,并不会触发垃圾回收。
第四次,再创建一个512k的对象后
这时候应该是内存不够,内存实在是紧张,就不会看阈值,直接放入老年代了,看下结果。


第五次,直接在堆中创建一个8M的对象后
可以看到,由于eden区的大小才只有8M,而本身运行需要消耗一些内存,所以一个8M的对象肯定是放不下的,这时候,minor GC也不会进行了,直接将对象放入到老年代.

第六次,直接在堆中创建2个8M的对象后
由于我们分配的堆最大内存为20M,新生代的Eden区占用了8M,老年代占用10M,2个地方都不能单独放下2个8M的对象,最终会导致oom异常,但是要注意下oom异常之前的垃圾回收操作。

注意:多线程场景下,一个线程的oom 并不会导致别的线程也oom别的线程还是会正常运行。参考文章:一个进程有3个线程,如果一个线程抛出oom,其他两个线程还能运行么?

5.垃圾回收器

5.1 垃圾回收器_串行

5.2 垃圾回收器_吞吐量优先


UseParalleGC:Paralle是并行的意思,UseParalleGC工作在新生代,采用复制算法

UseParalleoldGC:工作在老年代,采用标记整理算法

只要打开一个,另外一个就默认开启

-XX:+UseAdaptiveSizePolicy:自动调整伊甸园区和幸存区的比例

-XX:+GCTimeRatio=ratio:1/(1+radio)垃圾处理的目标(radio默认为99)

-XX:MaxGCPauseMillis=ms:最大暂停(默认200ms)

-XX:ParallelGCThreads=n:垃圾处理线程的数量

5.3 垃圾回收器_CMS垃圾收集器(响应时间优先)

-XX:UseConcMarkSweepGC:工作在老年代
-XX:+UseParNewGC:工作在新生代
初始标记:标记根对象,较快
并发标记:不用STW,和用户线程并发执行
并发清理:和其他用户线程并发执行
CMS垃圾收集器

5.4 G1

5.4.1

5.4.2

5.4.3

5.4.4

5.4.5

5.4.6

5.4.7

5.4.8

5.4.9

5.4.10

5.4.11

6.垃圾回收器调优

6.1.调优领域

6.2.确定目标

posted @ 2020-11-28 02:27  皮卡丘和羊宝贝😄  阅读(124)  评论(0编辑  收藏  举报