前言:在学习Java第一课时,老师就讲到:Java不同于C/C++的手动内存分配与回收。原来这都得益于JVM的内存自动管理机制,但是在这背后又暗藏什么玄机呢???本人从图书馆借来了《Java虚拟机精讲》来一探究竟。
一.JVM的内存模型:
如下图所示可以分为5个模块:堆,栈,本地方法栈,PC寄存器,方法区。
这些内存区域用来存储程序运行时的数据。
根据线程访问的权限不同,其中线程共享的内存区域:堆,方法区,运行时常量池;线程私有的内存区域:栈,本地方法栈,PC寄存器。
1.线程共享的内存区域(堆区,方法区,运行时常量池):
1.堆(heap):
在JVM启动的时候创建该区域,堆区在实际的物理内存中是可以不连续的。我们程序中所创建的对象实例,和数组都存放在堆区,所以堆区是GC(垃圾回收器)的高频工作地点,但是当回收和使用大的内存区域时,可能会出现性能瓶颈,这时我们就提出疑问对象一定要放在堆区吗???当然是可以不放在堆区。对此我们有两套解决方案:逃逸分析,栈上分配以及TaoBaoVM都是可以将对象放在堆区之外的解决方案,用来提升GC的效率。
由于Java中对象的生命周期不同,有的对象生命周期非常短暂,有的对象声明周期甚至和JVM的生命周期一样,这就使得要采用不同的GC算法来收集不同类型的对象。由此分代回收算法诞生!目前几乎所有的GC算法都是分代收集算法。
堆区的内存区域又可细分为:新生代(Young Gen),年老代(Old Gen)其中新生代又可按8:1:1分为Eden,FromSurvivor,ToSurvivor。堆区的参数设置大小在JVM启动的时候就已经设置好了,可以通过 -Xms:获得堆区的起始大小; -Xmx:获得堆区的最大大小。当超出-Xmx(堆区的最大值)就会抛出OOM(out of memory)Error。
//一个OOM的代码实例 public class OOMTest { public static void main(String [] args){ List<OOMObject> list=new ArrayList<>(); while (true){ list.add(new OOMObject()); System.out.println("创建了一个静态对象!"); } } static class OOMObject{ } }
2.方法区:
方法区和堆区是一样的,也是属于线程共享的区域。方法区中存储了每一个Java类的结构信息,比如:运行时常量池,字段,方法数据,以及构造函数和普通方法的字节码内容,和类,实例,接口初始化时所用到的特殊方法等数据。在HotSpot(是sunJDK,openJDK所带的JVM,也是目前使用最广的Java虚拟机)的实现中方法区的实际的物理内存位于堆中,也就是说方法区只是逻辑上的独立。
方法区也被称为永久代,因为方法区并不会象堆区那样进行频繁的GC,甚至还可以设置参数让GC不回收方法区,若GC收集方法时也只是收集运行时常量池和类型卸载。方法区可通过-XX:MaxPermSize设置内存大小进行动态内存扩展。
方法区也会发生内存溢出,当内存大小超过-XX:MaxPermSize时就会抛出OOMError。
3.运行时常量池:
运行时常量池属于方法区,一个有效的字节码文件中除了包含字段,方法,接口等信息外,还应包括常量池表,运行时常量池就是常量池表运行时的表示形式,运行时常量池中可以存放多种不同的常量。
当类加载器成功的将一个类或者接口加载进JVM,就会分配相应的运行时常量池,由于运行时常量池位于方法区中,故也会发生OOMError。
2.线程私有的内存区域(PC寄存器,栈,本地方法栈):
1.pc寄存器:
pc寄存器不同于物理寄存器,更像是一个pc计数器,其生命周期和线程的生命周期保持一致。PC寄存器记录了当前字节码指令地址;
pc寄存器是唯一一个不会发生OOMError的内存区域。
2.Java栈:
栈区的生命周期和线程的生命周期一样,栈主要用来存储栈帧,栈帧中存储了局部变量表,操作数栈,以及方法出口等信息。Java堆中存的是对象实例,那么Java栈的局部变量表中存放的是各类原始数据类型,对象引用,以及returnAddress(是JVM内部的原始数据类型)类型。
Java栈的大小,可以设置为固定大小,也可设置为动态的大小。当线程请求分配的栈容量超过Java栈的最大大小JVM就会抛出StackOverflowError异常。
3.本地方法栈:
用于支持本地方法的执行,并不是必须要的一块内存区域。同样也会发生OOM和StackOverFlow。