深入理解JVM--Java垃圾回收机制(4WH原则)

文章目录

    • 为什么需要垃圾回收?why
    • 垃圾回收那些东西?(where JVM内存模型)
    • 什么时候垃圾回收?(when)
      • 引用类型
      • 垃圾回收算法:判断对象是否已死
        • 1、引用计数法
        • 2、可达性分析算法
    • 如何进行垃圾回收?(how方法论)
      • 1、复制算法
      • 2、标记清除算法
      • 3、标记整理算法(标记压缩)
      • 4、分代回收算法
    • 用什么回收垃圾?(what具体实现)
      • 1、Serial收集器
      • 2、ParNew
      • 3、Parallel Scavenge收集器
      • 4、Serial Old收集器
      • 5、Parallel Old 收集器
      • 6、CMS 收集器
      • 7、G1收集器
    • 引用

提到这个问题的时候首先需要去考虑三点问题:
1、为什么需要垃圾回收?(why)
2、垃圾回收那些东西?(where)
3、什么时候垃圾回收?(when)
4、如果进行垃圾回收?(how)
5、用什么回收垃圾?(what)

为什么需要垃圾回收?why

C++中创建的对象,只有显示的去delete,才会回收内存。java提供了垃圾回收机制,旨在方面程序员编写,完成了C++程序员的一部分任务。为什么需要垃圾回收机制?
1、当一个对象不再被引用的时候,内存回收资源,为以便其他对象使用资源。
2、由于创建对象和回收对象会产生内存碎片,垃圾回收会整理碎片,将碎片移动到堆的一端,整理出的内存分配给新的对象。

垃圾回收器作为一个单独的低级别的线程,在不可预知的情况下对内存中已经死亡或者长时间没有使用的对象进行清理,程序员不能实时调用垃圾回收器,可以手动调用System.gc()通知GC运行,但不能保证GC就一定执行。

垃圾回收那些东西?(where JVM内存模型)

此时就需要考虑到JVM内存模型,回收的资源主要集中在堆内存中,如果不能够即使回收,就会导致内存泄漏OOM

深入理解JVM--Java垃圾回收机制(4WH原则)_第1张图片
1、程序计数器:线程私有数据区,指示当前线程执行到的行数,方便各个线程正确的切换

线程执行java方法,程序计数器记录虚拟机字节码指令的地址,执行native底层方法,计数器为空,唯一没有OOM的区域

2、虚拟机栈:描述的是java方法执行的内存模型,每个方法执行的时候都会创建一个“栈帧”,用于存储局部变量表、操作栈、动态链接、返回地址,平常所说的栈都认为是局部变量表

  • 局部变量表

1、是一片连续的内存空间,一般用来存储八大数据类型+引用+返回地址,最小单位是slot,其中long和double占据2个slot,方法的访问顺序是对应入栈到出栈的过程
2、所处位置:内存–>运行时数据区–>每个线程对应的虚拟机栈–>栈帧
3、slot的复用会影响垃圾回收机制(如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。),也就是说会触发GC操作:https://www.cnblogs.com/noKing/p/8167700.html

  • 操作数栈:大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈
    深入理解JVM--Java垃圾回收机制(4WH原则)_第2张图片
  • 动态链接:符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接,在程序编译期间,通过符号引用来使用该空间的数据,只有在程序真正运行的时候才会去将符号引用转变为直接引用。
  • 返回地址:Java虚拟机根据不同数据类型有不同的底层return指令。当被调用方法执行某条return指令时,会选择相应的return指令来让值返回(如果该方法有返回值的话)

虚拟机栈异常
线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

3、本地方法栈:与虚拟机栈发挥的作用相似,不过本地方法栈主要针对Native方法,也就是底层c++方法,也会抛出StackOverflowError和OutOfMemory。
4、方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,如static修饰的变量加载类的时候就被加载到方法区中。

