Java 内存区域
运行时数据区域
程序计数器
程序计数器是一块较小的内存,它可以看做是当前线程所执行的字节码行号指示器.在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成.
为了线程切换后能恢复到正确的执行位置,每个线程都需要这样一个独立的程序计数器.
如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行一个Native方法,这个计数器则为空(Undefined).
此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域.
java 虚拟机栈
Java虚拟机栈也是线程独有的.
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(stack frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
虚拟机栈中的局部变量中存放了编译期中可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址).
其中64位长度的long和double类型会占用2个局部变量空间(slot),其余的数据类型只占用一个.当进入一个方法时, 这个方法需要在帧中分配多大局部变量空间是完全确定的,而且在运行中是不会改变的.
JVM规范中在这个区域中规定了两种异常状况:
- 如果线程请求的栈的深度大于虚拟机栈所允许的最大深度,就会抛出StackOverflowError异常
- 如果虚拟机栈可以扩展,当扩展时无法申请到足够内存将会抛出OutOfMemoryError异常.
本地方法栈
本地方法栈与虚拟机栈的区别不过于虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务.
Java堆
Java堆(Java Heap)是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,几乎所有的对象实例都在这里分配.
Java堆分为新生代和老年代,再细致一点分为Eden空间、From Survivor空间、To Survivor空间等。
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB).(为了更好地回收内存或者更快地分配内存)
通过-Xmx和-Xms控制java堆的大小.如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常.
方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池
运行时常量池是方法区的一部分,这个区域存放类和接口的常量,除此之外,它还存放方法和域的所有引用。当一个方法或者域被引用的时候,JVM就通过运行常量池中的这些引用来查找方法和域在内存中的的实际地址。Class文件中除了有累的版本、字段、方法、接口等描述还有一项关键的信息是常量池(Constant Pool Table), 用于存放编译时期生成的各种字面量和符号引用,这部分内存会在类加载后进入方法区的的运行时常量池中存放.一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中.java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池中的内容才能进入方法区运行时常量池,比如String类的intern()方法.
直接内存
直接内存并不是JVM规范中定义的内存区域.但是这部分区域被频繁地使用,而且也可能导致OOM异常.
在JDK1.4中新加入了NIO (New Input/Output类),引入了一种基于内存通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据.
垃圾收集器与内存分配策略
需要对GC实施监控和调节的情况:
- 排查各种内存溢出
- 排查内存泄露问题
- 当垃圾收集成为系统达到更高并发量的瓶颈时
对象已死吗
再谈引用
引用分为强引用(Strong Reference), 软引用(Soft Reference), 弱引用(Weak Reference), 虚引用(Phantom Reference).
- 强引用就是指在程序代码之中普遍存在的,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
- 软引用是用来描述一些还有用但非必须的对象.但系统将要发生内存溢出异常之前,系统才会把这部分对象列进回收范围之中进行第二次回收
- 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前(无论内存是否充足).
- 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用取得一个对象的实例.为一个对象设置引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知.
引用计数算法
给对象添加一个引用计数器,每当有一个对象引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能被再使用的.
主流的java虚拟机没有选用引用计数算法来管理内存的,其中最重要的原因是它很难解决对象相互循环引用的问题.
可达性分析算法
这个算法的基本思想就是通过一系列的称为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到”GC Roots”没有任何引用链相连时,则证明此对象是不可用的.
java语言中,GC Roots包括下面几种:
- 虚拟机栈(栈帧中本地变量表)中引用的对象
- 方法去中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类.
- 废弃常量:以常量池中字面量的回收为例,比如一个字符串”abc”已经进入了常量池中,但是没有一个String对象是叫做”abc”的,也没有其他任何地方引用了这个字面量,那么”abc”就可能要面临被回收掉,被系统清理出常量池.常量池中其他的类(接口)、方法、字段的符号引用也与此类似。
- 无用的类:
- 该类所有的实例都已经被回收了,也就是Java堆中不存在该类的任何实例了
- 加载该类的ClassLoader已经被回收了
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射该类的方法
垃圾收集算法
标记-清除算法
首先标出所有需要回收的对象,在标记完成回统一回收所有被标记的对象.
它有不足主要有两个:
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.
复制算法
复制算法是为了解决效率问题,它将内存划分为大小相同的两块,每次只使用其中的一块,当这一块内存用完了就将还存活的对象复制到另一块去,然后再把已使用过的另一块一次性清理掉.
当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后才清理掉Eden和刚才用过的Survivor空间.HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,当Survivor空间不够的时候,需要依赖于其他内存(这里指老年代)进行分配担保.
标记整理算法
复制算法在对象存活率较高的时候,就要进行较多的复制操作,效率将会变低.而且不想浪费50%的空间就要由额外的空间进行担保,所以在老年代一般不能直接选用这种算法.
根据老年代的特点,标记整理算法让所有的存活的对象都向一端移动,然后直接清理掉边界以外的内存.
分代收集算法
当前的商业虚拟机的垃圾收集都采用”分代收集”算法,根据各个年代的特点采用最适当的收集算法.
在新生代中,每次垃圾收集都会有大量的对象死亡,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集.
在老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用”标记清除算法”或者”标记整理算法”来进行回收.