JVM内存区域与OOM
说明:本篇博客属于读书笔记,大量参考《深入理解Java虚拟机》这本书
JVM的内存
程序计数器
- 程序计数器是线程私有的,每一个线程都有自己的一个程序计数器,并且互不干扰,程序计数器相当于当前代码所执行指令的指针,控制了当前线程的执行流程,当Java程序在执行Java方法的时候,程序计数器记录的是当前执行代码的指令地址,当Java程序正在执行Native方法,程序计数器则为空(Undefined),程序计数器是不会抛出OOM异常的
Java虚拟机栈
- Java虚拟机栈也是线程私有的,它的生命周期与线程的生命周期相同,Java程序在执行一个方法的时候都会创建一个栈帧(Stack Frame)用于存储局部变量,方法出口等信息,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中的入栈到出栈的过程,虚拟机栈中存储这Java的基本数据类型以及对象的引用,在Java虚拟机栈中会抛出*Error异常和OOM异常,下面来看一个demo:
public class DemoMain {
public static void main(String[] args) {
System.out.println("test");
DemoMain.testMethod();
System.out.println("end");
}
public static void testMethod() {
testMethod();
}
}
如上的应用程序运行之后,在我的机器上会抛出*Error:
test
Exception in thread "main" java.lang.*Error
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
当虚拟机在执行方法testMethod的时候,这时候就会在Java虚拟机栈上创建一个栈帧,然后入栈,然而在testMethod方法内又不断的递归调用testMethod方法,导致Java虚拟机栈不断的嵌套执行testMethod方法,不断的创建testMethod的栈帧,然后入栈,而testMethod并没有执行完成,所以testMethod对应的栈帧不会出栈,当Java虚拟机栈中的栈深度超过了虚拟机允许的深度,这时候就抛出了*Error异常了,如果虚拟机可以动态拓展,在新的栈帧入栈的时候再去申请内存,要是申请不到足够的内存,此时就会抛出OOM异常了
本地方法栈
- 本地方法栈是线程私有的,存储Native方法的信息,这个内存区域也会抛出*Error和OOM异常
Java堆
- Java堆是线程共享的,这是虚拟机中内存最大的一块,它唯一目的就是用来存放对象实例的,就是:
Object obj = new Object();
obj是对象的引用,存储在Java虚拟机栈中,而new出来的Object对象实例就存储在Java堆中,obj引用指向Java堆中实例的地址,Java堆是垃圾回收管理的主要区域,Java堆的内存空间不需要物理上的连续,只需要逻辑上的连续即可,Java堆也会抛出*Error 和OOM异常
方法区
- 方法区是线程共享的内存区域,它用来存储已经被虚拟机加载的类信息(类名,类字段,方法名等),常量(final修饰),静态变量(static修饰)等,此区域也会抛出OOM异常
运行时常量池
- 运行时常量池是方法区的一部分,常量池用于存放编译期生成的各种字面量(文本字符串、声明为final的常量值等)和符号引用(类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符),运行时常量池具有动态性,也就是说不一定是预置到class中的常量才能进入运行时常量池,在运行期间也可能将新的常量放入池中,例如String类的intern(),该内存区域也会抛出OOM异常
字符串常量池
- 字符串常量池也是方法区的一部分,用来存放字符串常量,举个例子:
public class DemoMain {
public static void main(String[] args) {
System.out.println("test");
String s1 = "s1";
String s2 = "s1";
String s3 = new String("s1");
System.out.println(s1 == s2);
System.out.println("end");
}
}
以上运行的结果是:
test
true
end
也就是说是s1指向的地址和s2指向的地址是一样的,为什么?因为“s1”这个字符串常量被存储到了字符串常量池中了,虚拟机发现了s1对象指向“s1”,s2对象也指向“s1”,因此不会再次创建一个“s1”,而是将s1和s2对象都指向存储于常量址的“s1”,这里就做到了常量池的对象共享,节省内存
对象创建的过程
- 在JVM中当收到一个new指令的时候首先会去常量池中检查是否存在这个类的符号引用,并检查这个类是否已经被加载,解析和初始化过,如果没有,那就先执行类加载过程
- 类加载检查过后,接下来JVM就会为新生的对象分配内存,对象所需要的内存空间大小在类加载的时候就能够确定,内存分配其实就是在Java堆中开辟一块确定大小的内存出来,Java堆的内存分配有两种,第一种是“指针碰撞”,当Java堆中的内存是规整的,即用过的内存都在一边,空闲的内存在另一边,那么此时的内存分配就是把指针指向空闲内存空间挪动一段于对象大小相同的距离;第二种是“空闲列表”,当Java堆中使用内存和空闲内存相互交错的时候,此时JVM必须维护一个列表,记录哪些内存是可用的,在分配内存的时候从列表中寻找一块足够大的空间划分给对象,并更新列表上的记录,具体选择哪一种内存分配的方式取决于Java堆内存是否规整,而Java堆内存是否规整取决于GC回收器是否又压缩整理功能
- 对象内存的分配过程还要注意多线程问题,假如在给一个对象分配内存的时候,指针还没来得及修改,此时又要操作指针给另一个对象也分配内存,解决这个问题又两种方案,一种是堆内存分配动作进行同步操作,另一种是预先给每一个线程在Java堆中分配一块小内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),那么只有在分配TLAB的时候才需要同步处理,对象内存分配完了之后就要对对象中的值进行初始化为零值,最后再执行方法,也就是构造方法,来初始化对象中的值,这样一个对象才算完全创建成功
- 所以总结下对象创建的过程大致分为以下几个阶段
对象的访问定位
- Java虚拟机栈中存储的是对象的引用,Java堆中存储的才是对象的实际数据,对象的访问定位通常有两种,一种是句柄访问,一种是指针访问,句柄访问就是在Java堆中有一个句柄池,句柄池中才是存储了对象地址,而JVM栈中的对象引用存的是对象的句柄地址,也就是说reference指向句柄,句柄指向对象,这么做的好处就是对象要是移动,JVM栈中的reference不用做修改,只要修改句柄就行了;指针访问就是JVM栈中的reference直接存的就是对象地址,reference直接指向JVM堆中的对象,这么做的好处就是访问对象速度快,要是对象被频繁的访问,那指针访问的方式将有明显的效率提升
内存溢出
Java堆内存溢出
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new Object());
}
}
不断的分配对象,并添加到list当中,这样对象就不会被回收,程序跑一会儿就报Java堆的OOM异常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
什么情况可能会导致Java堆的内存泄漏?很明显,内存泄漏,一些对象创建后,一直被持有导致GCRoot一直存在,所以不会被回收
虚拟机栈和本地方法栈溢出
- 虚拟机栈和本地方法栈都会抛出*Error和OutOfMemoryError,对于*Error异常会有两种情况,一种是虚拟机栈的深度大于虚拟机规定的最大深度,另一种是在申请栈帧内存的时候没有足够的内存,这时候也会抛出这个异常
public class DemoMain {
int i = 0;
public static void main(String[] args) {
DemoMain demoMain = new DemoMain();
try {
demoMain.test();
} catch (Throwable e) {
System.err.println("stack:" + demoMain.i);
e.printStackTrace();
}
}
public void test() {
i++;
test();
}
}
运行结果:
stack:34879
java.lang.*Error
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
- 线程创建造成的内存溢出
public class DemoMain {
public static void main(String[] args) {
DemoMain demoMain = new DemoMain();
demoMain.createThread();
}
public void createThread() {
while (true) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:714)
at com.lhd.jvmdemo1.DemoMain.createThread(DemoMain.java:26)
at com.lhd.jvmdemo1.DemoMain.main(DemoMain.java:12)
这个是本地方法区抛出的OOM异常
方法区和常量池溢出
- 方法区抛出的OOM本机没有模拟出来,不过方法区的OOM异常是:
java.lang.OutOfMemoryError:PermGen space
在android开发中,如果一个apk的类非常多,安装这个apk的时候就可能出现方法区的内存不够用导致方法区内存溢出
下一篇: docker部署tomcat集群