Java与C的内存管理区别
在C/C++中,需要使用 delete/free 等函数来手动释放内存;
而在Java中,因为Java采用了 垃圾回收机制,程序猿无需释放内存,由 虚拟机自动管理内存,会较少的出现内存泄漏和溢出的问题。
因此我们需要深入了解虚拟机是如何使用内存的,万一出现内存泄漏和溢出,也方便排查。
运行时的数据区域
![](https://img.haomeiwen.com/i6802002/ef861fc9ec432ac9.png)
程序计数器(Program Counter Register)
程序计数器用于记录当前线程 正在执行 的 字节码 指令地址。
计数器通过改变记录的值,来选取下一条要执行的字节码指令,循环、跳转、异常处理、线程恢复等都是通过计数器完成的。每个线程有独立的计数器,互不影响。
虚拟机栈(VM Stack)
虚拟机栈用于记录 执行 的 Java方法。
栈只保存基本数据类型的对象(byte、char、int等)和自定义对象的引用(reference类型,不是对象)
当方法执行时,虚拟机栈会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 方法的执行过程就是一个栈帧入栈出栈的过程。
Java虚拟机规范中,对该区域规定了两种异常:
- StackOverFlowError:线程请求的栈深度大于虚拟机允许的栈深度时
- OverOfMemoryError:动态扩展的线程无法申请到足够的内存时 (OOM常见情况)
StackOverFlowError示例:通过递归不断增加栈深度,导致栈溢出
public class JVMStacksOverFlowError {
public int stackLength = 1;
public void JVMStackOverFlowError() {
stackLength++;
JVMStackOverFlowError();
}
public static void main(String[] args) throws Throwable {
JVMStacksOverFlowError stack = new JVMStacksOverFlowError();
try {
stack.JVMStackOverFlowError();
} catch (Throwable e) {
System.out.println("stack length is :" + stack.stackLength);
e.printStackTrace();
}
}
}
运行结果如图所示:
![](https://img.haomeiwen.com/i6802002/f00d2f0a5c1c421d.png)
OutOfMemoryError示例:通过不断创建线程来分配栈内存,导致内存不够
public class JVMOutOfMemoryError {
public int threadCount = 0;
public void addNewThread() {
while (true) {
threadCount++;
new Thread() {
@Override
public void run() {
while (true) ;
}
}.start();
}
}
public static void main(String[] args) throws Throwable {
JVMOutOfMemoryError ofmeMain = new JVMOutOfMemoryError();
try {
ofmeMain.addNewThread();
} catch (Throwable e) {
System.out.println("thread count is :" + ofmeMain.threadCount);
e.printStackTrace();
}
}
}
运行结果如图所示:
![](https://img.haomeiwen.com/i6802002/3846ceec5e23e6bc.png)
本地方法栈(Native Method Stack)
本地方法栈用于记录 执行 的 Native方法。
当虚拟机调用本地(native)方法时,虚拟机不会创建新的栈帧,虚拟机栈会保持不变,虚拟机只是简单的动态连接并直接调用相关的本地方法。
如果本地方法接口是c连接模型的话,它的本地方法栈就是c栈。当c程序调用一个c函数时,传递给该函数的参数以相应的顺序压入栈,它的返回值以确定的方式返回给调用方。这就是虚拟机实现中本地方法栈的行为。(本地方法栈说明)
本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
堆(Heap)
堆用于存储 对象实例,管理 垃圾回收。
堆是JVM所管理的内存中最大的一块。
![](https://img.haomeiwen.com/i6802002/c78d28157737cf23.png)
根据分代收集算法,堆被分为新生代和老生代。
新生代:Young Generation,主要用来存放新生的对象实例。
老年代:Old Generation/Tenured Generation,主要存放应用程序声明周期长的内存对象实例。
方法区(Method Area)
方法区存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等。
参见 方法区存储信息
![](https://img.haomeiwen.com/i6802002/a96b5a5245053958.png)
栈、堆、方法区比较
堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap),被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
通过代码堆三者进行形象的解释:
//JVM先把ApplicationMain类放入方法区
public class ApplicationMain {
//main方法放入方法区
public static void main(String[] args) {
//obj1是引用,指向SimpleObject实例的引用,放入虚拟机栈中
//new的SimpleObject是自定义的对象,指向方法区中SimpleObject类的内存地址,放入堆中
SimpleObject obj1 = new SimpleObject("obj1");
//调用printName方法,找到堆中的对象,根据对象找到方法区中的类信息
//根据类信息中方法的字节码,从而让线程执行方法
obj1.printName();
}
//SimpleObject类信息放入方法区
static class SimpleObject {
//new Simple实例后,name引用放入栈区,name对象放入堆区
private String name;
/**
* 构造方法
*
* @param name
*/
public SimpleObject(String name) {
this.name = name;
}
//printName方法放入方法区
public void printName() {
System.out.println(name);
}
}
}
代码执行过程,各内存块的存储过程如下:
- 启动后,系统收到用户指令,启动一个Java虚拟机进程,先找到ApplicationMain.class文件,读取二进制数据,把类信息放入方法区,这个过程为ApplicationMain的加载过程;
-
程序计数器定位到 main() 方法的字节码,并开始执行它的第一条指令
SimpleObject obj1 = new SimpleObject("obj1");
- JVM要创建一个SimpleObject对象实例,并使局部变量obj1引用这个实例。
JVM先去方法区寻找类SimpleObject的类型信息,未找到,则加载了SimpleObject类。把信息存在方法区;然后JVM为创建的新对象实例在堆中分配内存,这个实例持着指向方法区SimpleObject类的类型信息的引用,引用即SimpleObject类在方法区中的内存地址。
4.局部变量obj1指向了实例的引用,被存放在运行main方法的主线程的虚拟机栈中。
5.之后obj1.printName();
执行printName方法时,JVM根据obj1的引用,定位到堆区的实例,再根据实例的引用找到方法区中的类型信息,从而找到printName方法的字节码,从而让线程执行printName方法。
网友评论