JVM的运行时数据区之Java堆and本地方法栈
一、本地方法栈 (Native Method Stack)
1、java虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法的调用。
2、本地方法栈也是线程私有的。
3、允许被实现成固定或者是可动态扩展的内存大小。内存溢出方面也是相同的。如果线程请求分配的栈容量超过本地方法允许的最大容量则会抛出StackOverflowError异常,这个在概述里面提到过。
4、本地方法是用C语言写的。
5、它的具体做法是在Native Method Stack栈中登记native方法,在执行引擎Execution Engine执行时加载到本地方法库。
二、Java堆内存
1、堆内存概述
(1)一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
(2)Java堆区在JVM启动时的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间。
(3)堆内存是可以调节的。
例如:-Xms:10m(起始堆大小) -Xmx:30m(堆最大内存大小)
一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。
(4)《Java虚拟机规范》中规定,对可以处于物理上不连续的内存空间,但逻辑上它应该被视为连续的。
(5)所有的线程共享java堆,在这里还可以划分私有的缓冲区。
(6)《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时分配在堆上。
(7)在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
(8)堆是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
2、堆内存区域划分
Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为Eden(伊甸园)区和Survivor(幸存者)区
为什么会分区(代)?
将对象根据存活率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法实行扬长避短。
4、对象创建内存分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
(1)new的新对象先放到伊甸园区,此区有大小限制。
(2)当伊甸园区的空间填满时,程序有需要创建对象是,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁,再加载新的对象放到伊甸园区
(3)然后将伊甸园区中剩余对象移动到幸存者0区。
(4)如果再次出现垃圾回收,此时上次幸存下来的放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次必须保证有一个幸存者是空的。
(5)如果再次经历垃圾回收 ,此时会重新放回幸存者0区,接着再去幸存者1区。
(6)什么时候去养老区呢?默认是15次,也可以设置参数,最大值为15
-XX:MaxTenuringThreshold=<N>
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111, 即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。
(7)在老年区,相对悠闲,当养老区内存不足时,再次触发Major GC,进行养老区的内存清理。
(8)若养老区执行了Major GC之后发现依然无法进行对象保存,就会产生OOM异常。
例如:
public class Demo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
while (true){
list.add(new Random().nextInt());
}
}
}
死循环入栈直到内存溢出。会爆出异常,如下:
对象在堆内存中的分配过程总结:
新创建的对象放在伊甸园区,第一次垃圾回收时,垃圾对象会被直接回收,存活下来的对象会将它存放到幸存者0或幸存者1区,再次垃圾回收时,如果没有被垃圾回收的存放在幸存者0区或者幸存者1区的对象会被转移到幸存者0区或者幸存者1区,每次必须保证有一个幸存者0区或者1区是空的(即就是保持内存是完整的),当对象经过15次垃圾回收之后还存活那么会将该对象转移到老年区。老年区的对象被垃圾回收的效率都很低
在E:\Program Files\Java\jdk1.8.0_261\bin在java的jdk的bin目录下有一个jvisualvm.exe检测工具
在该检测工具里面有一个Visual GC
5、新生区与老年区配置比例
配置新生代与老年代在堆结构的占比(一般不会调)
1、默认-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3
2、可以修改-XX:NewRatio=4
,表示新生代占1,老年代占4,新生代占整个堆的1/5
3、当发现在整个项目中,生命周期承德对象偏多,那么就可以通过调整老年代的大小,来进行调优。
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项 -XX:SurvivorRatio 调整这个空间比例。比 如-XX:SurvivorRatio=8 新生区的对象默认生命周期超过 15 ,就会去养老区养老
6、分代收集思想Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都是新生区和老年区一起回收的,大部分时候回收的都是指新生区。针对HotSpotVM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
部分收集: 不是完整收集整个java堆的垃圾收集。其中又分为:
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集。
老年区收集(Major GC / Old GC):只是老年区的垃圾收集。
整堆收集(Full GC): 收集整个Java堆和方法区的垃圾收集。
整堆收集出现的情况:
(1)System.gc();时
(2)老年区空间不足
(3)方法区空间不足
开发期间尽量避免整堆收集.
7、TLAB机制
为什么会有TLAB(Thread Local Allocation Buffer)机制?
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB?
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
如果设置了虚拟机参数-XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自 己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。 TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以 通过选项-XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的 百分比大小。
8、堆空间的参数设置
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
-Xms:初始堆空间内存(默认为物理内存的 1/64)
-Xmx:最大堆空间内存(默认为物理内存的 1/4)
-Xmn:设置新生代的大小(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
9、字符串常量池
字符串常量池为什么要调整位置?
JDK7 及以后的版本中将字符串常量池放到了堆空间中。因为方法区的回收效率 很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空 间不足、方法区不足时才会触发。 这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建, 回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。