不同版本JVM内存划分的变化
1.6,使用永久代(PermGen)来实现方法区,运行时常量池在方法区中。
1.7,还有永久代,运行时常量池在堆中。
1.8,没有永久代,使用元空间(在直接内存)
总结:1.7 运行时常量池到了堆,1.8方法区被元空间替换
什么变量会在栈里,什么变量会在堆里?
方法里的局部变量
对象中的成员变量
JVM 内存结构
JVM内存的划分
(1)线程私有区:
- 程序计数器,记录正在执行的虚拟机字节码的地址;
- Java虚拟机栈:Java方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧;
- 本地方法栈:虚拟机的Native方法执行的内存区;
(2)线程共享区:
-
Java堆:对象分配内存的区域;
-
方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
- 常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
程序计数器(PC 寄存器)
程序计数器的定义
程序计数器是一块较小的内存空间,是当前线程正在执行的那条字节码指令的地址。若当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined
。
程序计数器的作用
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
- 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。
Java 虚拟机栈(Java 栈)
Java 虚拟机栈的定义
Java 虚拟机栈是描述 Java 方法运行过程的内存模型。
Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口信息
压栈出栈过程
当方法运行过程中需要创建局部变量时,就将局部变量的值存入栈帧中的局部变量表中。
Java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。
方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。
堆
堆的定义
堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。
堆的特点
- 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。
- 在虚拟机启动时创建。
- 是垃圾回收的主要场所。
- 进一步可分为:新生代(Eden区 From Survior To Survivor)、老年代。
不同的区域存放不同生命周期的对象,这样可以根据不同的区域使用不同的垃圾回收算法,更具有针对性。
堆的大小既可以固定也可以扩展,但对于主流的虚拟机,堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已无法再扩展时,就抛出 OutOfMemoryError 异常。
Java 堆所使用的内存不需要保证是连续的。而由于堆是被所有线程共享的,所以对它的访问需要注意同步问题,方法和对应的属性都需要保证一致性。
方法区✨受到版本变化
JDK 1.8 的时候,方法区被彻底移除了,取而代之是元空间,元空间使用的是直接内存。
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
-XX:PermSize=N //方法区 (永久代) 初始大小 -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
JDK 1.8
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
方法区的定义
Java 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息:
- 已经被虚拟机加载的类信息
- 即时编译器编译后的代码
方法区的特点
- 线程共享。 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
- 永久代。 方法区中的信息一般需要长期存在,方法区又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。永久代是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。
- 内存回收效率低。 方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。
- Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。
运行时常量池 Runtime Constant Pool
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
运行时常量池是方法区的一部分。class文件常量池将在类加载后进入方法区的运行时常量池中存放。
一个类加载到JVM中后对应一个运行时常量池,运行时常量池相对于class文件常量池来说具备动态性,即字面量可以动态的添加。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量(基本类型包装类和String),这些常量被放在运行时常量池中。
-
字面量:字符串字面量和声明为final的常量值(基本数据类型)
-
符号引用:编译语言层面的概念,包括以下3类:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
class文件常量池(静态常量池)
class文件常量池是指编译生成的class字节码文件结构中的一个常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后,存放于方法区的运行时常量池。
字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法名和这些类与方法的字符串描述、字段(成员变量)的名称和描述符
声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。
符号引用包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用
当类被 Java 虚拟机加载后, .class 文件中的常量池就存放在方法区的运行时常量池中。
而且在运行期间,可以向常量池中添加新的常量。如 String 类的 intern() 方法就能在运行期间向常量池中添加字符串常量。
运行时常量池和字符串常量池
运行时常量池存储的是一系列字节,字符串是被序列化的,而不是字符串对象,还有其他常量,不止字符串。
字符串常量池是在运行时用的,里面都是java对象。
字符串字面量是读String类对象的引用。
Class类的对象存在JVM哪里?
Class对象是存放在堆区的。
类的元数据(元数据并不是类的Class对象,Class对象是加载的最终产品。类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。
一个对象的内存划分。
https://cloud.tencent.com/developer/article/1129494
public class Student {
private String name;
private static Birthday birthday = new Birthday();
public Student(String name) {
this.name = name;
}
public static void main(String[] args) {
Student s = new Student("zhangsan");
int age = 10;
System.out.println(age);
}
}
class Birthday {
private int year = 2010;
private int month = 10;
private int day = 1;
}

从内存区域来分析
- 虚拟机栈:只存放局部变量
- 堆:存储对象的实例
- 方法区:存放Class信息和常量信息(final的东西)。
从变量的角度来分析
-
局部变量:
存放在虚拟机栈中(具体应为[栈->栈帧->局部变量表])
- 基本类型的值直接存在栈中。如age=10
- 如果是对象的实例,则只存储对象实例的引用。如s=ref
-
实例变量:存放在堆中的对象实例中。如Student的实例变量 name=ref
-
静态变量:
存放在方法区中的常量池中。如Student.class中的birthday=ref。
- 如果常量的类型是对象的实例则只存储对象实例的引用地址
通过变量的角度来分析,我们就可以了解为什么静态变量不用new就能调用,而实例变量必须new出对象,才能调用。
参考
https://github.com/doocs/jvm/blob/master/docs/01-jvm-memory-structure.md
网友评论