1、也可以称作是永久区,但是得注意永久区与方法区存在差别,方法区可以认为是堆的逻辑部分,而永久区是方法区的一个实现
2、永久代的垃圾回收与老年代的垃圾回收绑在一起,无论谁慢,都会触发垃圾回收
3、运行时常量池:方法区的一部分,存放编译生成的字面量与符号引用,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。
4、 从JDK7开始移除永久代(但并没有移除,还是存在),贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。从JDK8开始使用元空间(Metaspace),元空间的大小受本地内存限制,新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。

注意:常量池在jdk不同版本导致了报错方式不同

public class StringIntern {
    //运行如下代码探究运行时常量池的位置
    public static void main(String[] args) throws Throwable {
        //用list保持着引用 防止full gc回收常量池
        List list = new ArrayList();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}
//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代
//如果是jdk1.7或1.8环境下运行 同时限制堆的大小  将报heap space 即常量池在堆中

5、堆:由线程共享的数据区,一般是用来存放对象的实例与数组,GC管理的主要区域
深入理解JVM--Java垃圾回收机制(4WH原则)_第3张图片
先从宏观上看一下堆,可以将堆划分为新生代=Eden+from Survivor+to Survivor、老年代和永久代,将对内存划分的更加细致,只是为了更好的内存回收,在整个堆中依旧存储的是对象

  • 新生代
    新生代是类创建、成长、消亡的地方,新生代的垃圾回收机制采用的是复制算法,Eden:survivor=8:1,当创建新的对象的时候(1)首先存储在Eden区域(包括一个survivor,假定是from),当Eden的空间用完时,再次创建对象就会导致MInor GC ,之后依旧存活的对象会存放到to Survivor中,之后创建对象存储在(Eden+to)中,如果to空间用完,进行MinorGC存储在from Survivor中,如果from或者to一直有空间,则会创建对象
    (2)如果from和to都满了,那么新创建的对象就会存储在老年代中
    (3)如果老年代满了就会触发一次Major GC,进行老年代清理,如果在MajorGC之后依旧无法保存对象,产生OOM
    (4)上边三步只是大致流程,当然还包括大对象直接进入老年区、长期存活对象进入老年代、动态年龄判断(超过一半具有相同年龄对象,直接进入老年代)

java.lang.OutOfMemoryError: Java heap space
a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

