下面的内容如果我没有专门提及Java版本,则都是以Java8+为例,已经过期的Java7-就不过多研究了。
内容仅为个人观点,如果有不对的请留言互相学习。
1. Java内存模型
在Java8+中内存模型中主要包括:
① 堆内存(年轻代、老年代、字符串常量池)
② 其他的非堆内存,包括了元空间(类的元数据、运行时常量池)、代码缓存、线程栈
1.1. 字符串常量池和运行时常量池
这两个都是常量池,为什么一个在堆内存中,另一个在元空间中呢?
运行时常量池中存储的是字符串常量池的引用,可以理解为你把文件存到了D盘,但给这个文件创建了一个快捷方式,把快捷方式存在C盘,这样你就可以直接在C盘快速的访问D盘中的文件了。
基本类型常量(如int
、float
)会嵌入代码或存于运行时常量池。
假设我们有以下代码:
String s = "hello";
它的加载方式如下图所示:
在编译阶段时,会先根据.class文件生成符号引用的记录。
在类加载阶段,把这个生成的符号引用记录加载到运行时常量池中,此时还没有创建实际的字符串。
在运行阶段,JVM才会根据符号引用记录去字符串常量池中创建,就相当于是运行之前,JVM假装已经有了这个字符串(反正也暂时不用),运行的时候才去真的创建。
**注意:**这里有一个误区,String s = "hello"
和String s = new String("hello")
加载方式不一样。后者因为使用了new,所以在堆中创建了两个对象。
1.2. 年轻代和老年代
年轻代和老年代是堆内存的主要区域,每一个新建对象都会先进入年轻代中的Eden 区,当Eden 区满时会触发Minor GC,GC过后Eden 区幸存下来以及非空闲幸存者区的对象会通过复制算法复制到空闲的幸存者区(S0和S1始终会保持其中一个为空),复制完成后Eden区和原非空闲幸存者区对象会被清空。
每一次 Minor GC年轻代中存活的对象年龄都会+1,当幸存者区中的对象年龄≥15时会晋升至老年代中。
在Java8中会动态调整晋升阈值,如果某年龄对象总大小 > Survivor 区 50%,则≥该年龄的对象直接晋升。
这个是理想情况下是这样的,但有时候就是不太理想,比如创建的对象太大、幸存者区放不下的情况都会导致对象直接被放到了老年代中。
而晋升到老年代的对象太多时,就会导致频繁的Full GC,,导致性能下降甚至OOM,如果确实业务需要,可以考虑增加堆空间大小(-Xmx
)或者调整老年代的比例(默认老年代和年轻代是2/1)。
1.3. 永久代和元空间
Java8之前的版本中还有一个叫永久代的东西,在Java8中大部分作用被元空间取代了,但也并不是替换这么简单,Java8之前的永久代跟年轻代、老年代一样是在堆内存中,现在的元空间并没有在堆内存中。
由于永久代是堆内存的一部分,所以它的内存空间是固定的。而元空间并不受堆内存的影响,它是动态扩展,你电脑系统的内存有多大它就能用多大,当然你也可以通过配置JVM参数-XX:MaxMetaspaceSize
设置上限。
上文说到的字符串常量池在Java8之前是在永久代中的,Java8之后字符串常量池的“老大”永久代被干掉了,字符串常量池翻身当大哥了,直接属于堆内存中的一块空间了。
1.4. GC行为
上文中多次出现Minor GC和Full GC并不是一种具体的垃圾回收算法,而是指一种行为,Minor GC是指局部回收,一般用在年轻代中的回收,所以也被称 Young GC。而Full GC是指全局回收,整个堆内存和元空间都会被回收。
在GC过程中会STW(Stop-The-World),就是说这个期间会把正在运行的线程暂停,是为了对象引用不会在这期间修改,可以类比一个清洁工来办公室清理垃圾,但得让你们腾地方,就会导致此期间无法工作。一般Minor GC行为的STW时间是毫秒级,但Full GC是全堆清理,它的STW时间一般都得秒级,甚至能达到30s以上。
除了上述两种GC行为,还有另一种Java9+才应用的Mixed GC行为,即混合行为。就是为了防止上述说的Full GC时秒级的STW。Mixed GC能让垃圾回收像回收年轻代一样回收老年代,不需要全堆的停顿,一般是将回收过程分多次进行,这样也可以分摊停顿时间。
但被用的最多的Java8当老年代满时,就会触发Full GC,不会进行Mixed GC,要在Java 8中使用Mixed GC需要手动配置,并且不太稳定,建议是直接升级JDK版本到JDK11+。
如果需要在Java8中使用Mixed GC行为,可以通过配置如下参数:
-XX:+UseG1GC # 使用G1垃圾回收器
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的老年代阈值(老年代占堆内存比例)
-XX:G1MixedGCLiveThresholdPercent=85 # Mixed GC回收区域的存活率上限(存活对象 > 85% 的区域不回收)
-XX:G1HeapWastePercent=10 # 碎片空间占堆内存比例,也就是允许10%空间浪费
调优指南
① -XX:G1MixedGCLiveThresholdPercent=85可能会因为太多区域被跳过,导致每次GC堆释放空间太少,从而导致频繁的GC。需要根据自己系统业务,判断是否会存在这个情况来调整比例。
② -XX:G1HeapWastePercent=10可能会导致堆中有大量碎片空间,但还没有达到10%的这个阈值,可以根据自己系统堆空间大小来实际调整。
1.5. GC类型
六大GC类型垃圾收集器对比表
收集器 | 年轻代算法 | 老年代算法 | 优势 | 适用场景 |
---|---|---|---|---|
Serial GC | 复制 | 标记-整理 | 单线程低开销 | 客户端/嵌入式(资源受限) |
Parallel GC | 复制(多线程) | 标记-整理(多线程) | 高吞吐量 | 计算密集型后端 |
CMS | 复制 | 并发标记-清除 | 低停顿(老年代) | 响应敏感的 Web 服务(已废弃) |
G1 GC | 复制(Region) | 复制+标记-整理 | 平衡吞吐与停顿 | 大堆内存(6GB~16GB) |
ZGC | 并发复制 | 并发复制 | 亚毫秒停顿 | 超大堆(TB级)、低延迟要求 |
Shenandoah GC | 并发复制 | 并发复制 | 低停顿+高吞吐 | 大堆内存、混合负载 |
JDK 8 默认:Parallel GC(吞吐优先)
JDK 9~14 默认:G1 GC(平衡型)
JDK 15+ :ZGC/Shenandoah(极致低延迟)
下面是主要的几个GC收集器的STW停顿时间