Java垃圾回收
复习:垃圾回收的主要区域就是 JVM 堆,也称堆内存。堆内存和方法区共同组成线程的共享区域。方法区存放类,堆存放对象。方法区在 JDK 1.8时被移动到本地内存中,不会收到 JVM 内存的限制。
类和对象的创建过程?线程私有区域有哪些?分别有什么用?

JVM 堆主要存放的是对象,它主要分为新生代和老年代(JDK1.8之前还有永久代暂时不做考虑),新生代又分为Eden和2个Suivivor区域。
分代回收机制_3代
Minor GC / Young GC:Eden放不下新创建的对象时触发,负责对新生代回收。
Minor GC一般是用标记-复制算法,因为对象存活时间比较短。该算法首先回收Eden和Survivor中的垃圾对象,并将存活对象的年龄+1复制到空闲的S区;这些存活对象也可能被放到老年代去,具体得看对象的年龄阈值和S区剩余空间,即动态年龄计算。
动态年龄计算:将S区的对象大小,从小到大依次累加,直到S区空间的一半,此时取min{该对象的年龄,年龄阈值}作为进入老年代的年龄基准;如果Eden区的对象太大,则直接放到老年代。Major GC / Old GC:负责对老年代进行回收,但除了CMS垃圾回收器之外,都是在Full GC时对老年代回收的
Full GC:整堆回收,也会对方法区进行垃圾收集;会在 老年代空间不足/元空间不足/堆分配担保失败 时触发,包含Minor和Major GC,还包括回收方法区。
不同对象的存活时长是不一样的,也就可以针对不同的对象采取不同的垃圾回收算法,默认几乎所有的垃圾收集器都是采用分代收集算法进行垃圾回收的。
我们会把堆分为新生代和老年代:
- 新生代中的对象存活时间比较短,那么就可以利用复制算法,它适合垃圾对象比较多的情况。
- 老年代中的对象存活时间比较长,所以不太适合用复制算法,可以用标记-清除或标记-整理算法,比如:
- CMS垃圾收集器采用的就是标记-清除算法
- Serial Old垃圾收集器采用的就是标记-整理算法
方法区的回收
主要包含类卸载和运行时常量池回收,常量池(例如字符串字面量,类型/字段/方法的符号引用)的回收往往随着类卸载一同进行。需要判断是否还有被引用/
引用和死亡
死亡判断方法_2种
在垃圾回收之前,得标记哪些对象需要被回收,有2种方法:
- 引用计数法:操作简单,但是无法解决循环引用的问题,因此基本不使用;
- 可达性分析法
以GC Roots作为起始点,一层层找所引用的对象,被找到的对象就是存活对象,不可达对象就是垃圾对象; - 可以作为GC Roots的对象:
- 虚拟机栈(栈帧的局部变量表)引用的对象
- 本地方法栈(Native方法)引用的对象(Native方法也能引用Java对象)
- 方法区中类静态属性引用的对象
- 方法区中常量属性引用的对象
所有被同步锁持有的对象JNI(Java Native Interface)引用的对象
引用类型_4种
- 强引用:程序代码中普遍存在的引用赋值,垃圾回收器绝不会回收具有强引用的对象。
- 软引用:比强引用弱一点,它主要用于实现内存敏感的缓存。当内存充足时,软引用对象不会被回收;只有当内存不足时,才会被回收。例如一些图片缓存,示例如下:
1 | private static Map<String, SoftReference<byte[]>> imageCache = new HashMap<>(); |
- 弱引用:只要垃圾回收器运行,无论内存是否充足,都会回收被弱引用关联的对象。
1 | Object weakObj = new Object(); |
WeakHashMap 的键就是弱引用。当键不再被强引用时,WeakHashMap 会自动移除对应的条目,例如Class对象和相关的元数据对象(Method, Field, Constructor),或者ClassLoader和相关的类数据。
ThreadLocal也用到了弱引用
- 虚引用:主要用来跟踪对象被垃圾回收的活动。必须和引用队列(ReferenceQueue)联合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前将虚引用加入到关联的引用队列中。
程序可以通过引用队列了解被引用的对象是否将要被垃圾回收,从而在对象被回收之前采取必要操作。
ReferenceQueue
即引用队列,专门与软引用SoftReference、弱引用WeakReference和虚引用PhantomReference配合使用。它提供了一种“通知”机制,可以通过轮询poll或等待remove这个引用队列,以便及时得知某个被特定引用类型(软、弱、虚)关联的对象已经被垃圾回收器“盯上”或者已经死亡。
软引用和弱引用在回收后入队,这样就无法通过.get()获取对象本身,避免访问已死对象;而虚引用在回收前入队,因为无论何时都无法通过.get()获取对象。
垃圾回收算法
下面的垃圾回收算法都是针对某块内存空间的,例如新生代、老年代等等
| 标记-清除 | 标记-整理 | 复制 | |
|---|---|---|---|
| 速度/效率 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(有碎片) | 少(无碎片) | 最多(无碎片) |
| 移动对象(修改栈帧引用) | 否 | 是 | 是 |
标记-清除 Mark-Sweep 算法
当内存不足时,进行STW(Stop The World),暂停用户线程的执行,然后执行如下:
- 标记阶段:从GC Roots开始,找到可达对象,并在对象头中进行记录
- 清除阶段:对空间内存进行线性遍历,如果发现对象头中没有记录时可达对象,则回收之
效率不高,要遍历2次,并且存在内存碎片;不需要修改栈帧中的引用
复制 Copying 算法
- 将内存空间分为两块(例如S0和S1),每次指使用一块;
- 在进行垃圾回收时,将可达对象复制到另外没有被使用的内存块中,再清除当前内存块中的所有对象。其实清除与否无所谓,后续直接用就行。
- 后续再按同样的流程进行垃圾回收,交换着来。
适合可达对象不多、垃圾对象较多的情况,即新生代;不会产生内存碎片,但是始终有一半内存空闲;需要修改栈帧中的引用;标记的是可达的,所以复制算法只遍历1次,而MarkSweep清除的是不可达的,所以需要遍历2次

