文章大量参考周志明《深入理解Java虚拟机》
程序计数器 线程私有
记录正在执行的虚拟机字节码地址(如果正在执行的是本地方法则为空)
Java虚拟机栈线程私有
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在Java虚拟机中出栈和入栈的过程。
JDK1.4中默认为256K,JDK1.5+之后默认为1M
当线程请求的栈深度超过最大值,会抛出StackOverflowError异常栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常本地方法栈线程私有
本地方法栈与虚拟机栈所发挥的作用非常相似,区别是本地方法栈为为本地方法服务。
本地方法一般是用其他语言编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
堆
堆是对象分配内存的场所,也是垃圾收集的主要区域(“GC堆”)
现代垃圾收集器采用的是分代收集算法,主要思想是针对不同类型的对象采用不同的垃圾回收算法,可以将堆分为两块:
新生代(Young Generation)老年代(Old Generation)堆不需要连续内存,并且可以动态增加内存,增加失败会抛出OutOfMemoryError异常
方法区
用于存放已被加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
和堆一样不需要连续内存,并且可以动态扩展,扩展失败时也会抛出一样的异常。对这块区域进行垃圾回收的主要目标是对常量池的回收以及对类的卸载
运行时常量池
运行时常量池是方法区的一部分。
Class文件中的常量池会在类加载后进入方法区的运行时常量池存放。
有些运行期间也可能将新的常量放入池中(String类的intern()方法)
直接内存
JDK1.4引入的NIO类,它可以使用本地函数库直接分配对外内存,然后通过Java堆里的DirectByteBuffer对象作为这块内存的引用操作,此举可以避免在堆内存和堆外来回拷贝数据的麻烦。
引用计数法
为对象添加一个引用计数器,当对象增加一个引用时计数器+1,引用失效时计数器-1。引用计数为0的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永不为0,导致无法对他们进行回收。也正因为循环引用的存在,因此Java虚拟机不使用引用计数算法。
可达性分析算法
以GC Roots为起始点进行搜索,可达的对象都是存活的,不可达的对象都会被回收。
JVM一般使用此算法来判断对象是否可被回收,GC Roots一般包含以下内容:
虚拟机栈中局部变量表中的引用对象本地方法栈中接口引用对象方法区中类静态属性引用的对象方法区中的常量引用对象无论是哪一种哪种算法,都与引用有关,所以我们这里就来讨论一下引用。
强引用
被强引用关联的对象不会被回收
Object object = new Object();//通过new一个新对象来创建强引用软引用
被软引用关联的对象只有在内存不够的情况下才会被回收
Object object = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);//使用SoftReference类创建软引用 obj = null;弱应用
被弱引用关联的对象一定会被回收,即存货至下一次垃圾回收发生之前
Object object = new Object(); WeakReference<Object> sf = new WeakReference<Object>(obj);//使用WeakReference类创建弱引用 obj = null;虚应用
一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收之前收到一个系统通知。
Object object = new Object(); PhantomReference<Object> sf = new PhontomReference<Object>(obj);//使用PhontomReference类创建虚引用 obj = null;标记清除
在标记阶段,程序会检查每个对象是否是活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。
不足:
标记和清除过程效率都不高会产生大量不连续的内存碎片,导致无法给大对象分配内存标记整理
让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
优点:不会产生内存碎片
缺点:需要移动大量碎片,处理效率比较低
复制
将内存划分为大小相等的两块,每次只使用其中的一块,当这一块用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
不足:只使用了内存的一半
现在的商业虚拟机都采用了这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。在回收时,将Eden和Survivor中还存活的对象全部复制到另一块Survivor上,最后清理Eden和使用过的那一块Survivor。
HotSpot虚拟机的Eden和Survivor大小比例默认为8:1,保证了内存的利用率达到90%。如果每次回收哦有多于10%的对象存活,那么一块Survivor就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借助于老年代的空间存储放不下的对象。
分代收集
当前的商业虚拟机采用分代收集算法,他根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
一般将堆分为新生代和老年代。
新生代使用:复制算法老年代使用:标记-清除 或者 标记-整理 算法待更新😀😀😀
对象优先在Eden上分配
大多数情况下,对象在新生代Eden上分配,当Eden空间不够时,触发Minor GC
大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在Eden和Survivor之间的大量内存复制。
长期存活的对象进入老年代
为对象定义年龄计数器,对象在Eden出生并经过Minor GC依然存活,将移动到Survivor中,年龄增加1岁,增加到一定年龄则移动到老年代中
动态对象年龄判定
虚拟机也并不是永远要求对象的年龄到达规定年龄才能晋升老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代
空间分配担保
在发生Minor GC前,虚拟机先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC可以确认是安全的。
手动调用 System.gc()
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代,长期存活的对象进入老年代等。
空间分配担保失败
使用复制算法的Minor GC需要老年代的内存空间做担保,如果担保失败会执行一次Full GC.
JDK1.7 及以前的永久代空间不足
Concurrent Mode Failure
😊😊😊😊