JVM之堆空间(上)|Java 开发实战

414 阅读4分钟

这是我参与更文挑战的第 3 天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看活动链接

1.概述

  • 一个JVM实例只有一个堆内存,是Java内存管理的核心区域。
  • 堆在JVM启动的时候创建,其大小也已经确定,是JVM中最大的一块空间。
  • 堆在物理上可以不是一段连续的内存空间,但逻辑上应该被视为连续的。
  • 所有的线程共享堆,可以划分线程私有的缓冲区。
  • 几乎(逃逸分析)所有的对象实例以及数组都应该在运行时分配在堆上。
  • 堆中的对象在方法结束后并不会马上被移除,仅仅在GC的时候才会被移除。
  • 堆是GC的重点区域。
  • 堆空间的内存结构逻辑上分为三个区域:新生区+养老区+元空间(1.7之前叫永久代),实际上元空间的具体实现在方法区内。

2.堆空间内存大小设置

  • -Xms设置堆空间的初始大小
  • -Xmx设置堆空间最大的大小
  • 默认情况下初始内存为电脑内存的1/64,最大内存为电脑内存的1/4
  • Runtime.getRuntime().totalMemory() //获取最堆空间内存初始值
  • Runtime.getRuntime().maxMemory() //获取最堆空间内存最大值
  • 初始值和最大值最好设置一样的,避免频繁扩容。
  • 其中内存获取到的内存中,两个幸存者区只包含一个区域的内存,两个幸存者区同一个时间只有一个在被使用。
  • 使用 -XX:+PrintGCDetails 参数打印GC的详细细节

3.堆空间分代与对象分配

新生代和老年代

存储在JVM中的Java对象可以被划分为两类:

  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
  • 另一类对象的生命周期非常长,甚至和JVM的生命周期一样。

堆空间中的划分:

堆空间划分12313131321

新生代和老年代结构占比:

  • 默认 -XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 修改 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

新生代区域中Eden区和Survivor区占比:

  • Eden区和Survivor区默认占比为 8:1:1
  • 使用 -XX:SurvivorRatio=8 来设置Survivor区的占比,使用 -Xmn:设置新生代大小(一般不设置),如果与-XX:NewRatio=2参数冲突,以-Xmn:为准。

虽然Eden区和Survivor区默认占比为 8:1:1,但是实际上通过 jstat -gc 15728 命令去查看并不是这样 image-20210531004230193 设置的堆空间大小为600m,但实际显示的时候却是6:1:1,因为JVM有一个自适应的内存分配策略,如果设置为 8:1:1,就必须加上参数 -XX:SurvivorRatio=8

对象内存分配

对象分配过程:

  • 创建对象绝大部分都在Eden区,有大小限制,如果超过限制可能创建在老年代。
  • 如果Eden区满了还创建对象,就会触发(Minor GC),将Eden区没有被引用的对象销毁,把剩下没有被销毁的对象移动到Survivor 0区,并且给一个阈值下标1。加载新创建的对象进Eden区。
  • 如果再次触发Minor GC,除了上一步的操作之外,还会把Survivor 0区的没有被回收对象移动到Survivor 1区,此时Survivor 0 区就为空,也即to区,Survivor 1区为From区(谁空谁是To)。
  • 每次触发GC都重复上一步,两个Survivor区互相转移,并且每次幸存下来的对象下标都会加1。
  • 当幸存者区的对象下标为15时,会移动到老年代。默认15,可以使用-XX:MaxTenuringThreshold=N参数来设置。如果Survivor区满了,对象也可能直接进入老年代。

TLAB:

  • why:由于堆区时线程共享的,任何线程都可以访问堆区中共享的数据,所以在并发环境下从内存划分内存空间是线程不安全的。
  • what:从内存模型的角度,堆Eden区继续进行划分,JVM为每个线程分配了一个线程私有的缓存区域。在多线程分配内存下使用TLAB避免一系列非线程安全的问题,可以称这种内存分配策略为夸苏分配策略。

总结:

  • 针对幸存者S1,S2区,复制之后有交换,谁空谁是To区
  • GC频繁在新生代触发,很少在老年代触发,几乎不在元空间触发。

image-20210531134954423