这一阵子阅读了《深入理解Java虚拟机》第二章,写的真的很好,所以决定记录下来,以后没事翻翻博客看看。?
Java虚拟机在运行java代码时,将内存区域分为
1.程序计数器
该区域是线程私有,字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,例如:循环,跳转,异常处理,线程恢复等基础功能。该内存区域是唯一一个在jvm里面没有规定任何OOM情况的区域
2.java虚拟机栈
该区域线程私有,生命周期与线程相同,java每个方法执行的时候都会创建一个栈桢用于存放局部变量表、操作栈桢、动态链接、方法出口等信息。每个方法从调用到执行完成都对应一个栈桢从虚拟机栈中入栈到出栈过程。其中局部变量表存放了编译期可知的基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用类型。抛出异常为StackOverFlow和OOM。
3.本地方法栈
该区域线程私有,与虚拟机栈发挥的作用十分相似。区别为虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用的Native方法服务。本地方法栈也会抛出StackOverFlow和OOM。
4.java堆
该区域线程共享,java堆是java虚拟机所管理的内存中最大的一块,虚拟机启动时创建,用于存放对象实例,所有对象实例以及数组都要在堆上分配。且是垃圾收集器管理的主要区域。java堆还可以细分为新生代和老年代,用于进行不同的垃圾收集算法GC。当前的主流虚拟机可以扩展,通过-Xmx和-Xms控制。如果堆没法再扩展内存时,将会抛出OOM异常。
5.方法区
该区域线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。java虚拟机规范把方法去描述为堆的一个逻辑部分。抛出异常类型为OOM。
6.运行时的常量池
这是方法区的一部分。用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个重要特性是具备动态性,Java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,比较常见的就是String类的intern()方法。抛出异常类型为OOM。
下面这张图是对象的访问定位
通过这张图我们就能清楚的知道,发生的内存溢出,是哪一块儿出来的,而我也会贴上书上写的代码,并分析这种溢出情况。
1.java堆溢出代码
看了上面的内容,我们应该知道了,java堆是存放对象的地方(怪不得我单身这么久,原来对象要在java堆里面找啊),所以java堆想要溢出,只要不断创建对象,并保证GC Roots根到对象之间可达-GC算法。当对象数量达到最大堆的容量时就会产生内存溢出异常。
/**
*VM Args: -Xms 20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject); //list添加声明的对象,保证GC Roots根可达
}
}
}
基本参数设置,这个程序执行后会发生OOM,并在后面提示是 Java heap space,道出是java堆的溢出。
2.虚拟机栈和本地方法栈溢出
一种是单线程下不断递归
我们应该都知道,不断递归的话是容易爆栈的,因为每次递归,都需要入栈来存放中间变量和部分参数等一些信息。
/**
* VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak(); //无限递归
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
内心os:这种画图方式仿佛你画我猜,2333。 就类似这样,因为不断地递归,不断的入栈,却没有出栈的操作,仿佛貔貅一样,只吃不吐,所以最终就会发生StackOverflow的异常。
另外一种是不断创建线程。
我们知道虚拟机栈和本地方法栈是线程私有的,所以每当我们一个线程,就会开辟一个虚拟机栈。如图1.1
![](https://i-blog.csdnimg.cn/blog_migrate/0fe05c454a50550281ea6b61b08cc686.png)
这种情况下最终会发生内存溢出异常,原因为:操作系统给每个进程分配的内存有限,而线程是在进程下的,所以当为每个线程的栈分配的内存越大时,进程下拥有的线程数量就越小,当然也就越容易产生内存溢出异常。而如果不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。需要注意的是,运行上面代码的时候要保存当前工作,因为在windows平台上的JVM,Java线程是映射到操作系统线程上的,可能会导致操作系统假死。运行结果:OOM:unable to create new native thread
3.方法区和运行时常量池溢出
很简单,保持GC Roots根的引用,再不断在常量池添加对象,我们用String.intern()的方法。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPool100M {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
//10MB的PermSize在integerr范围内足够产生OOM了
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:OOM: PermGen space。“PermGen space”,说明运行时常量池属于方法区的一部分。
完结,这次的第二章,目前我感觉能用的总结完了,小伙伴们如果看我的博客感觉哪一块儿理解的有问题或者哪一块儿的语言组织的有问题,可以提出来,大家一块儿努力进步。
下图是关于jvm结构的信息。图片出处:https://blog.csdn.net/wo541075754/article/details/102623406