复习:垃圾回收的主要区域就是 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. 软引用:比强引用弱一点,它主要用于实现内存敏感的缓存。当内存充足时,软引用对象不会被回收;只有当内存不足时,才会被回收。例如一些图片缓存,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static Map<String, SoftReference<byte[]>> imageCache = new HashMap<>();

public static byte[] getImage(String imageUrl) {
SoftReference<byte[]> softRef = imageCache.get(imageUrl);
if (softRef != null && softRef.get() != null) {
System.out.println("从缓存中获取图片: " + imageUrl);
return softRef.get(); // 缓存命中
} else {
System.out.println("从网络加载图片并放入缓存: " + imageUrl);
// 模拟从网络加载图片(这里用一个大数组模拟)
byte[] imageData = new byte[1024 * 1024 * 5]; // 5MB 图片数据
softRef = new SoftReference<>(imageData);
imageCache.put(imageUrl, softRef);
return imageData;
}
}
  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也用到了弱引用

  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:打印运行时的命令参数