运行时数据区的组成
- 运行时数据区由 程序计数器、本地方法栈、java虚拟机栈、堆、方法区 组成
程序计数器
- 是一块很小的内存空间,用来记录每一个线程运行的指令位置,是线程私有的,每个线程都有一个程序计数器,生命周期与线程一致,是运行时数据区中唯一一个不会出现内存溢出的空间,运行速度最快。
本地方法栈
- 用来运行本地方法的区域,是线程私有的,空间大小可以调整,可能会出现栈溢出的情况。
Java虚拟机栈
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用。Java 虚拟机栈是线程私有的,主管 Java 程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
- 栈中会出现异常,当线程请求的深度大于虚拟机所允许的深度时,会出现StackOverflowError
栈的运行原理 - JVM直接对java栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出,后进先出的原则。
- 在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class)。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中 引用另一个线程的栈帧(方法)
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种抛出异常.不管哪种方式,都会导致栈帧被弹出
栈帧的内部结构
- 一个栈帧包含:
局部变量表(存储在方法中声明的变量)
操作数栈(实际计算运行)
动态链接
void A(){
B();//B的方法的地址
}
方法的返回地址
堆
基本作用特征
-
是存储空间,用来存储对象 ,是内存空间最大的一块儿区域
-
在jvm启动时就被创建,大小可以调整(jvm调优)
-
本区域是存在垃圾回收的,是线程共享的区域
堆内存区域划分 -
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
-
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
-
伊甸园区:对象刚刚创建
-
幸存者1区/2区 : 经历过垃圾回收
-
老年代:较少进行垃圾回收
为什么要进行分区?
- 可以根据独享存活时间来放在不同的区域,可以区别的进行对待;频繁的回收年轻代,较少的回收老年代。
创建的对象在堆中的分布
- 新创建的对象在伊甸园区
- 当发生垃圾回收时,将伊甸园中的垃圾对象直接销毁,将存活的对象移动到幸存者1区
- 之后创建的新对象还是存在于伊甸园区,再次进行垃圾回收时,将伊甸园区中的存活对象一刀幸存者2区,同时将在本次扫描中幸存者1区中存活的对象一同存放到幸存者2区,每次都确保1个幸存者区为空,来进行相互转换
- 每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历15次回收,仍然存活,就会被移动到老年代;垃圾回收次数在对象头中有一个4bit的空间记录,最大值只能是15
- 老年代的回收次数较少,当内存空间不够用时,才会去回收老年代
堆空间的配置比例
- 默认新生代:老年代 = 1 :2 ;可以通过-XX:NewRatio = 2 进行设置
- 如果项目中生命周期长的对象较多,就可以把老年代设置的更大一些
- 在新生代中,伊甸园和两个幸存者区的比例为:8:1:1;可以通过
-XX:SurvivorRatio = 8 进行设置 - 对象垃圾回收的年龄:-XX:MaxTenuringThreshold=
分代收集的思想
- JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
- 部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集,老年区收集(Major GC / Old GC):只是老年区的垃圾收集。 - 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。
- 整堆收集出现的情况:
System.gc();时
老年区空间不足
方法区空间不足
开发期间尽量避免整堆收集。
堆空间参数设置
官网地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
字符串常量池
- JDK7 及以后的版本中将字符串常量池放到了堆空间中。因为方法区的回收效率很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。
这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
public static void main(String[] args) {
String temp = "world";
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();//将字符串存储到字符串常量池中
}
}
方法区
- 主要用来存储加载类的信息,以及及时编译器编译后的信息和运行时常量池
- 特点:在jvm启动时创建,大小是可以调整,是线程共享,也会出现内存溢出
方法区、堆、栈交互关系 - 方法区存储类信息(元信息)
堆中存储创建的对象
栈中存储对象的引用
方法区大小设置
-XX:MetaSpaceSize 设置方法区的大小
windows jdk默认的大小是21MB
也可以设置为-XX:MaxMetaspaceSize 的值为-1级,没有限制。没有限制时就可以使用计算机内存
也可以设置初始值大一点,减少 FullGC的发生
方法区内部结构 - 方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。
- 运行常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、参数类型、字面量(常量)等信息,存放编译期间生成的各种字面量(常量)和符号引用。
方法区的垃圾回收
- 方法区在FullGC时进行垃圾回收
主要是回收类信息,类信息的回收条件比较苛刻,满足以下3点- 在堆中,该类及其子类的对象都不存在了
- 该类的类加载器不存在了
- 该类的Class对象不存在了
- 也可以认为类一旦加载就不会卸载了
特点总结
- 程序计数器,Java栈,本地栈是线程私有的
- 程序计数器不会出现内存溢出
- java栈,本地栈,堆,方法区,可能会出现内存溢出
- java栈,本地栈,堆,方法区大小是可以调整的
- 堆,方法区是线程共享的,是可以进行垃圾回收的