继上一篇 JVM学习之路2-对象内存布局及逃逸分析 介绍完jvm相关对象在内存中如何布局、如何进行访问以及jvm进行逃逸分析并作优化以后,本篇准备聊一下jvm最关键的一个点(也是现实中遇到问题最多的点)垃圾回收,本篇相对比较长,你们能够只看本身须要看的部分。
知识点
一、垃圾回收机制
二、垃圾收集算法及常见的垃圾收集器html
顾名思义,垃圾回收就是对垃圾进行回收,可是jvm中哪些属于"垃圾"呢?无用的对象。大概整理了下目前主流的gc流程:
jvm判断对象是否能够被回收主要有两种算法,引用计数法和可达性分析。下面分别来介绍一下。java
在对象中添加一个计数器,用来统计对象被引用的次数,每次对象被引用一次就加1,引用减小一次就减1,当引用次数为0的时候就说明对象没有被使用,那就能够销毁了。算法
可达性分析也比较简单,就是有一系列做为"gc root"的对象做为起始点,由该节点开始向下查找,当一个对象到gc root没有任何引用链连接时就认为该对象能够被回收了,目前主流的虚拟机都是采用这种方式进行回收断定。如下对象能够做为gc root:
一、虚拟机栈中引用的对象;
二、方法区中类静态属性引用的对象;
三、方法区中常量引用的对象;
四、本地方法栈中引用的对象;
要注意一点,正常来讲对象不可达都会被回收,可是对象也并不是"非死不可",咱们能够经过重载finalize()对对象再引用一次,便可阻止对象被回收(不建议这么作)。segmentfault
在谈到对象是否被引用的时候,咱们要讲一下java中的4种引用类型:强引用、弱引用、软引用、虚引用;多线程
只要对象还被引用,那就不会被gc所回收。这个是咱们大多数时候在用的,举个例子:并发
A a = new A();
这个a就是一个强引用。jvm
若是一个对象只有弱引用在引用它,那么下次gc的时候就会被回收。这个平时咱们代码中用得很少,可是在看源码的时候会发现用得仍是比较多的(ThreadLocal就是用得弱引用key的管理),这也是一种内存对象的管理方式。举个例子:函数
ReferenceQueue<Employee> referenceQueue = new ReferenceQueue<>(); WeakReference<A> weakA = new WeakReference<>(new A()); WeakReference<A> weakA = new WeakReference<>(new A(), referenceQueue);
上述代码中的weakA就是一个弱引用,在下次gc的时候就会被回收。布局
若是一个对象只有软引用,在内存即将不够的时候,会将这些对象进行回收。举个例子:性能
ReferenceQueue<Employee> referenceQueue = new ReferenceQueue<>(); SoftReference<A> softEmployee = new SoftReference<>(new A()); SoftReference<A> softEmployee = new SoftReference<>(new A(),referenceQueue);
该引用能够认为没有被引用,也没法经过该引用获取到对象实例,总得来讲该引用只有一种做用,就是在对象被回收的时候收到系统通知。举个例子:
PhantomReference<Employee> phantomEmployee = new PhantomReference<>(new Employee(), referenceQueue);
能够看出虚引用只有一个构造函数,而且须要传递引用队列,引用队列的做用就是在对象被回收以后能在引用队列里找到该引用(不是该对象)。固然弱引用、软引用均可以用到引用队列。
在介绍内存模型中堆的概念的时候,说到堆上的空间分为新生代、老年代(默认1:2),新生代又分为eden区、to区、from区(默认8:1:1),有一张图已经比较清晰的展示了不一样代的状况,这里把相关的概念介绍下,后面分析gc日志的时候仍是比较有用的。
新生代收集(Minor GC/Young GC):对于新生代空间上的垃圾进行回收。
老年代收集(Major GC/Old GC):对于老年代空间上的垃圾进行回收。Major GC有些资料也指整堆收集。
整堆收集(Full GC):收集全部堆和方法区。
对于大对象,咱们知道是直接进入老年代的,因此代码中尽可能避免大对象:
针对不一样的分代,有不一样的收集算法适配,目前常见的垃圾收集算法有三种:标记-清除、标记-复制、标记-整理。
最先最基础的算法,很是简单,对于知足回收条件的对象进行标记,而后在回收的时候对带标记的对象进行回收。
缺点:
一、效率低,存在大量对象时就会有大量的标记和清除动做;
二、内存碎片,会产生大量不连续的内存碎片,致使后面分配内存可能由于连续空间不够致使须要full gc;
该算法也比较简单,就是将可用内存分为相等的两块,一块为在用的内存,一块为空闲内存,当在用内存空间不够分配的时候,就会将该内存中存活的对象复制一份到空闲内存中,而后对在用内存进行清理。
缺点:
一、内存浪费,因为存在空闲内存,最多可能会浪费50%内存空间;
二、对象存活率较高时会有大量复制,存在效率问题;
目前主流的虚拟机对于新生代的回收都是采用该算法。
和标记-清除很像,只不过是在清除完后会对空间进行整理,以保证内存空间的连续,减小内存碎片。
缺点:
一、存活对象较多时候,内存空间整理会比较耗性能,形成卡顿;
垃圾收集器就是垃圾收集算法的实践者。直接引用书里的一张图,大概了解一下对于不一样区目前有哪些收集器以及如何搭配使用。
存在连线的说明是能够搭配使用的收集器。下面咱们逐一介绍这些收集器。
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
顾名思义,单线程收集器,会有一个额外线程对垃圾进行收集,在收集的过程当中会暂停其余全部工做线程。
缺点:体验差,会形成程序卡顿;
优势:简单高效、内存消耗少;
算法:标记-复制;
使用场景:对于新生代只须要几百兆内存的应用,卡顿时间差很少在几十上百毫秒,非频繁收集下是能够接受的。应用于客户端应用。
设置参数:-XX:+UseSerialGC
其实就是Serial的多线程版本,在单核系统下相比Serial没有优点,多核下就有优点,会有多个线程同时进行垃圾回收,优缺点和Serial同样。
使用场景:配合CMS进行使用,应用于服务端应用。
设置参数:
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew做为新生代收集器;
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
与ParNew相似,可是关注点在吞吐量而不在卡顿,该收集器与其老年代版本为jdk8默认收集器。
吞吐量=处理器用于运行用户代码的时间/处理器总消耗时间的比值,也就是尽可能让处理器运行用户代码。该收集器支持经过参数设置自适应策略,虚拟机会自动进行调优。
算法:标记-复制。
使用场景:后台计算类应用。
设置参数:
"-XX:MaxGCPauseMillis":控制最大垃圾收集停顿时间,大于0的毫秒数,MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量降低,由于可能致使垃圾收集发生得更频繁;
"-XX:GCTimeRatio":设置垃圾收集时间占总时间的比率,0<n<100的整数,GCTimeRatio至关于设置吞吐量大小,计算方法是1 / (1 + n)
,默认值99;
"-XX:+UseAdptiveSizePolicy":开启Jvm自适应策略,JVM会根据当前系统运行状况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量;
Serial收集器的老年代版本,没什么好多说的。
算法:标记-整理。
Parallel Scavenge收集器的老年代版本,传说中的吞吐量组合,没什么好多说的。
算法:标记-整理。
全称Concurrent Mark Sweep,以低停顿做为目标的收集器,基于标记-清除算法,总体分为4个过程:初始标记、并发标记、从新标记、并发清除。其中初始标记和从新标记是须要中止全部用户线程的,可是耗时较短。默认收集线程数为(cpu核数+3)/4个。
优势:收集效率高,低停顿。
缺点:内存碎片,cpu数量少于4个状况下对应用影响较大,没法处理浮动垃圾(并发标记和清除过程当中用户线程在运行又产生的垃圾)。
应用场景:对于响应性能要求高的系统。
设置参数:
"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
全称Garbage-First,该收集器的使命是比较重的,一个里程碑的收集器,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式,再也不区分新生代和老年代。什么是Region?能够理解为一块连续的内存区域,一个堆分为大小相等的N个Region。G1就是基于Region进行垃圾回收,Region既会扮演新生代eden、survivor区,又会扮演老年代区。用Humongous区存放大对象(对象大小超过Region一半,一个不够则用多个Region放)。
大体堆分布变成这样(引用自参考资料第四个博客):
基本上和上面提到的垃圾收集器实现彻底不同了,不须要回收新生代整个区或者老年代整个区,G1内部会维护一个回收的优先级列表,在目标时间周期内只要把列表中"价值"(可回收空间以及所需时间)最大的Region回收过来就行。
回收步骤:
一、初始标记,须要停顿,耗时短。
二、并发标记,无需停顿。
三、最终标记,须要停顿,耗时短。
四、筛选回收,对各个Region的回收价值和成本进行排序,制定具体的回收计划,无需停顿。
G1虽然好,可是咱们也不是必定要用G1,仍是得结合具体场景和实际状况来选择;
优势:延迟可控,高吞吐,没有内存碎片,不须要和其余收集器搭配使用。
缺点:为了解决对象跨Region引用问题,须要记忆集进行记录引用关系,须要额外内存开销。
算法:总体看集于标记-整理算法进行收集,局部看基于标记-复制算法进行收集。
应用场景:面向服务端应用,针对具备大内存、多处理器的机器。
设置参数:
"-XX:+UseG1GC":指定使用G1收集器。
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45。
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒。
"-XX:G1HeapRegionSize":设置每一个Region大小,范围1MB到32MB,要为2的N次幂;目标是在最小Java堆时能够拥有约2048个Region。
"-XX:ConcGCThreads":并发GC使用的线程数。
本篇内容比较多又比较长,花了很多时间来写,基本上说清了jvm的垃圾回收机制,包括对4种引用类型也作了相关使用介绍,对于垃圾收集的算法以及主流的7种收集器也作了较为详细的说明,固然如今还有ZGC、Shenandoah这些更牛逼的收集器,可是用的很少,就不花时间整理介绍了,有须要你们能够再本身找一下资料学习。后面有新的收获会再进行补充,下一篇会介绍Jvm的类加载机制,在系列文最后会写调优案例,到时候会结合相关案例来实践前面的理论。
参考资料:
周志明《深刻理解Java虚拟机》
https://www.cnblogs.com/jswan...
https://www.cnblogs.com/cxxjo...
https://blog.csdn.net/pedro7k...