1.jvm的内存模型
jdk( java development kit )java开发的最小环境,包括java语言,java虚拟机,java类库
虚拟机在java程序运行中,将其管理的内存,划分为不同的几个区域:
1.方法区
2.堆
3.虚拟机栈
4.本地方法栈
5.程序计数器
其中虚拟机栈和本地方法栈以及程序计数器是线程私有的,方法区和堆是线程共享的。
以上统称为运行时数据区。
程序计数器:
是一块较小的内存空间,里面记录了当前线程执行的字节码的行号(其实就是当前要执行的指令地址)。
程序的分支 跳转 循环 异常处理 线程恢复等功能都要依赖这个程序计数器。
java虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式实现的,当线程切换后,就要在计数器中保存当前指令地址
恢复的时候继续从之前的指令下一条开始执行,因此每个线程都有一个自己的程序计数器,互不干扰,这类内存就是 线程私有。
java虚拟机栈:
java虚拟机栈也是线程私有的,生命周期和线程相同。
java虚拟机栈 描述的是java方法执行的线程内存模型:
每个方法执行的时候,在虚拟机栈中就会创建一个栈帧,里面存储了局部变量表,操作数栈,动态链接,方法出口等信息。
方法从调用到执行结束,就对应栈帧从入栈道出栈的过程。
局部变量表中,存储了java虚拟机中的基本数据类型,对象引用类型,returnAddress类型,每个数据都被分配对应的局部变量槽大小。
因此在编译期,一共需要多少个局部变量槽就是已经确定,也因此在加载方法到内存中时,需要多大的内存空间也已经确定了。
单个变量槽的具体大小由虚拟机自己实现决定。
虚拟机栈,如果栈深度超过了指定的阈值,就会抛出栈溢出异常,stackOverflowError。
如果线程申请栈空间的时候,内存不够分,就会抛出内存溢出的异常,outofMemoryError。
本地方法栈:
和java虚拟机栈类似,只是本地方法栈是给native方法使用的,虚拟机栈给java方法使用。
java堆:
这个区域的唯一目的就是存放对象实例,是被所有线程共享的。
但是有些优化手段 栈上分配 标量替换等,使得并非所有对象实例都会分配到堆上。
此外,还有为了提高对象分配的效率,还存在线程私有的分配缓冲区。
如果对象实例分配空间不足,也无法继续拓展堆空间的话,就会抛出内存溢出异常,outofMemoryError。
现代垃圾收集器大多都是基于分代收集理论设计,收集算法会根据对象的存活时间,将堆划分为新生代,老年代,永久代
然后新生代又分为eden区和survivor区等等。 但是有些垃圾收集器并非基于分代收集理论,就不会这样划分区域了。
方法区:
线程共享的内存区域,用来存储已被虚拟机加载的类型信息,常量,静态变量,编译器编译后的代码缓存等数据。
hotspot虚拟机用了堆中的永久代来实现方法区,以便于虚拟机可以像管理堆一样管理方法区。
缺点就是这样实现的方法区有大小限制,更容易出现内存溢出。
像J9等虚拟机使用本地内存来实现方法区,只要不超过机器的可用内存,就不会溢出。
hotspot团队在jdk6之后也逐渐采用这个方式,jdk7之后,将原本放在永久代的字符串常量池,静态变量移出,jdk8则彻底放弃了
永久代,改用了在本地内存实现的元空间(metaspace)替代,将之前永久代的类型信息全部移到元空间中。
如果方法区无法满足分配时,也要抛出内存溢出异常 outofMemoryError。
这部分的内存回收,主要是常量池的回收和类型的卸载,但是条件都相当苛刻。
方法区的运行时常量池:
在Class文件中,里面有一项信息叫常量池表,用于存放编译期生成的各种字面量和符号引用,这部分内容会在类加载后存放到方法区的运行时常量池中。
运行时常量池中,存放的不一定是编译期的数据,也有可能是运行时加入的,比如说调用String.intern()
当常量池无法申请到内存时候,也会抛内存溢出异常 outofMemoryError。
直接内存:
直接内存(directMemory) ,不属于java运行时数据区的内存,在jdk1.4引入NIO的时候引入的,可以使用native方法直接分配内存,
然后通过存储在java堆里面的一个DirectByteBuffer对象来操作这块内存。这部分内存, 也可能有内存溢出异常出现。