1-JVM内存模型
Java开发人员一般情况下,使用过程中,不用关注内存的申请和释放,得益于JVM自动内存分配机制,但是其实是个双刃剑,这可以提升Java开发的效率,但是弱化了开发人员内存管理意识,系统容易发生内存异常,垃圾回收(GC)的方式不合适以及GC次数过于频繁等问题,这些都将直接影响到应用服务的性能。
在Java中,JVM内存模型主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈。
1.1-堆
堆是JVM内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Survivor和To Survivor组成。
在Java6版本中,永久代在非堆内存区;到了Java7版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了Java8,永久代被元空间取代了,元空间存储静态变量和运行时常量池与Java7的永久代一样,都移到了堆中。 结构如下图所示:
1.2-程序计数器
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
1.3-方法区
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息。
方法区只是JVM中规范的一部分,可以说,在HotSpot虚拟机中,设计人员使用了永久代来实现了JVM规范的方法区。
JVM的内存模型只是一个规范,方法区也是一个规范,一个逻辑分区,并不是一个物理空间,我们这里说的字符串常量放在堆内存空间中,是指实际的物理空间。
元空间是属于方法区的,方法区只是一个逻辑分区,而元空间是具体实现。所以类的元数据是存放在元空间,逻辑上属于方法区。
在HotSpot虚拟机、Java7版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在JVM的非堆内存中,而Java8版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。之前永久代的类的元数据存储在了元空间,永久代的静态变量(class static variables)以及运行时常量池(runtime constant pool)则跟Java7一样,转移到了堆中。
Java8为什么使用元空间替代永久代?
官方给出的解释是:
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为JRockit没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,爆出异常java.lang.OutOfMemoryError: PermGen。这是因为在JDK1.7版本中,指定的PermGen区大小为8M,由于PermGen中类的元数据信息在每次FullGC的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为PermGen分配多大的空间很难确定,PermSize的大小依赖于很多因素,比如,JVM加载的class总数、常量池的大小和方法的大小等。
1.4-虚拟机栈
Java虚拟机栈是线程私有的内存空间,它和Java线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个栈帧,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。
1.5-本地方法栈
本地方法栈跟Java虚拟机栈的功能类似,Java虚拟机栈用于管理Java函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用Java实现的,而是由C语言实现的。
2-JVM运行原理
通过示例来认识JVM运行的原理
示例代码
public class JVMCase {
// 常量
public final static String MAN_SEX_TYPE = "man";
// 静态变量
public static String WOMAN_SEX_TYPE = "woman";
public static void main(String[] args) {
Student stu = new Student();
stu.setName("nick");
stu.setSexType(MAN_SEX_TYPE);
stu.setAge(20);
JVMCase jvmcase = new JVMCase();
// 调用静态方法
print(stu);
// 调用非静态方法
jvmcase.sayHello(stu);
}
// 常规静态方法
public static void print(Student stu) {
System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge());
}
// 非静态方法
public void sayHello(Student stu) {
System.out.println(stu.getName() + "say: hello");
}
}
class Student{
String name;
String sexType;
int age;
//省略get/set
}
当我们通过Java运行以上代码时,JVM的整个处理过程如下:
1-JVM向操作系统申请内存,JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给JVM,接下来JVM就进行内部分配。
2-JVM获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
3-class文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值。
4-完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM首先会执行构造器<clinit>方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。
5-执行方法。启动main线程,执行main方法,开始执行第一行代码。此时堆内存中会创建一个student对象,对象引用student就存放在栈中。
6-此时再次创建一个JVMCase对象,调用sayHello非静态方法,sayHello方法属于对象JVMCase,此时sayHello方法入栈,并通过栈中的student引用调用堆中的Student对象;之后,调用静态方法print,print静态方法属于JVMCase类,是从静态方法中获取,之后放入到栈中,也是通过student引用调用堆中的student对象。