标记-整理 Mark-Compact 算法
标记:和标记-清除一样
移动:将所有存活对象移动到内存的一端
清理:清理边界外所有的空间
不会产生内存碎片,不会有空闲内存;效率偏低;需要修改栈帧中的引用
垃圾收集器

- Serial GC 和 Serial Old GC:复制算法和标记-整理算法,都是串行的;
- ParNew GC 和 CMS GC:前者是Serial GC的多线程版,后者是低暂停的,但是被移除了,有4个步骤:初始标记、并发标记、重新标记、并发清除;
- Parallel (Scavenge) GC 和 Parallel Old GC:前者也是Serial GC的多线程版,但比起ParNew GC,能动态调整内存分配情况,比如CMS更关注CPU的效率,而非用户的停顿时间;
- G1(garbage-first):JDK9之后默认的垃圾收集器,直到现在。G1收集器在后台维护了一个Region的优先列表,每次根据允许的收集时间,优先选择回收价值最大的内存区域Region;
- ZGC:ZGC和G1还没有细看https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q
整堆回收
常见参数
内存参数:
-Xms<size>(等同于-XX:InitialHeapSize=<size>,memory start)设置JVM启动时初始堆内存大小,例如
java -Xms512m MyWebApp,默认物理内存/64。-Xmx<size>(等同于-XX:MaxHeapSize=<size>, memory max)设置 JVM 运行时最大堆内存大小,例如
java -Xmx2g MyWebApp,默认物理内存/4。一般让-Xmshe-Xmx的值一样,这样JVM就不会去修改内存大小。-Xmn<size>(等同于-XX:NewSize=<size>, memory new)设置新生代(Young Generation)的初始大小
-XX:NewRatio配置新生代和老年代的比例,默认为2,即新生代占整个堆的1/3,老年代占2/3;如果明确知道存活时间长的对象偏多,可以调大比例-XX:SurvivorRatio配置Eden:S0:S1的比例关系,默认Eden占8/10,两个s区分别1/10.元空间的初始容量和最大容量:
-XX:MetaspaceSize=<size>和-XX:MaxMetaspaceSize=<size>。默认情况下会自动扩容,直到达到系统内存耗尽。
垃圾回收参数:
-XX:+UseSerialGC:Serial + Serial Old 收集器-XX:+UseParNewGC和-XX:+UseConcMarkSweepGC:ParNew和CMS收集器-XX:+UseParallelGC:Parallel Scavenge + Parallel Old 收集器- -XX:+UseG1GC 和 -XX:+UseZGC
监控排查:
-XX:+PrintGC和-XX:+PrintGCDetails:打印GC日志,后者更详细-XX:+PrintCommandLineFlags:打印运行时的命令参数





