复习:垃圾回收的主要区域就是 JVM 堆,也称堆内存。堆内存和方法区共同组成线程的共享区域。方法区存放存放对象。方法区在 JDK 1.8时被移动到本地内存中,不会收到 JVM 内存的限制。

类和对象的创建过程?线程私有区域有哪些?分别有什么用?


JVM内存总图

JVM 堆主要存放的是对象,它主要分为新生代和老年代(JDK1.8之前还有永久代暂时不做考虑),新生代又分为Eden和2个Suivivor区域。

分代回收机制_3代

  1. Minor GC / Young GC:Eden放不下新创建的对象时触发,负责对新生代回收。
    Minor GC一般是用标记-复制算法,因为对象存活时间比较短。该算法首先回收Eden和Survivor中的垃圾对象,并将存活对象的年龄+1复制到空闲的S区;这些存活对象也可能被放到老年代去,具体得看对象的年龄阈值和S区剩余空间,即动态年龄计算。
    动态年龄计算:将S区的对象大小,从小到大依次累加,直到S区空间的一半,此时取min{该对象的年龄,年龄阈值}作为进入老年代的年龄基准;如果Eden区的对象太大,则直接放到老年代。

  2. Major GC / Old GC:负责对老年代进行回收,但除了CMS垃圾回收器之外,都是在Full GC时对老年代回收的

  3. Full GC:整堆回收,也会对方法区进行垃圾收集;会在 老年代空间不足/元空间不足/堆分配担保失败 时触发,包含Minor和Major GC,还包括回收方法区。


不同对象的存活时长是不一样的,也就可以针对不同的对象采取不同的垃圾回收算法,默认几乎所有的垃圾收集器都是采用分代收集算法进行垃圾回收的。

我们会把堆分为新生代和老年代:

  • 新生代中的对象存活时间比较短,那么就可以利用复制算法,它适合垃圾对象比较多的情况。
  • 老年代中的对象存活时间比较长,所以不太适合用复制算法,可以用标记-清除或标记-整理算法,比如:
    • CMS垃圾收集器采用的就是标记-清除算法
    • Serial Old垃圾收集器采用的就是标记-整理算法

方法区的回收

主要包含类卸载运行时常量池回收,常量池(例如字符串字面量,类型/字段/方法的符号引用)的回收往往随着类卸载一同进行。需要判断是否还有被引用/

引用和死亡

死亡判断方法_2种

在垃圾回收之前,得标记哪些对象需要被回收,有2种方法:

  1. 引用计数法:操作简单,但是无法解决循环引用的问题,因此基本不使用;
  2. 可达性分析法
    以GC Roots作为起始点,一层层找所引用的对象,被找到的对象就是存活对象,不可达对象就是垃圾对象;
  3. 可以作为GC Roots的对象:
    1. 虚拟机栈(栈帧的局部变量表)引用的对象
    2. 本地方法栈(Native方法)引用的对象(Native方法也能引用Java对象)
    3. 方法区中类静态属性引用的对象
    4. 方法区中常量属性引用的对象
    5. 所有被同步锁持有的对象
    6. JNI(Java Native Interface)引用的对象

引用类型_4种

  1. 强引用:程序代码中普遍存在的引用赋值,垃圾回收器绝不会回收具有强引用的对象。
  2. 软引用:比强引用弱一点,它主要用于实现内存敏感的缓存。当内存充足时,软引用对象不会被回收;只有当内存不足时,才会被回收。例如一些图片缓存,示例如下,需要添加-Xmx64m命令,表示jvm内存最大为64MB:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 创建软引用对象
Object softObj = new Object();
SoftReference<Object> softRef = new SoftReference<>(softObj);
System.out.println("初始状态 - 软引用对象存在: " + (softRef.get() != null));

// 2. 解除强引用,但软引用仍保留对象(内存充足时)
softObj = null;
System.gc(); // 建议GC,但软引用在内存充足时不会被回收
try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("GC后(内存充足)- 软引用对象仍存在: " + (softRef.get() != null));

// 3. 模拟内存压力(触发软引用回收)
System.out.println("\n尝试制造内存压力...");
try {
// 分配大数组制造内存压力(根据JVM堆大小调整)
byte[][] memoryHog = new byte[1024][];
for (int i = 0; i < 1024; i++) {
memoryHog[i] = new byte[1024 * 1024]; // 每次分配1MB
}
} catch (OutOfMemoryError e) {
System.out.println("内存压力已触发: " + e.getMessage());
}

System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("内存压力后 - 软引用对象状态: " + (softRef.get() != null));

// 当内存不足时,JVM会自动清理软引用缓存,避免OOM
  1. 弱引用:只要垃圾回收器运行,无论内存是否充足,都会回收被弱引用关联的对象。
1
2
3
4
5
Object weakObj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(weakObj);
weakObj = null; // 解除强引用
System.gc(); // 尝试进行垃圾回收
System.out.println("Weak reference after GC: " + weakRef.get()); // 此时很可能为 null

WeakHashMap的键就是弱引用,这个类型是一种通用缓存。当键不再被强引用时,WeakHashMap 会自动移除对应的条目,例如Class对象和相关的元数据对象(Method, Field, Constructor),或者ClassLoader和相关的类数据。

