从零开始理解JVM:对象的生命周期之对象销毁(垃圾回收)
一、JVM参数
在学垃圾回收器之前,我们先要知道,jvm参数是怎么回事。因为配置各种回收器,必须对应各种参数设置。
-
标准参数(-)
所有的JVM实现都必须实现这些参数的功能,而且向后兼容
- -help
- -version
-
非标准参数(-X)
默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容
- -Xint 解析模式运行,启动很快,执行稍慢
- -Xcomp 纯编译模式运行,执行很快,启动很慢
- -Xmixed 混合模式,开始解释执行,启动速度较快,对热点代码实行检测和编译。默认此模式
-
非Stable参数(-XX)
各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用
- -XX:newSize
- -XX:+UseSerialGC
jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变。
jvm的-X参数是非标准参数,也就意味着,在不同版本的jvm中,参数可能会有所不同
[root@node01 test]# java -X
-Xmixed 混合模式执行 (默认) #了解!
-Xint 仅解释模式执行 #了解!
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小 #掌握!
-Xmx<size> 设置最大 Java 堆大小 #掌握!
-Xss<size> 设置 Java 线程堆栈大小 #掌握!
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项, 如有更改, 恕不另行通知。
-XX参数也是非标准参数,主要用于改变jvm的一些基础行为,比如垃圾回收行为、jvm的调优、输出debug调试信息等。
#行为参数(功能开关)
-XX:-DisableExplicitGC 禁止调用System.gc();但jvm的gc仍然有效
-XX:+MaxFDLimit 最大化文件描述符的数量限制
-XX:+ScavengeBeforeFullGC 新生代GC优先于Full GC执行
-XX:+UseGCOverheadLimit 在抛出OOM之前限制jvm耗费在GC上的时间比例
-XX:-UseConcMarkSweepGC 对老生代采用并发标记交换算法进行GC
-XX:-UseParallelGC 启用并行GC
-XX:-UseParallelOldGC 对Full GC启用并行,当-XX:-UseParallelGC启用时该项自动启用
-XX:-UseSerialGC 启用串行GC
-XX:+UseThreadPriorities 启用本地线程优先级
#性能调优
-XX:LargePageSizeInBytes=4m 设置用于Java堆的大页面尺寸
-XX:MaxHeapFreeRatio=70 GC后java堆中空闲量占的最大比例
-XX:MaxNewSize=size 新生成对象能占用内存的最大值
-XX:MaxPermSize=64m 老生代对象能占用内存的最大值
-XX:MinHeapFreeRatio=40 GC后java堆中空闲量占的最小比例
-XX:NewRatio=2 新生代内存容量与老生代内存容量的比例
-XX:NewSize=2.125m 新生代对象生成时占用内存的默认值
-XX:ReservedCodeCacheSize=32m 保留代码占用的内存容量
-XX:ThreadStackSize=512 设置线程栈大小,若为0则使用系统默认值
-XX:+UseLargePages 使用大页面内存
#调试参数
-XX:-CITime 打印消耗在JIT编译的时间
-XX:ErrorFile=./hs_err_pid<pid>.log 保存错误日志或者数据到文件中
-XX:-ExtendedDTraceProbes 开启solaris特有的dtrace探针
-XX:HeapDumpPath=./java_pid<pid>.hprof 指定导出堆信息时的路径或文件名
-XX:-HeapDumpOnOutOfMemoryError 当首次遭遇OOM时导出此时堆中相关信息
-XX:OnError="<cmd args>;<cmd args>" 出现致命ERROR之后运行自定义命令
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" 当首次遭遇OOM时执行自定义命令
-XX:-PrintClassHistogram 遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同
-XX:-PrintConcurrentLocks 遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同
-XX:-PrintCommandLineFlags 打印在命令行中出现过的标记
-XX:-PrintCompilation 当一个方法被编译时打印相关信息
-XX:-PrintGC 每次GC时打印相关信息
-XX:-PrintGCDetails 每次GC时打印详细信息
-XX:-PrintGCTimeStamps 打印每次GC的时间戳
-XX:-TraceClassLoading 跟踪类的加载信息
-XX:-TraceClassLoadingPreorder 跟踪被引用到的所有类的加载信息
-XX:-TraceClassResolution 跟踪常量池
-XX:-TraceClassUnloading 跟踪类的卸载信息
-XX:-TraceLoaderConstraints 跟踪类加载器约束的相关信息
上面列举了很多JVM参数,我们不需要记,而是用到的时候知道怎么查就行了,我们在接下来的介绍中就会用到一些上面的参数,如有不懂得,可以上来查
二、垃圾回收
回收事件三要素
在哪收(地点)
-
程序计数器、jvm虚拟机栈、本地方法栈,这些随着线程诞生和消亡,线程释放它就释放,无需回收。
-
方法区,这里是一些类信息和静态变量,也有回收的可能性,但是很鸡肋,收不回多少东西。
实际上,虚拟机规范也并不强制要求回收这里。
-
堆,这才是大头。因为运行期频繁创建和丢弃对象的事件都在这里发生!
什么时候收(时间)
-
在堆内存存储达到一定阈值之后
当年轻代或者老年代达到一定阈值,Java虚拟机无法再为新的对象分配内存空间了,那么Java虚拟机就会触发一次GC去回收掉那些已经不会再被使用到的对象
-
主动调用System.gc() 后尝试进行回收
手动调用System.gc()方法,通常这样会触发一次的Full GC,所以一般不推荐这个东西的使用,你会干扰jvm的运作
回收谁(人物)
回收谁?哪些对象能够被回收,哪些还不能?总得有个判断标准。
在编程语言界,有两种办法判定一个对象是否已消亡:
1)引用计数法
假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用问题。(最大的缺点)
这个了解即可,因为JVM没采用这个方法
2)可达性分析
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,就说明从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的,就是可以回收的对象。
在JVM虚拟机中,可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象(重点)
- 在方法区中类静态属性引用的对象(类变量)。(重点)
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。(重点)
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- 所有被同步锁(synchronized关键字)持有的对象。(重点)
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
再谈对象的四类引用
-
强引用
- 在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。
- 无论任何情况下,内存用不回收,够就够,不够抛内存溢出异常。
-
软引用
- 用来描述一些还有用,但非必须的对象。被SoftReference包装的那些类
- 先回收没用的对象,收完后发现还不够,再触发二次回收,对软引用对象下手。
-
弱引用
- 用来描述那些非必须对象,强度比软引用更弱。被WeakReference包装的那些类
- 无论当前内存是否足够,垃圾收集一旦发生,弱引用直接回收。
-
虚引用(实际开发基本不用)
- 最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
- 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
三、回收算法(策略)
标记清除法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 标记:从根节点开始标记引用的对象。
- 清除:未被标记引用的对象就是垃圾对象,清理掉。
- 执行效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
- 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
标记压缩算法
也叫标记-整理,标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。
和标记清除算法一样,也是从根节点开始,对对象的引用进行标记
在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。可以理解成清除的时候做了内存整理,见下图
- 该算法解决了标记清除算法的碎片化的问题,下一步分配内存的时候更方便
- 多了一步整理操作,对象需要移动内存位置,效率也好不到哪去。
标记复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
优点:
- 在垃圾对象多的情况下,效率较高,因为要把存活的全部移动一遍
- 清理后,内存无碎片
缺点:
- 在垃圾对象比例少的情况下,不适用,如:年轻代这么用可以,老年代就不合适
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
年轻代的标记复制算法
年轻代内存的回收就是典型的标记复制法
- sruvivor区有两个,一个from,另一个叫to,这俩交替互换角色
- 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
- 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
- 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置) 的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
- 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
- GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
分代GC的思想
确切的说,分代不算是一种算法,它是一种解决回收问题的思路:具体情况具体分析
在堆内存中,有些对象短暂存活有些则是长久存活,所以需要将堆内存进行分代,将短暂存活的对象放到一起,进行高频率的回收,长久存活的对象集中放到一起,进行低频率的回收
细粒度的控制不同区域,调节不同的回收频率,节约系统资源(回收期间系统要额外干活的!)。
分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
一些英文单词概念:
Minor GC/Young GC:新生代收集;
Major GC/Old GC:指目标只是老年代的垃圾收集。(CMS收集器)
Mixed GC:指目标是收集整个新生代以及部分老年代的垃圾收集。(G1收集器)
Full GC:所有的内存整理一遍,包括堆和方法区。轻易不要触发
四、回收器(执行者)
前面我们讲了垃圾回收的算法,还需要有具体的实现。
策略有了,谁来执行呢?这事就落到任劳任怨的回收器头上了
在jvm中,实现了多种垃圾收集器,这些收集器种类繁多,看似乱七八糟,其实理清楚后很简单。
1)基本概念术语等
- 用户线程:java程序运行后,用户不停请求操作jvm内存,这些称为用户线程
- GC线程:jvm系统进行垃圾回收启动的线程
- 串行:GC采用单线程,收集时停掉用户线程
- 并行:GC采用多线程,收集时同样要停掉用户线程
- 并发:用户线程和GC线程同步进行,这意义就不一样了
- STW:stop the world ,暂停响应用户线程,只提供给GC线程工作来回收垃圾(很不爽的事情)
- 分代:垃圾回收器是要工作在某个代上的,可能是年轻代,老年代,有的可能两个代都能工作
- 组合:因为分代,所以得有组合,你懂得……
2)串行回收器
其实是两个收集器,年轻代的叫 Serial , 老年代的叫 Serial Old,很好记!
这是最基础的,历史最悠久的收集器。
GC时,停掉用户线程,同时,GC本身也是只有一个线程在跑
使用方式 -XX:+UserSerialGC
# 为了测试GC,将堆的初始和最大内存都设置为16M
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m
启动程序,可以看到下面打印出来的详细GC信息
[GC (Allocation Failure) [DefNew: 4416K->512K(4928K), 0.0046102 secs] 4416K->1973K(15872K), 0.0046533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 10944K->3107K(10944K), 0.0085637 secs] 15871K->3107K(15872K), [Metaspace: 3496K->3496K(1056768K)], 0.0085974 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
-
DefNew
- 表示使用的是串行垃圾收集器。
-
4416K->512K (4928K)
- 表示,年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K
-
0.0046102 secs
- 表示,GC所用的时间,单位为秒。
-
4416K->1973K (15872K)
- 表示,GC前,堆内存占有4416K,GC后,占有1973K,总大小为15872K
-
Full GC
- 表示,内存空间全部进行GC,老年代、元空间
3)并行回收器
-
ParNew回收器:
新生代的,无非就是将Serial的单线程换成多线程,它现在存在的唯一价值就是作为新生代收集器配合老年代的CMS收集器一起工作,并且在jdk9里也已不再推荐这套组合,而是推荐G1。
我们只需要知道的是:曾经,它存在过。
-
另外一对并行回收器:
Parallel Scavenge (新生代的) / Parallel Old (老年代的)
-
-XX:+UseParallelGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
-
-XX:+UseParallelOldGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
#打印的信息
[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)] 4096K->1840K(15872K), 0.0034307 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 505K->0K(4608K)] [ParOldGen: 10332K->10751K(11264K)] 10837K->10751K(15872K), [Metaspace: 3491K->3491K(1056768K)], 0.0793622 secs] [Times: user=0.13 sys=0.00, real=0.08 secs]
PSYoungGen:年轻代,Parallel Scavenge
ParOldGen:老年代,Parallel Old
4)并发 - CMS
CMS收集器,工作在老年代。 ParNew
前面的收集器都是要停止用户线程的,而CMS收集器这是真正意义上的并行处理器,也就是用户线程和GC线程在同一时间一起工作。
- 初始化标记 :标记root直接关联的对象(只扫描可达性分析中的第一层根节点),会导致stw,但是这个没多少对象,时间短
- 并发标记:沿着上一步的root,往下追踪,这步耗时最长,但是与用户线程同时运行
- 重新标记:因为上一步是并发进行的,所以再增量过一遍有变化的,会导致stw,但比上一步少很多
- 并发清理:标记完的干掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程同时运行
- 重置线程:重置状态等待下次CMS的触发,与用户线程同时运行
#设置启动参数
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m
#运行日志
#注意,cms默认搭配的新生代是 parnew :
[GC (Allocation Failure) [ParNew: 4926K->512K(4928K), 0.0041843 secs] 9424K->6736K(15872K), 0.0042168 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#老年代开始:
#第一步,初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6224K(10944K)] 6824K(15872K), 0.0004209 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第二步,并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第三步,预处理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第四步,重新标记
[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan (parallel) , 0.0005811 secs][weak refs processing, 0.0000136 secs][class unloading, 0.0003671 secs][scrub symbol table, 0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS-remark: 6224K(10944K)] 7881K(15872K), 0.0018324 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第五步,并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
优点:
- 不可否认,一款优秀的收集器,并发收集,低停顿。
- 互联网服务器上低停顿的现实要求很吻合,一个网站总不能告诉用户你用10分钟,歇会再来用。
但是,CMS也不是完美的:
- 它不能等到内存吃紧了才启动收集。因为收集期间用户线程还在跑,得预留。
- 浮动垃圾干不掉,在并发标记、并发清理时,产生的新垃圾必须到下一次收集时处理。
- 标记-清除算法,免不了产生碎片,可以开启压缩但这些参数在jdk9里也已废弃掉
- 最后,搭配CMS的年轻代现在只剩下了ParNew,是那么的苍白无力。实际上,jdk9开始已经把它逐步淘汰
5)并发 - G1
G1 的主要目标是提供一种低延迟的垃圾收集方案,适用于大堆内存的应用程序。G1 收集器通过将堆内存划分为多个小的区域(Region),并采用并发和增量的方式进行垃圾收集,从而实现高效的内存管理。
这些region可以是Eden、Survivor、Old、Humongous(专门存大对象的老年区)
这些区在物理地址上不再连续。而是把整个物理地址分成一个个大小相等的region,每一个region可以是上面角色中的一个,还可以在某个时刻转变角色,从eden变成old !(就是个标签)
这样收集的时候,它收集某些性价比高的region回收就可以了。所以某个时刻,G1可能连老带少一起收拾。
那它是怎么做的呢?收拾哪些region呢?
先看两个概念,容易搞混:
-
Remembered Set:记忆集,简称RS,每个 Region关联一个。RS 比较复杂,简单来说就是记录Region之间对象的引用关系。
-
Collection Set:简称CSet,在一次收集中,那些性价比高的Region揪出来组成一个回收集,将来一口气回收掉。这个集合里是筛选出来的一些Region
至于Region里面剩下的存活的对象,多个Region压缩到一个空闲Region里去,这样就完成了一次收集。
G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
所谓的模式,其实也就是G1收集的时候,Region选哪种,是只选年轻代的Region?还是两种都筛选?
- Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
- Mixed GC
选定所有年轻代里的Region,外加统计的在用户指定的开销目标范围内选择收益高的老年代Region。
- full GC
严格意义上讲,这不属于G1的模式。但是使用G1时是有可能发生的。
当mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会改为使用serial old GC(full GC)来收集整个堆。
-
初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,STW,单线程执行。
-
并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
-
重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录。STW,并发执行。
-
筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出CSet后移动合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必须STW。
思考一下,这属于什么算法呢???
答:从Region的动作来看G1使用的是标记-复制算法。而在全局视角上,类似标记 - 整理
总结:
G1前面的几步和CMS差不多,只有在最后一步,CMS是标记清除,G1需要合并Region属于标记整理
优缺点
- 并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并发标记阶段。其他还是需要STW
- 分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代;
- 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。
- 可预见性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。