程序计数器:
记录正在执行的虚拟机字节码指令的地址,如果是本地方法则为空
虚拟机栈:
每个Java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至结束,对应着一个栈帧在虚拟机栈中的入栈出栈操作。
可能会抛出Stack Overflow异常,当线程请求的栈的深度超过最大值
栈动态扩展时无法申请到足够的内存会抛出OutofMemoryError异常
本地方法栈
与虚拟机栈类似,区别在于本地方法栈为本地方法服务。本地方法一般是其他语言写的,需要特别处理。
堆
所有对象都在这里分配内存,是垃圾收集的主要区域。
垃圾回收器一般分代收集,分为新生代和老年代。
堆不需要连续内存,可以动态增加内存,增加失败会抛出OutOfMemoryError异常
方法区
存放已加载的类信息,常量,静态变量,即时编译的代码等
可以动态增加内存,增加失败会抛出OutOfMemoryError异常
本区域主要的垃圾回收针对常量池和类的卸载。一般较难实现,hotspot作为永久代进行垃圾回收,JDK1.8将其移至元空间,位于本地内存而不是虚拟机内存。元空间存储类的元信息,静态变量和常量池等移至堆中。
运行时常量池
属于方法区的一部分,类文件中的字面量及符号引用会在类加载后放到这个区域
除了编译期间生成的常量,动态生成的也算,如String的intern方法
直接内存
JDK1.4后引入NIO,使用本地方法分配堆外内存,使用堆的DirectByteBuffer对象作为这块内存的引用进行操作。可以提高内存操作效率,避免在堆外内存与堆内存之间的数据复制。
垃圾收集主要指向方法区和堆内存。程序计数器、虚拟机栈、本地方法栈属于线程私有的,线程执行结束就会释放。
1. 引用计数法
对象增加引用,引用计数器加一,引用失效,引用计数器减一。引用为0的时候,可以被回收。
在对象出现循环引用的时候,引用计数器永远不会为0,导致无法回收。
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
2. 可达性分析
以GC Root为起始点进行搜索,可达的对象都是存活的,不可达的可被回收。
GC Root一般是以下几种:
虚拟机栈局部变量表中引用的对象
本地方法栈中JNI引用的对象
方法区类静态属性引用的对象
方法区常量引用的对象
3.方法区回收
方法区主要放置永久代对象,回收率较低。主要是常量池的回收和类的卸载
类的卸载条件:
类的实例被回收了。
加载类的classloader被回收了。
该类对应的class对象没有任何地方被引用。
4. finalize
该方法用于资源的回收释放,可以实现一次自救,即将对象添加给变量引用。
强引用
使用new创建的对象属于强引用,被强引用关联的对象不会被回收
软引用
内存不足的时候才会回收
Object obj = new Object();
SoftReference
obj = null; //此时只剩下软引用
弱引用
被弱引用关联的对象在下一次垃圾回收时被回收
Object obj = new Object();
WeakReference
obj = null; //此时只剩下弱引用
虚引用
虚引用在对象被回收时收到系统通知。
Object obj = new Object();
PhantomReference
obj = null; //此时只剩下虚引用
1. 标记-清除
标记:程序检查每个对象是否是活动对象,进行标记
清除:对象回收,取消标志位。如果被回收的分块与前一个空闲分块是连续的,就合并两个分块。
分配的时候,找大于等于size的块,大小相等的直接返回,大于size的块,取block-size返回,剩余部分返还给空闲链表。
缺点是:标记和清除的效率不高,会产生大量内存碎片,无法分配给大的对象。
2. 标记-整理
类似于磁盘碎片整理,将存活的对象移至一端,清除此外的空间。
虽然不会产生碎片,但是需要大量移动,效率不高。
3.复制
将内存划为相等的两块,一块用完了,将存活的对象复制到另一块,本块清除。这个缺点是内存只使用了一半。
现代虚拟机常常采用上述方法回收新生代。
划分较大的Eden空间,两块小的Survivor,每次使用Eden和一块Survivor。将Eden和Survivor还存活的复制到另一块,清理当前使用的内存。
HotSpot划分比例是8:1:1,如果出现超过10%的对象存活,将借用老年代的存储空间存储剩余的存活对象。
4. 分代收集
现代虚拟机采用分代收集,新生代采用复制方法,老年代采用标记-整理。
HotSpot分为七种垃圾收集器。
单线程与多线程:单线程只用一个线程,多线程使用多个线程进行操作。
串行与并行:串行会阻断用户的操作,交替进行,并行不会影响。除CMS及G1都是串行的。
Serial
串行,单线程,效率较高,不存在线程间的切换。Client场景下新生代的默认收集器。
ParNew
串行,多线程。Server场景下的新生代默认收集器。
Parallel Scavenge
多线程。其他收集器都是为了缩短垃圾回收时用户线程停顿时间,它的目标是达到一个可控的吞吐量。这里的吞吐量指的是CPU作用于运行用户程序的时间比率。
停顿时间是从用户交互的流畅度方面考虑,高吞吐从高效执行代码方面考虑。
Serial Old
Serial收集器的老年代版本,给Client场景下的虚拟机使用。
Parallel Old
Parallel Svcavenge的老年代版本
CMS(Concurrent Mark Sweep)
初始标记:仅仅标记GC Roots能够直接关联到的对象,速度很快,需要停顿。
并发标记:GC Roots Tracing过程,耗时最长,不需要停顿
重新标记:修正并发标记期间用户程序导致的变得。需要停顿
并发清除:不需要停顿。
吞吐量低,无法处理浮动垃圾(并行用户程序产生),内存碎片
G1
面向服务器对于多CPU和大内存场景下有很好的性能。G1可以直接对新生代与老年代一起回收。
把堆划分成大小相等的独立区域Region,新生代与老年代不再物理隔离。
记录每个Region垃圾回收的时间和回收所获得的空间,维护一个优先列表,根据允许的收集时间优先回收价值最大的region。
初始标记
并发标记
最终标记:将并发标记期间产生的浮动内存记录到Remembered Set log,最终于Rememebered Set合并
筛选回收:收集价值较大的region
Minor GC:回收新生代,运行速度快,执行频繁。
Full GC:回收老年代与新生代,执行速度慢,频率低。
1. 对象优先在Eden分配
在新生代分配空间给对象,内存不足触发Minor GC
2.大对象直接进入老年代
类似于长字符串,大的数组,需要大量连续内存的对象,直接进入老年代。
3.长期存活的对象进入老年代
在Eden分配后,一次Minor GC存活的移至Survivor,多次存活进入老年代
4.动态对象年龄判定
并非达到阈值才会进入老年代,如果survivor中,某年龄的对象大小总和大于survivor的一半,则超过此年龄的直接进入老年代
5.空间分配担保
在Minor GC之前判断老年代中空间的可用大小是否满足新生代所以对象的空间。如果不成立,将确认HandlePromotionFailure是否担保失败。继续检查历次晋升到老年代的对象平均大小,空间剩余则尝试Minor GC,否则Full GC
1. 调用System.gc()
2. 老年代空间不足
大对象,长期存活的对象进入老年代造成。
3.空间分配担保失败
4.永久代空间不足(JDK1.7以前)
class信息,常量,静态变量等数据是方法区存储的,一般是老年代实现。要加载、反射、调用的方法过多,永久代会占满,触发Full GC
5.Concurrent Mode Failture
CMS GC过程中有对象存到老年代,老年代空间不足,会触发Concurrent Mode Failture,并触发Full GC
1. 加载
通过类的完全限定名称获取二进制字节流
将字节流表示的静态存储结构转换为方法区的运行时存储结构
内存中生成代表该类的class对象,作为方法区中该类各种数据的访问入口。
2.验证
确保二进制字节安全可靠。
3. 准备
使用方法区内存为类的静态变量分配内存赋初值。
类变量初始化为0,类常量初始化为定义的值。
4.解析
将常量池的符号引用转为直接引用
5.初始化
正式执行类构造器
1. 主动引用
对类静态变量访问,静态方法调用,使用new进行实例化对象的时候。
使用反射进行调用的时候
父类还没有初始化,先初始化父类。
main方法所在的类在虚拟机启动的时候初始化
2. 被动引用
子类调用父类的静态字段,不会导致子类初始化
类数组不会引起初始化
类常量不会引发初始化
类本身相等,同时使用相同的加载器加载。
启动类加载器:虚拟机的一部分
其他类的加载器:使用java实现,独立于虚拟机
扩展类加载器:加载系统变量指定的路径下的类库
应用程序加载器:用户类路径下的类库(默认类加载器)
优先让父类加载器加载,父类加载器加载不了的话自己再加载。