  • 老年代:一般存储从新生代中筛选出的对象
  • 永久带:存储了加载器加载的类信息、存储运行环境必须的类信息,可以看做是方法区的实现

java.lang.OutOfMemoryError: PermGen space(只针对jdk1.7之前)
a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。

6、直接内存(额外内容
虽然不属于运行时数据区,这部分内存也被频繁使用,也会导致OOM,不会受到java堆大小的限制,会受到本地内存限制

什么时候垃圾回收?(when)

引用类型

1、强引用:通过new来产生对象,GC不会回收,只有通过显式的将对象设置为null才会回收,虚拟机宁愿抛出OOM也不会回收强引用
2、软引用:第一次GC不会回收软引用,只有第二次GC才会回收软引用,如果回收之后还没有足够的内存,才会抛出OOM异常使用场景:一般使用在图片缓存或者网页缓存,只有出现OOM,可以回收这部分缓存
3、弱引用:只要垃圾回收器扫描到弱引用,无论内存是否充足,都会回收内存,使用场景:当强引用导致循环引用,循环引用造成了内存泄漏,弱引用避免造成的内存泄漏
4、虚引用:不会决定对象生命周期,在任何时候都可能呗垃圾回收器回收.为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知或者是对象是否存活的一个监控

虚引用使用场景:
jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外,所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

垃圾回收算法:判断对象是否已死

1、引用计数法

每一个对象具有一个计数器,有一个地方引用,计数器+1,引用失效,则计数器-1,当计数器为0的对象是不会被使用,可以回收

缺点:会导致循环引用,造成内存泄漏,因此出现强引用循环的时候,就将其中一个引用设置为weak类型(虚引用),这样就打破了引用循环,不会造成内存泄露

2、可达性分析算法

GC root的对象作为起始点,当一个对象到GC Root没有任何引用链相连,此对象是不可用的,可以判定为可回收对象
GC Root:
1、栈帧本地变量表引用对象
2、静态属性引用对象
3、常量引用的对象
4、本地方发展引用的对象

如何进行垃圾回收?(how方法论)

1、复制算法

将内存空间分成两份a.b,当其中a内存已满的时候,将a内存中的存活的对象(沿着对象引用链可以找到展上的对象)复制到b内存,之后清楚a内存中的内容;
当b内存慢的时候,按照同样的操作流程处理

通过复制算法的介绍,该算法适合于对象失效比较快的情况,符合与回收新生代对象(朝生夕死)
这种方式空间利于率不高

2、标记清除算法

首先标记需要回收的对象,在标记之后统一回收被标记的对象

缺点:
1、标记与回收效率都不高
2、清除算法会导致大量的内存碎片

3、标记整理算法(标记压缩)

与标记清除算法类似,首先将或者想(整理)压缩到堆的一侧,之后清楚另一侧的内存,一般适合于老年代的回收方式

4、分代回收算法

采用分而治之的思想,将对内存划分为老年代和新生代,在HotSpot中,新生代:老年代=1:2,新生代采取复制算法,而老年代采取标记清理或者标记压缩的算法
1、新生代=(Eden:Survivor from:Survivor to = 8:1:1)回收策略:
(1)首先创建对象,存储Eden+from区域,一旦没有足够的内存进行分配,虚拟机发起一次Minor GC,之后如果存活对象内存小于to区域内村,将存活的对象放置到to区域,并且将对象年龄+1,如果有大对象直接进入老年代,如果年龄大于阈值(默认是15),则直接进入老年代。
(2)之后继续创建对象,存储到Eden+to区域,一直会保持一个Survivor空白
(3)当连续分配对象的时候,对象会逐渐从eden到 survivor到老年代,最终OOM

新生代的空间分配担保
在发生minor gc之前,虚拟机会检测 : 老年代最大可用的连续空间>新生代all对象总空间
1、满足,minor gc是安全的,可以进行minor gc。
2、不满足,虚拟机查看HandlePromotionFailure参数:
(1)为true,允许担保失败,会继续检测老年代最大可用的连续空间>历次晋升到老年代对象的平均大小。若大 于,将尝试进行一次minor gc,若失败,则重新进行一次full gc。
(2)为false,则不允许冒险,要进行full gc(对老年代进行gc)。

2、老年代回收策略:老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收

用什么回收垃圾?(what具体实现)

图片参照深入理解Java虚拟机

1、Serial收集器

稳定效率高,可能会产生较长时间的停顿。新生代采取复制算法,老年代采取标记整理,收齐过程中会stop the world
Client模式下的首选新生代收集器
深入理解JVM--Java垃圾回收机制(4WH原则)_第4张图片

2、ParNew

多线程垃圾收集,其余与Serial相同,Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作

深入理解JVM--Java垃圾回收机制(4WH原则)_第5张图片

3、Parallel Scavenge收集器

新生代收集器,也是多线程+复制算法的模式,但是关注吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),关注停顿时间主要是为了与用户程序的交互,而关注吞吐量主要是为了提高cpu效率,自适应调节策略

1、提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收起停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数
2、-XX:+UseAdaptiveSizePolicy自适应开启后,只需要设置基本数据(最大堆)和控制吞吐量的参数作为优化目标就可以

4、Serial Old收集器

主要是用在client模式下的虚拟机使用,单线程收集器,使用标记整理算法
Server模式下的用途:
(1)jdk1.5之前与Parallel Scavenge搭配使用
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
深入理解JVM--Java垃圾回收机制(4WH原则)_第6张图片

5、Parallel Old 收集器

jdk1.6之后提出,真正能够实现关注吞吐量,由于jdk1.5之前,只能与Serial Old合作,不能达到吞吐量最大化
注重吞吐量以及CPU敏感的场所,可以选择Parallel Old +Parallel Scavenge的组合
深入理解JVM--Java垃圾回收机制(4WH原则)_第7张图片

6、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器

步骤:
(1)初始标记:stop the world,只是标记GcRoot 能直接关联到的对象,速度快
(2)并发标记:并发标记就是GC Root Tracing的过程,可以与用户线程一起工作
(3)重新标记:stop the world,修改由于由于用户程序运作而导致的变动的对象的标记
(4)并发清除:并打清楚可回收对象,可以与用户线程一起工作

优缺点:
优点:并发收集、低停顿
缺点:
(1)CPU敏感无可厚非,并发处理都会导致总吞吐量的下降,处理:增量式并发收集器(就是让用户程序,清理线程、并发标记交替执行),减少GC线程独占资源
(2)无法处理浮动垃圾,浮动垃圾是由于在并发清除的时候用户线程依旧会产生新的垃圾,这些浮动垃圾只能在下一次GC时候清理,由于在垃圾收集阶段需要保证预留足够的空间给用户线程使用,一旦运行期间预留的内存无法满足程序需要,就会出现“”Concurrent Mode Failure“”,此时虚拟机启动serial Old进行老年代收集,停顿时间变长
(3)标记清除算法会导致空间碎片,空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。
-XX:+UseCMSCompactAtFullCollection用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。
-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)

