Java虚拟机的内存分布
操作系统为每个进程分配的内存是有限的,例如32位的Windows被限制为2GB,虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值,剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器小号内存很小,可忽略。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
1.程序计数器(线程私有)
可以理解为当前线程的字节码行号指示器,多线程的实现是通过处理器轮流执行来实现的,所以每个线程为了保证下一次被轮到后处理器可以找到程序执行的位置,都有这么一小块的内存用来存储程序执行的行号位置。
2.虚拟机栈(线程私有)
这部分就是“栈内存”,每个Java方法在执行前都会创建一个栈帧,栈帧的内容包括:局部变量表、操作树栈、动态链接、方法出口等(后面文章有详细介绍)。
此部分区域对应这两种异常:内存泄露(StackOverflowError)和内存溢出(OutOfMemoryError)。
- StackOverflowError异常是由于线程请求的栈深度大于虚拟机所允许的最大深度。
- OutOfMemoryError异常的产生原因是因为虚拟机在扩展栈时无法申请到足够的空间。
//内存泄露StackOverflowError
public class ErrorTest {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
ErrorTest test = new ErrorTest();
test.stackLeak();
}
}
上面程序运行结果:java.lang.StackOverflow Error。结果说明了在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机都会抛出内存泄露异常。
public class ErrorTest {
private void dontStop(){
while (true){}
}
public void stackLeakByThread(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
}
}
public static void main(String[] args) {
ErrorTest test = new ErrorTest();
test.stackLeakByThread();
}
}
上面程序运行结果:java.lang.OutOfMemory Error
3.本地方法栈(线程私有)
与虚拟机栈一样,只不过本地方法栈是用来给Java调用本地(Native)方法使用的内存。
4.Java堆(线程共享)
用来存储Java对象,这部分内存是Java垃圾回收机主要管理的区域。很多时候被称作GC堆。
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么对象数量到达最大堆的容量限制后就会产生内存溢出异常。
//VM Args : -Xms20m -Xmx20m -XXL+HeapDumpOnOutOfMemoryError
public class ErrorTest {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
运行异常:java.lang.OutOfMemory Error:Java heap space
解决该异常的途径:如果是内存泄漏,可进一步通过工具查看泄露对象到GC Roots的引用链,就可以找到泄露的对象是通过怎样的路径与GC Roots相关并导致垃圾回收机无法回收。 如果不存在内存泄露,此时需要检查虚拟机参数(-Xmx与-Xms),与机器的物理内存对比看是否可以调大。从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况。
5.方法区(线程共享)
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、及编译器编译后的代码数据。Java虚拟机规范是把方法区描述为堆的一个逻辑部分,但是他却有一个别名Non-Heap(非堆)。很多时候方法区被称作“永久代”。Java虚拟机在处理方法区时与处理堆内存的过程基本一致,包括内存分配的过程与垃圾回收的过程。
6.运行时常量池(线程共享)
Class文件中除了有类的版本、字段、方法、接口的描述信息以外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError。
//VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i=0;
while (true){
list.add(String.valueOf(i++).intern());
}
}
在JDK1.6以前的运行结果:java.OutOfMemory Error:PermGen space
在JDK1.7以后会一直循环下去。
intern的特殊之处:
//JDK1.6以前是true,JDK1.7以后是false
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
}
原因:JDK1.6以前,如果调用intern()方法,会先去常量池中查找是否有该字符串,如果有直接返回该字符串的引用,如果没有则将该字符串放到常量池中然后再返回该字符串的引用。JDK1.7以后,还是会先去常量池中查找是否有该字符串,如果有直接返回该字符串的引用,如果没有则将该字符串出现的首次引用地址值放到常量池中,而不会再去拷贝字符串。
7.直接内存(线程共享)
这部分内存是本机内存,Java的NIO中用到的内存映射部分用到直接内存,这部分内存不受Jvm的限制,只受机器的内存的限制。
本部分的直接内存(DirectMemory)容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。
这部分的内存主要是Java的NIO部分经常会出现。
各种异常的处理办法参考文章:https://www.cnblogs.com/airduce/p/8093069.html