2 Java内存区域与内存溢出异常
2.2 运行时数据区域
2.2.1程序计数器
程序计数器占用空间较小,可以看作当前 线程执行字节码的行号。因此是线程独立的。
如果执行的是native方法,则该计数器为空。
2.2.2 java虚拟机栈
描述java方法的执行内存模型。每个方法执行都会创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
方法调用和执行完成的过程,伴随着栈帧入栈和出栈的过程。
每个局部变量空间为32bit,其中double和long占用两个slot。
该区域有两种异常情况:栈深度超过允许深度;无法申请到足够内存
2.2.3 本地方法栈
与虚拟机栈类似,只不过用于执行native方法时使用。sun-hotspot虚拟机将两种栈合二为一。
2.2.4 java堆
最大块的内存,几乎所有对象的实例、数组都在这里分配内存
可以细分为新生代和老年代,新生代可细分为Eden,From Survivor, To Survivor空间等。
是线程共享的,但是也可以为每个线程单独划出分配缓冲区,称为TLAB空间,各个线程的对象会优先在自己的缓冲区内分配。
2.2.5 方法区
也是各个线程共享的区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据。
相对而言,在这个区域,GC很少出现。GC主要针对该区域的常量池回收和类的卸载。
关于常量池,经常见到的考题是:
String A = "a";
String B = "a";
A==B? true;
String A = "a";
String B = "" + 'a';
A==B? false;
2.2.6 运行时常量池
运行时放入常量池中的变量,例如String的intern方法。
运行时常量池是方法区的一部分
2.2.7 直接内存
主要是nio包内分配的内存,读写较为高效。直接分配本机内存,不受java堆限制,但是会受本机物理内存限制。
满时不会触发回收,只会会在fullgc时顺便回收。
2.3 HotSpot虚拟机对象
2.3.1 对象的创建
当遇到一个new指令(字节码)时,首先在常量池查找类的符号引用,检查这个类是否已经加载,如果没加载,则要触发加载流程;
采用CAS+失败重试的方式为对象分配内存(也有采用TLAB方式分配的,这样就不用CAS了)
将对象内部的内存空间全都初始化为0,保证实例字段在java代码中不赋初始值就可以使用。为什么局部变量就必须要赋值?因为局部变量可以很好地判断出有没有赋初始值,但是类的变量,实际上是共享的,无法准确的判断出代码是否已经赋初始值,而java是不允许使用未初始化的值的,因此就直接清空了。
存储对象头:元数据(用于确定是哪个类的实例),哈希码,GC分代,偏向锁等等
执行<init>方法
java堆内存的管理方式可以是:基于指针碰撞的,或者基于空闲列表的,但是分配时都需要采用CAS重试。
对象内部的数据
对象头,实例数据,对齐填充(对齐到8B)
3.GC和内存分配策略
3.2 判断对象存活与否的算法
3.2.1 引用计数法
对每个对象都添加引用计数器,当增加一个引用时,计数器+1,减少一个引用时(改为null或者该引用变量销毁),计数器-1。缺点:无法处理循环引用的问题。
3.2.2 可达性分析
通过一系列GC-ROOT的对象为起始点,从这些起始点开始向下搜索,走过的路径为引用链。当枚举完后,不在引用链上的对象即为死亡对象。
GC-ROOT包含:虚拟机栈中各个方法局部变量表内引用的对象;类静态成员、常量引用的对象;本地方法栈JNI引用的对象;所有被同步锁持有的对象;jvm内部引用对象,如基本数据类型对应的Class对象,常驻的异常对象,系统类加载器等;其它代的引用信息
jvm采用该方法进行存活判断
3.2.5 方法区回收的判断
对于废弃常量:没有任何引用该常量。
对于废弃类:该类所有的实例已经被回收、该类的ClassLoader已经被回收、Class对象没有被引用
对于大量使用反射、动态代理等动态字节码的框架,频繁装载、卸载类的场景下,回收方法区是很有意义的。
3.3 垃圾收集算法
3.3.1 分代收集理论
弱分代假说:绝大多数对象都是朝生夕灭的;
强分代假说:熬过越多次GC过程的对象就越难消亡;
基于上面两种假说,现在的垃圾回收器都是分区收集的,而且更先进的回收器都具有局部收集的特征。
跨代引用假说:跨代引用相对于同代引用来说仅占极少数
这样可以记录少量的跨代引用而不用通过扫描整个老年代来获取跨代引用。
后来新的理论,则是将整个堆内存划分为小块进行回收,是对分代收集理论的拓展。
3.3.2 标记-清除
首先标记所有的死亡对象(从gc-root开始枚举),然后进行清除。
缺点:标记很慢,清除造成碎片较多。虽然清除简单,但是分配时麻烦。
3.3.3 复制算法
将内存分为大小相同的两块,当一块内存用完后,将存活的对象复制到另一块,然后交换堆指针。
优点:避免碎片,内存利用更高效
缺点:可用内存缩小为原来的一半
现代虚拟机采用该方法来回收新生代。划分为两块小survivor和一块大eden区域,每次利用一块E和一块s,当回收时,将e和s中存活的对象一次性复制到另一块s中。默认e:s=8:1.这是因为java对象大多具有短命的特点。如果一次gc中survivor不够放这么多存活的对象,则会通过分配担保等方式将对象直接放入老年代。
3.3.4 标记-整理
和标记清除类似,只不过不做清除,而是把所有的对象都向一边移动,也即压缩。虽然清除时麻烦,但是再分配内存时比较简单。
但是它是移动操作,需要更新引用,这个过程通常也得STW
分代收集
一般将java堆划分为新生代和老年代。新生代每次gc通常只有少部分存活,因此采用复制算法较好;而老年代中对象存活率较高,且没有额外的空间进行分配担保,因此采用标记-清理、标记-整理方法。
3.4垃圾收集的过程
3.4.1 枚举根节点
该过程需要保证对象的引用关系不发生变化,因此必须要进行gc停顿(但是耗时最长的查找引用链的过程可以和用户程序并行运行)。也称为StopTheWorld。
由于hotspot虚拟机进行了准确式gc,可以精准的知道内存里的数据是引用还是实际数值,所以gc-root是可以很快得知的。
3.4.2 安全点
如果记录所有的引起引用关系变化指令的地方,则会需要太多额外空间。因此采用安全点,只在安全点记录这些信息;且进行gc时,所有的线程都会跑到最近的安全点停下。
安全点主要是方法调用、循环跳转、异常跳转等可能执行较长时间的指令。
3.4.3 安全区域
安全区域指的是一段代码片段中,引用关系不会发生变化。因此在这个区域中进行GC是安全的。可以看作是安全点的拓展。
3.4.4 记忆集与卡表
记忆集和卡表都是用于记录跨区域(也可能是跨(新生/老年)代)引用的。这样通过记录当前对象被哪些外部区域引用,就可以进行分区收集,而不是扫描整个堆来获取引用关系了;
记忆精度(也就是区域的粒度,记录该区域是否被外界引用):字长精度(即32位、64位等机器字长);对象精度(记录每个obj的跨代指针,即是否被跨代引用);卡精度(可以看成是页内存的感觉,若卡段内部的obj被跨代引用,则标志位为1)
3.4.5 写屏障
记忆集可以用来缩减GC的扫描范围,但是需要维护卡表元素,则要靠写屏障。也就是在每次修改引用关系时,触发卡表的更新。
3.4.6 并发可达性分析
三色标记法:白、黑、灰。最开始所有对象都为白色,从GC-ROOT(标记为黑色)开始搜索,对于全部引用对象已经扫描过的情况,标记为黑色;对于还有引用对象没有扫描完的情况(也就是正在扫描的对象),标记为灰色。
可能存在以下两种情况:
把原本应该死亡的对象标记为存活,比如后来黑色对象又删除了一些引用;
把原本应该存活的标记成死亡,比如后来一个黑色对象引用了新的白色对象,然后这个白色对象被其它对象删去了。
前者会造成浮动垃圾,但是后者会造成严重的错误。为了避免这种错误,可以用两种方法来打破这两个条件:增量更新和原始快照。
增量更新:一旦黑色对象引用了新的白色对象,那就将它记录下来,等待引用链扫描之后再次对这些黑色节点进行扫描
原始快照:一旦灰色对象删除了白色对象的引用(不管是直接的还是简洁的),那就将这一引用链记录下来,等待扫描结束后,重新扫描这些灰色节点的引用链记录(也就是尽量保留之前的引用记录),将引用链上的标记为白色。也即一切维持扫描前的引用状态,所以称为原始快照。
3.5 经典垃圾回收器
JVM根据不同代的特点,采用不同的垃圾回收器进行垃圾回收。
Serial
用于新生代,采用复制算法,暂停所有用户线程。
单线程,但是STW时间较长,会造