这是我参与更文挑战的第 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的生命周期一样。
堆空间中的划分:
新生代和老年代结构占比:
- 默认
-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 命令去查看并不是这样
设置的堆空间大小为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频繁在新生代触发,很少在老年代触发,几乎不在元空间触发。