1. JVM内存区域简介
Java虚拟机(JVM)是Java程序运行的基础,Java程序开发中最重要的就是掌握JVM内存区域的知识。JVM内存区域可以划分为以下五个区域:
程序计数器
虚拟机栈
本地方法栈
堆
方法区
1.1 程序计数器
程序计数器是JVM中一个较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,其存储的信息与线程相关。当线程执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址,若线程执行的是Native方法则该计数器的值为Undefined。
public class Test {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
}
}
在上述代码中,JVM执行main方法时,会将其字节码指令的值加载到程序计数器中,由程序计数器指向正在执行的指令,当执行完一条指令后,程序计数器就会更新为下一条指令的地址。
1.2 虚拟机栈
虚拟机栈是每个线程独有的内存区域,用于存放线程的栈帧。栈帧表示一个方法在虚拟机栈上的一块内存空间,在该空间中存储该方法局部变量、操作数栈、动态链接、方法出口等信息。虚拟机栈的大小可以在启动JVM时通过-Xss参数进行指定。
public class Test {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = add(a, b);
System.out.println(c);
}
public static int add(int a, int b) {
int c = a + b;
return c;
}
}
在上述代码中,JVM执行main方法时,首先会向虚拟机栈中压入一个main方法的栈帧,其中包括了main方法的局部变量表、操作数栈等信息。然后,当main方法调用add方法时,会向虚拟机栈中压入add方法的栈帧,当add方法执行完毕后,虚拟机弹出该栈帧,并继续执行main方法的下一条指令。
1.3 本地方法栈
本地方法栈和虚拟机栈的作用非常相似,区别在于本地方法栈为Native方法服务,而虚拟机栈为Java方法服务。同样地,本地方法栈也是每个线程独有的内存区域,在执行Native方法时使用。
public class Test {
public static void main(String[] args) {
System.loadLibrary("TestNative");
TestNative.sayHello();
}
}
在上述代码中,我们调用了一个Native方法sayHello,使用了System.loadLibrary("TestNative")方法。当Java程序调用该方法时,JVM会去加载TestNative.dll库,并将该方法所需的参数和return地址传递给Native方法栈上的栈帧。
1.4 堆
堆是JVM中最大的一块内存区域,也是Java程序运行期间用于存储对象实例的地方。在JVM启动时即可指定堆的初始大小和最大大小。
public class Test {
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 10000; i++) {
Object obj = new Object();
list.add(obj);
}
}
}
在上述代码中,我们定义了一个List类型的变量,并在其内存中不断加入Object类型的对象,这些对象都是存储在堆中的。
1.5 方法区
方法区也称为静态区,它用于存放类的元数据信息。在JVM中,对象的类信息、常量池、静态变量、即时编译器编译后的代码等都存储在方法区中。
public class Test {
private static String name = "jvm";
public static void main(String[] args) {
System.out.println(name);
}
}
在上述代码中,我们定义了一个静态变量name,并在main方法中使用它,该静态变量被存储在方法区中。
2. JVM内存区域之间的关系
JVM内存区域之间的关系如下:
程序计数器与虚拟机栈之间的关系:程序计数器指向正在执行的虚拟机字节码指令的地址,在虚拟机栈中即可找到该指令所在的的栈帧
虚拟机栈与方法区之间的关系:虚拟机栈中存放的是栈帧,而方法区存放类的元数据信息,也就是虚拟机指令的字节码。因此,当虚拟机栈中执行到某个方法时,需要从方法区中获取该方法的字节码指令。
堆与方法区之间的关系:在堆中存储的是对象实例,而方法区存储的是类的元数据信息,包括静态变量的类型信息等。
3. JVM内存参数
JVM启动时,可以通过命令行参数来指定初始值和最大值:
-Xms:指定堆的初始大小
-Xmx:指定堆的最大大小
-Xss:指定虚拟机栈的大小
例如:
-Xms256m -Xmx512m -Xss512k
表示指定堆的初始大小为256M,最大大小为512M,虚拟机栈大小为512K。
4. 内存溢出和内存泄漏
4.1 内存溢出
当JVM中的内存区域达到其最大值时,就会出现内存溢出的异常。常见的内存溢出类型有:Java heap space、PermGen space和Metaspace。
Java heap space:堆内存溢出,表示使用的Java对象超出了堆的内存大小,需要通过-Xmx参数调整堆的大小来解决。
PermGen space:永久代溢出,永久代即方法区,表示存储在方法区中的类的元数据信息过多,需要通过-XX:PermSize和-XX:MaxPermSize来进行调整(JDK8及以后版本已经没有PermGen space,被Metaspace代替)。
Metaspace:元空间溢出,表示存储在元空间中的类的元数据信息过多,需要通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来进行调整。
4.2 内存泄漏
内存泄漏指在程序运行过程中,分配的内存空间没有被释放,导致可用内存越来越少。对象在使用完后没有被垃圾回收器回收,导致永远无法再次使用该内存空间,这就是内存泄漏。
造成内存泄漏的原因很多,常见的有以下几种:
静态变量:静态变量一旦被引用,就会一直存在于内存中,如果该静态变量不再使用,就会造成内存泄漏。
循环引用:当两个对象相互引用时,如果没有及时解除引用,就会造成内存泄漏。
未关闭的资源:当程序无法正常关闭流、数据库连接等资源时,就会造成对内存的占用。
5. 小结
JVM内存区域是Java程序运行的基础,JVM将内存区域划分为程序计数器、虚拟机栈、本地方法栈、堆和方法区。这些内存区域之间存在着不同的关系。同时,还介绍了JVM启动时可以指定内存大小的相关参数。最后,我们还讨论了内存溢出和内存泄漏的问题。