一、简介
大家都知道,在Java编程中,内存都是由JVM虚拟机自动管理和分配的,如果项目中出现内存溢出或者内存泄漏的问题,如果对JVM内存结构还不太了解的话,解决这些问题将会比较棘手,笔者在学习JVM虚拟机内存结构之前也只是对内存结构有一个很浅的认识,本文将总结一下虚拟机的内存结构,希望能够加深对内存结构的理解。
二、JVM内存结构图
由上图可见,Java内存主要由: 方法区、堆(最大的内存区域)、虚拟机栈、程序计数器组成。其中堆内存是最大的一块内存区域,同时也是垃圾回收器主要的收集区域。
三、如何通过参数来控制各区域的内存大小
常见的设置内存大小相关控制参数如下:
- -Xms :设置堆的最小空间大小
- -Xmx:设置堆的最大空间大小
- -XX:NewSize:设置新生代最小空间大小
- -XX:MaxNewSize:设置新生代最大空间大小
- -XX:PermSize:设置永久代最小空间大小
- -XX:MaxPermSize:设置永久代最大空间大小
- -Xss:设置每个线程的堆栈大小
- 注意点:没有参数可以设置老年代的空间大小,但是可以通过控制堆内存的空间大小和新生代空间大小来间接控制老年代的空间。
四、JVM内存各个区域详解
【a】堆内存:堆内存是JVM内存结构中最大的一块区域,可以分为新生代和老年代,其中新生代又可以分为:Eden Space、From Survivor Space、To Survivor Space。堆内存是被所有线程共用的一块区域,主要存放new()出来的对象实例,几乎所有的对象实例都在堆里面分配内存。堆内存是垃圾收集器集(GC)中收集的区域,也可以成为“GC堆”。
- 如果堆内存中没有足够内存来完成对象实例的内存分配,并且内存也无法扩展扩大时,会报内存溢出(OutOfMemoryError)异常。
【b】方法区:方法区主要存放类的代码信息、常量信息、静态变量等数据,与堆内存一样同样是线程共享的区域。方法区可以认为是特殊的堆,但是并不等价,方法区有个别名“非堆”用于与堆内存区分开来。垃圾回收器在方法区的主要工作就是收集常量池信息和负责类型的卸载,但是对于类型的卸载收集工作比较困难,一般不会有什么明显的效果。
- 如果方法区无法足够空间来满足内存分配时,会报内存溢出(OutOfMemoryError)异常。
【c】Java虚拟机栈:JVM栈和线程的关系比较密切,每创建一个线程都会创建一个栈,所以JVM栈是线程私有的区域,各个线程之间不能共享数据。JVM栈主要作用是描述Java方法的执行过程,每个方法被调用执行一次都会创建一个栈帧(Stack Frame)并进行压栈操作,栈帧主要用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法从调用开始到返回结果出去就对应着一个栈帧从入栈到出栈的过程。
JVM栈是一个后入先出的数据结构,线程运行过程中,只有一个栈帧是处于活跃状态的,被称为"当前活动帧栈",当前活动帧栈始终是虚拟机栈的栈顶元素。
- 当线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverFlow异常,当JVM栈扩展内存空间无法申请到足够的内存空间时,将会报内存溢出(OutOfMemoryError)异常。
【d】本地方法栈:本地方法栈,作用与JVM栈类似,都是描述Java方法的执行过程,区别就是JVM栈为执行Java方法(字节码)服务,而本地方法栈为执行Native Method(本地方法)服务。
- 当线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverFlow异常,当JVM栈扩展内存空间无法申请到足够的内存空间时,将会报内存溢出(OutOfMemoryError)异常。
【e】程序计数器:程序计数器,是一块比较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址。Java中的多线程,是通过线程间的轮流切换并分配处理器执行时间来实现的,在任何时刻,处理器只能处理一个线程中的一条指令。为了确保线程在切换后还能够回到原来的状态,找到之前执行的指令信息,每个线程都会设立一个程序计数器,每个线程的程序计数器都不一样,互不影响,所以程序计算器是各个线程不能共享的区域。
- 如果线程正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,程序计数器值则为空(Undefined)。同时,程序计数器也是唯一一个在Java规范中没有规定任何OutOfMemory场景的区域。
五、内存分析示例一
下面通过一个示例并通过画图进行Java内存分析:
【a】首先创建Student类,主要name、age两个属性和sayHello()方法;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void sayHello() {
System.out.println("hello..." + name + "-->" + age);
}
}
【b】测试类
public class TestStorageStructure01 {
public static void main(String[] args) {
/**
* 1. Student经过类加载器的加载,Student类代码信息、静态变量信息等都存放在方法区中;
* 2. student对象存放在栈中,是Student对象的引用,持有堆中new Student()对象的内存地址,通过这个地址可以找到堆中new出来的对象;
* 3. new Student()出来的对象存放在堆中, 持有方法区中Student类信息的地址引用;
*/
Student student = new Student("lisi", 20);
/**
* 1. 调用sayHello()方法时,会创建一个栈帧对象,存放着方法的参数、过程结果等数据;
* 2. 通过栈中student对象持有的地址找到在堆中new出来的Student对象;
* 3. 通过堆中持有的方法区中Student类的信息,找到sayHello()方法字节码,执行sayHello()方法,输出结果;
* 4. 伴随着栈帧的出栈操作;
*/
student.sayHello();
}
}
如上图就是以上代码的大概的执行过程。
六、内存分析示例二
public class Test {
static String str1 = new String("123");
static String str2 = new String("123");
static String str3 = "123";
static String str4 = "123";
public static void main(String[] args) {
String str5 = "123";
String str6 = "123";
/**
* str1/str2都是String对象的引用,分别持有指向堆中new String("123")出来的对象的地址,但是两次new String("123")的地址不一样,所以str1 == str2为false
*/
//false
System.out.println("str1 == str2 :-->" + (str1 == str2));
/**
* str3/str4都直接指向方法区中常量池中的"123"这个常量的地址,地址指向都相同,所以str3 == str4为true.
*/
//true
System.out.println("str3 == str4 :-->" + (str3 == str4));
/**
* 原理类似str3/str4
*/
//true
System.out.println("str5 == str6 :-->" + (str5 == str6));
}
}
七、总结
本文通过对Java虚拟机各个区域的作用以及结构分析,让我们对虚拟机的底层结构进一步熟悉了,并通过两个示例和画图进行了内存分析,本文仅仅是笔者学习的一些总结和见解,不对之处,还请大家指点指点,希望对大家的学习有所帮助。