Java内存分配
一、运行时数据区域
众所周知,程序的运行要把数据和代码装入到内存中运行,所以明白程序执行过程中内存是如何分配的是很有必要的。
Java虚拟机中,根据不同对象的特点,将内存划分为不同的数据区,如下图:
其中,方法区和堆区是所有线程共享的区域,随着虚拟机进程的启动而存在。
栈区和PC是线程私有的,随着用户线程的启动和消亡。
1.1 程序计数器(PC)
与OS中PC的作用一样,每个线程私有的PC用来指示下一条要执行的字节码指令。
所以分支、循环等流程的实现都需要依靠PC来实现
1.2 Java虚拟机栈
Java虚拟机栈是线程私有的,生命周期与线程相同。
每个方法在执行的同时创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法被调用时,入栈,方法结束时,退栈。
局部变量表存放着编译期间可以知道的数据类型(char boolean等)
局部变量表所需的内存空间在编译期间完成分配,也就是说当一个方法分配栈帧的时候,大小是提前确定的,方法运行期间不会改变局部变量表的大小。
1.3 本地方法栈
作用于Java虚拟机栈类似
当执行Java方法时,使用的是Java虚拟机栈
本地方法时,使用的是本地方法栈
1.4 堆
Java堆是Java虚拟机所管理的内存最大的一块,所有new出来的对象都在这里分配。
堆是所有线程共享的,所以GC时主要是对堆里面的对象进行回收。
为了GC时方便,将内存的堆分为新生代和老年代,新生代的对象生命周期更新频繁,每次都有大量的对象死去,少量的存活,老年代的对象生命周期比较长,每次存活率都比较高。
同时,堆区在物理内存上是不连续的,只是逻辑上连续
1.5 方法区
方法区是所有线程共享的。
用来存储加载的类信息、常量、静态变量、即时编译器后的代码等数据。
1.6 运行时常量池
常量池是方法区的一部分
用来存放编译期间或者运行期间产生的常量。
1.7 直接内存
直接内存不属于虚拟机运行时内存的一部分
而是在本机当中剩余内存分配出来的一块空间,通过一些传递数据的手段,从外内存到Java内存进行传递数据。
其大小受到主机内存大小的限制。
二、Java内存分配
根据以上特点,可以总结如下:
- 栈:存放基本数据类型,对象的引用。注意对象的引用是放在栈中,而对象本身是放在堆中(new出来的对象)或者常量池中(字符串常量对象)
- 堆:存放所有new出来的对象
- 常量池:存放字符串常量和基本数据类型的常量(public static final)
- 方法区:放静态常量等(static)
2.1 String常量和引用的分配
如下面代码的内存分配:
String s1 = "Wang";
String s2 = "Wang";
String s3 = "Wang";
String s4 = new String("Wang");
String s5 = new String("Wang");
String s6 = new String("Wang");
首先字符串常量分配在常量池当中,其引用s1等在栈中
new出来的string对象分配在堆中,其引用s4等在栈中
所以可以看出,s1 s2 s3指向的是同一个字符串常量,而s4 s5 s6则不是
这里有一个细节需要注意,new出来的“Wang”对象,会首先去常量池中查找是否已经有“Wang”对象,如果没有则在常量池创建一个,再复制到堆中。
2.2 基础类型的变量和常量
int i1 = 9;
int i2 = 9;
int i3 = 9;
public static final int INT1 = 9;
public static final int INT2 = 9;
public static final int INT3 = 9;
基础的数据类型直接存放在栈中
stactic final的存放在常量池当中
2.3 成员变量和局部变量
局部变量(包括形式参数)分配在栈中,随着方法的消失而消失
成员变量存储在堆中,有GC负责回收
public class People{
private int day;
private int month;
private int year;
public People(int d, int m, int y) {
this.day = d;
this.month = m;
this.year = y;
}
}
public class TestPeople {
public static void main(String[] args) {
int date = 9;
TestPeople testPeople = new TestPeople();
testPeople .change(date);
People wang= new People(7, 7,1970);
}
public void change(int i){
i = 1324;
}
}
从main方法的执行分析:
- int date = 9;
date属于局部变量,此时分配在栈中。 - TestPeople testPeople = new ();
testPeople 属于引用,分配在栈中
对象new TestPeople()分配在堆中 - testPeople .change(date);
i属于局部变量,存放在栈中,随着change方法的结束而消失,因此,在这里并不会改变传入date的值 - People wang= new People(7, 7,1970);
wang是对象引用,存放在栈中
对象new People存放在堆中
传入的7 7 1970 对应的d m y属于局部变量,存放在栈中,当构造方法执行完毕时自动消失
People对象中的day month year为成员变量,分配在堆中 - main方法结束时
date变量 testPeople wang引用都从栈中消失
此时new出来的TestPeople People没有引用指向他们,等待被GC
三、内存溢出
3.1 Java堆溢出
根据堆的特性,Java堆主要存放new出来的对象。
所以只要不断的创建对象,并且保持GC Roots到对象之间有可达路径就可以避免这些对象被GC,超过堆的最大容量之后就会造成内存溢出(OutOfMemoryError)。
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
3.2 栈溢出
我们知道栈分配的是局部变量和每一个方法的栈帧,所以不断地调用方法,可以造成StackOverflowError
public class StackSOF {
public void stackLeak(){
stackLeak();
}
public static void main(String[] args) { // 无限递归调用自身
StackSOF stackSOF = new StackSOF();
stackSOF.stackLeak();
}
}
3.3 方法区和常量池溢出
常量区存放字符串常量等,所以这我们的思想就是在常量池中不断产生字符串常量。
这里我们借助String.intern()方法:如果字符串常量池中已经包含一个此String对象的字符串,则返回常量池中这个对象;否则,将此String对象添加到常量池中并返回其引用。
import java.util.ArrayList;
import java.util.List;
public class RuntiomeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(); // 使用list保持对String的引用,防止GC
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}