JVM知识点大全(未完...)
JVM运行时数据区域
-
堆
堆是Java虚拟机中用于存储对象的主要区域,包括字符串常量池。绝大多数对象都是在堆中创建的(少部分对象可能会在栈上分配)。为了更好地进行垃圾回收,堆被划分为年轻代和老年代两部分。年轻代又被进一步分为Eden区和两个Survivor区。 -
元空间
元空间使用直接内存,主要用于存储运行时常量池、类的Class对象实例(包含对象在堆的引用)、JIT编译后的代码缓存、类信息、方法信息、字段信息,以及类加载器的引用等。注意:类变量移动到了堆中,与Class对象在一起。 -
直接内存
直接内存可以减少在堆外内存和堆内内存之间的数据复制。如果没有直接内存,Java读取网络数据时,数据会经过内核缓冲区、堆外内存到达堆内内存。有了直接内存,Java可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer
对象来引用并操作这块内存。 -
虚拟机栈
虚拟机栈是线程私有的,由栈帧组成,负责Java方法调用。每个栈帧包含局部变量表、操作数栈、动态链接以及方法返回地址。动态链接指的是该栈帧执行的运行时常量池中方法的引用。局部变量表主要存储方法参数和局部变量以及对象引用。操作数栈用于存放计算过程中产生的中间结果。虚拟机栈可能会抛出StackOverflowError
和OutOfMemoryError
(如果栈允许动态分配内存,动态分配时内存不够则会发生此错误)。 -
本地方法栈
本地方法栈与虚拟机栈类似,但专门用于调用本地方法。 -
程序计数器
程序计数器指向下一条需要执行的字节码指令,是唯一一个不会出现OutOfMemoryError
的内存区域。
垃圾回收之后,对象如何从年轻代进入老年代
一般情况下,对象会首先在Eden区分配(大对象直接分配到老年代)。当年轻代的Eden区满时,会触发初次垃圾回收。这个过程中,垃圾回收器(GC)会标记所有存活的对象,并清除未被引用的对象,以释放出空间。这一过程通常被称为“Minor GC”。
存活的对象会被移动到两个Survivor区中的一个,例如S0或S1。在后续的每次垃圾回收中,存活的对象会从Eden区和其中一个Survivor区(例如S0)复制到另一个Survivor区(例如S1),然后将S0保留为空,供下次GC时进一步对象的复制使用。
每经历一次垃圾回收,对象的存活年龄会增加。在垃圾回收过程中,JVM会根据对象的年龄和其占用内存的大小进行统计和累加。当累加到某个年龄时,如果所占用的总大小超过了某个Survivor区的一半(每次垃圾回收都会将存活对象全部移动到其中一个Survivor区,如果对象的大小超过一个Survivor区的总大小,则无法放下),那么这个年龄和对象的累加年龄取最小值,将作为新的晋升年龄阈值。
类加载过程
类加载过程包括 加载、链接和初始化 三个阶段:
-
加载
在加载阶段,Java虚拟机将类的字节码加载到方法区,并根据该字节码代表的静态结构转换为在方法区的运行时数据结构,通常称为 Klass 类(大写的K),这是C++常用的表示方法。Klass内部存储了各种类元信息,随后在堆上生成一个代表该类的 Class 对象(这是一个Java对象),二者通过引用相互持有。 -
链接
链接阶段包括以下三个部分:-
验证
验证是链接的第一步,确保Class文件的字节流中包含的信息符合Java虚拟机规范的所有要求。 -
准备
在准备阶段,为static
变量分配空间,同时设置默认值。例如:static int a; // 仅分配空间,不会赋值 static int b = 9; // 也不会赋值
但是,对于
static final
的 基本类型 以及 字符串常量,赋值会在准备阶段完成。例如:static final int a = 12; // 在准备阶段赋值 static final String b = "aaa"; // 在准备阶段赋值
若是引用类型,如:
static final Object o = new Object(); // 会在初始化阶段赋值
-
解析
解析是将常量池内的符号引用替换为直接引用的过程。比如,将静态方法的符号引用替换为指向数据实际存储内存的指针或句柄等(即直接引用),符号引用类似于方法名。这一过程被称为 静态链接(在类加载过程中完成),而 动态链接 则是在程序运行期间完成的,将符号引用替换为直接引用。
-
-
初始化
初始化阶段对类的静态变量进行初始化为指定的值,并执行静态代码块。
CMS详解
想要详细了解CMS(并发标记清除收集器),必须先了解三色标记。三色标记算法是在对GC Roots可达性分析遍历对象的过程中,将遇到的对象按照“是否访问过”标记成以下三种颜色:
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,是安全存活的。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
- 白色:表示对象尚未被垃圾收集器访问过。在可达性分析刚开始时,所有的对象都是白色的;若在分析结束时仍然是白色的对象,则代表不可达。
CMS的回收过程以获取最短回收停顿时间为目标,使用标记-清除算法,整个回收过程分为以下四个步骤:
-
初始标记:
- 依靠安全点暂停所有其他线程(STW),并记录下GC Roots直接能引用的对象,速度很快。因此,一般来说,这一步完成后,GC Roots会变为黑色,因为扫描了每个GC Root的所有直接引用。这些直接引用的对象可能是灰色(还引用了其他对象)或黑色(没有其它引用了)。
- 安全点:程序只有在安全点才会进行GC。JVM依靠抢占式中断实现,不直接操作线程中断,而是简单地设置一个标志,让各个线程在下一个安全点处检测到该标志后,发现标志位为真时就自己中断挂起。安全点通常是在循环的末尾、方法返回前、调用方法后等。
-
并发标记:
- 并发标记阶段是从GC Roots的直接关联对象开始遍历整个对象图的过程。这个过程耗时较长,但不需要停顿用户线程,可以与垃圾收集线程并发运行。由于用户程序继续运行,可能会导致已经标记对象的状态发生改变,出现漏标和多标的情况。
- 多标可以接受,本来是垃圾但本次GC没有被回收;而漏标是不可以接受的,本来不是垃圾却被回收了。漏标要发生,需要满足以下两个条件:
- 有至少一个黑色对象在自己被标记后指向了这个白色对象。
- 所有的灰色对象在引用扫描完成之前删除了对白色对象的引用。
网上看到一张图可以帮助理解:
漏标的解决方案:
- CMS是写屏障+增量更新。
- G1是写屏障+原始快照。
增量更新指的是当黑色对象插入新的指向白色对象的引用关系时,就会将这个新插入的引用记录到一个集合中,等并发扫描结束后,再处理这个集合中的引用关系。而对引用关系的记录是依靠写后屏障实现的。
原始快照的方式是在灰色对象要删除指向白色对象的引用关系时,将要删除的引用记录到一个集合中,等并发扫描结束后再处理该集合中的引用关系。对引用关系的记录是依靠写前屏障实现的。
-
重新标记:
- 重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致标记变化的对象标记记录(主要处理漏标问题)。这个阶段的停顿时间一般会比初始标记稍长,远远比并发标记阶段的时间短,主要依靠三色标记中的增量更新算法。
-
并发清理:
- 开启用户线程的同时,GC线程开始对未标记的区域进行清扫。此阶段如果有新增对象,会被标记为黑色,不做任何处理。在此阶段不会出现新的引用指向白色对象的情况,例如:
A a = new A(); a = null;
上面的new A()
对象已经无法再引用到了。
- 开启用户线程的同时,GC线程开始对未标记的区域进行清扫。此阶段如果有新增对象,会被标记为黑色,不做任何处理。在此阶段不会出现新的引用指向白色对象的情况,例如:
-
并发重置:
- 重置本次GC过程中的标记数据。