ThreadLocal中的ThreadLocalMap类型也用到了弱引用,它的key是ThreadLocal的实例对象(记作tl)。但他们的区别在于,即使将tl=null,tl对应的value会仍然存在于每个线程独有的ThreadLocalMap对象中,直到该线程再次针对其他ThreadLocal对象进行set操作;但是WeakHashMap通过ReferenceQueue队列,当它的key失效时,内部的ReferenceQueue对象就会得到对应的Entry<K,V>,进而会惰性地将这个Entry清理了,相当于value也被清理了。

  1. 虚引用:主要用来跟踪对象被垃圾回收的活动。必须和引用队列ReferenceQueue)`联合使用。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前将虚引用加入到关联的引用队列中。
程序可以通过引用队列了解被引用的对象是否将要被垃圾回收,从而在对象被回收之前采取必要操作,这不仅仅是虚引用可以做的事情,所有引用都可以做这个事情。

ReferenceQueue

即引用队列,专门与软引用SoftReference弱引用WeakReference虚引用PhantomReference配合使用。它提供了一种“通知”机制,可以通过轮询poll或等待remove这个引用队列,以便及时得知某个被特定引用类型(软、弱、虚)关联的对象已经被垃圾回收器“盯上”或者已经死亡。

软引用弱引用在回收后入队,这样就无法通过.get()获取对象本身,避免访问已死对象;而虚引用在回收前入队,因为无论何时都无法通过.get()获取对象。

垃圾回收算法

下面的垃圾回收算法都是针对某块内存空间的,例如新生代、老年代等等

标记-清除 标记-整理 复制
速度/效率 中等 最慢 最快
空间开销 少(有碎片) 少(无碎片) 最多(无碎片)
移动对象(修改栈帧引用)

标记-清除 Mark-Sweep 算法

当内存不足时,进行STW(Stop The World),暂停用户线程的执行,然后执行如下:

  1. 标记阶段:从GC Roots开始,找到可达对象,并在对象头中进行记录
  2. 清除阶段:对空间内存进行线性遍历,如果发现对象头中没有记录时可达对象,则回收之

效率不高,要遍历2次,并且存在内存碎片;不需要修改栈帧中的引用

复制 Copying 算法

  1. 将内存空间分为两块(例如S0和S1),每次指使用一块;
  2. 在进行垃圾回收时,将可达对象复制到另外没有被使用的内存块中,再清除当前内存块中的所有对象。其实清除与否无所谓,后续直接用就行。
  3. 后续再按同样的流程进行垃圾回收,交换着来。

适合可达对象不多、垃圾对象较多的情况,即新生代;不会产生内存碎片,但是始终有一半内存空闲;需要修改栈帧中的引用;标记的是可达的,所以复制算法只遍历1次,而MarkSweep清除的是不可达的,所以需要遍历2次

copying

标记-整理 Mark-Compact 算法

  1. 标记:和标记-清除一样

  2. 移动:将所有存活对象移动到内存的一端

  3. 清理:清理边界外所有的空间

不会产生内存碎片,不会有空闲内存;效率偏低;需要修改栈帧中的引用

垃圾收集器

垃圾收集器

  1. Serial GC 和 Serial Old GC:复制算法和标记-整理算法,都是串行的;
  2. ParNew GC 和 CMS GC:前者是Serial GC的多线程版,后者是低暂停的,但是被移除了,有4个步骤:初始标记、并发标记、重新标记、并发清除;
  3. Parallel (Scavenge) GC 和 Parallel Old GC:前者也是Serial GC的多线程版,但比起ParNew GC,能动态调整内存分配情况,比如CMS更关注CPU的效率,而非用户的停顿时间;
  4. G1(garbage-first):JDK9之后默认的垃圾收集器,直到现在。G1收集器在后台维护了一个Region的优先列表,每次根据允许的收集时间,优先选择回收价值最大的内存区域Region;
  5. ZGC:ZGC和G1还没有细看https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q

整堆回收

常见参数

内存参数:

  1. -Xms<size> (等同于 -XX:InitialHeapSize=<size>,memory start)

    设置JVM启动时初始堆内存大小,例如java -Xms512m MyWebApp,默认物理内存/64。

  2. -Xmx<size> (等同于 -XX:MaxHeapSize=<size>, memory max)

    设置 JVM 运行时最大堆内存大小,例如java -Xmx2g MyWebApp,默认物理内存/4。一般让-Xmshe -Xmx的值一样,这样JVM就不会去修改内存大小。

  3. -Xmn<size> (等同于 -XX:NewSize=<size>, memory new)

    设置新生代(Young Generation)的初始大小

  4. -XX:NewRatio配置新生代和老年代的比例,默认为2,即新生代占整个堆的1/3,老年代占2/3;如果明确知道存活时间长的对象偏多,可以调大比例

  5. -XX:SurvivorRatio配置Eden:S0:S1的比例关系,默认Eden占8/10,两个s区分别1/10.

  6. 元空间的初始容量和最大容量:-XX:MetaspaceSize=<size>-XX:MaxMetaspaceSize=<size>。默认情况下会自动扩容,直到达到系统内存耗尽。

垃圾回收参数:

  1. -XX:+UseSerialGC:Serial + Serial Old 收集器
  2. -XX:+UseParNewGC-XX:+UseConcMarkSweepGC:ParNew和CMS收集器
  3. -XX:+UseParallelGC:Parallel Scavenge + Parallel Old 收集器
  4. -XX:+UseG1GC 和 -XX:+UseZGC

监控排查:

  1. -XX:+PrintGC-XX:+PrintGCDetails:打印GC日志,后者更详细

  2. -XX:+PrintCommandLineFlags:打印运行时的命令参数