7、G1收集器

1、G1的来历
G1是一款面向于服务端应用的垃圾收集器,将整个java堆划分为大小相等的独立区域region(不需要连续的集合),之后根据每个region中的价值大小,在后台维护一个优先列表,每次回收价值大的Region(也就是Garbage-First)
2、G1特点
(1)并行与并发
(2)分代收集
(3)空间整合(采用标记整理,G1运行期间不会产生内存空间碎片)
(4)可预测时间停顿:在一个长度为M浩渺的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征
3、G1步骤
G1 内存“化整为零”的思路,每一个Region都有一个与之对应的Remembered Set,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
(1)初始标记:标记一下GC Root能直接关联到的对象,需要停顿但是耗时短
(2)并发标记:堆中对象可达性分析,找出存活对象,可与用户并发执行
(3)最终标记:并发标记过程中用户程序会产生标记记录的变化,虚拟机将变化存储在Remembered Set Log,只需要将Remembered Set Log合并到Remembered Set;
(4)筛选回收:根据回收价值与成本进行对象回收
深入理解JVM--Java垃圾回收机制(4WH原则)_第8张图片

新生代GC策略 老年老代GC策略 说明
组合1 Serial Serial Old Serial和SerialOld都是单线程进行GC,特点就是GC时暂停所有应用线程。
组合2 Serial CMS+Serial Old CMS(ConcurrentMarkSweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用SerialOld策略进行GC。
组合3 ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
组合4 ParNew Serial Old 使用 -XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
组合5 Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 +GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
组合6 ParallelScavenge Parallel Old Parallel Old是Serial Old的并行版本
组合7 G1GC G1GC -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例

引用

【1】虚拟机栈中栈帧局部变量的理解:https://www.cnblogs.com/noKing/p/8167700.html
【2】堆的理解:https://blog.csdn.net/u010488116/article/details/79800142
【3】引用:https://blog.csdn.net/sinat_21118695/article/details/82392028
【4】回收:https://www.cnblogs.com/ludashi/p/6694965.html
【5】空间分配担保:https://blog.csdn.net/g1607058603/article/details/80521521
【6】收齐器组合:https://www.cnblogs.com/williamjie/p/9497906.html

你可能感兴趣的:(jvm)