整理JVM知识点大梳理
Posted JAVA
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了整理JVM知识点大梳理相关的知识,希望对你有一定的参考价值。
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 ----来源:百度百科
JVM 组成部分
-
类加载器,在 JVM 启动时或者类运行时将需要的 class 加载到 JVM 中。 -
内存区,将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。 -
执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU 。 -
本地方法调用,调用 C 或 C++ 实现的本地方法的代码返回结果。
1、类加载器
1.1、加载
-
通过一个类的全限定名获取定义此类的二进制字节流。 -
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。 -
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。
1.2、验证
-
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 -
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。 -
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 -
符号引用验证:确保解析动作能正确执行。
1.3、准备
进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
初始值通常情况下是数据类型默认的零值(如0、0L、null、false等)
1.4、解析
符号引用:简单的理解就是字符串,比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
1.5、初始化
-
1、使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。 -
2、通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。 -
3、当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。 -
4、当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。 -
5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
2、对象的创建过程
2.1、检查类是否被加载
2.2、为对象分配内存
多线程并发时会出现正在给对象 A 分配内存,还没来得及修改指针,对象 B 又用这个指针分配内存,这样就出现问题了。
解决这种问题有两种方案:
第一种,是采用同步的办法,使用 CAS 来保证操作的原子性。
另一种,是每个线程分配内存都在自己的空间内进行,即是每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),分配内存的时候再TLAB上分配,互不干扰。可以通过 -XX:+/-UseTLAB 参数决定。
2.3、为分配的内存空间初始化零值
2.4、为对象进行其他设置
2.5、执行 init 方法
在new B一个实例时首先要进行类的装载。(类只有在使用New调用创建的时候才会被java类装载器装入)
在装载类时,先装载父类A,再装载子类B
装载父类A后,完成静态动作(包括静态代码和变量,它们的级别是相同的,按照代码中出现的顺序初始化)
装载子类B后,完成静态动作
类装载完成,开始进行实例化
在实例化子类B时,先要实例化父类A2,实例化父类A时,先成员实例化(非静态代码)
父类A的构造方法
子类B的成员实例化(非静态代码)
子类B的构造方法
先初始化父类的静态代码--->初始化子类的静态代码-->初始化父类的非静态代码--->初始化父类构造函数--->初始化子类非静态代码--->初始化子类构造函数
3、对象的内存布局
3.1、对象头(markword)
-
第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。 -
第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例; -
Klass Word 这里其实是虚拟机设计的一个oop-klass model模型,这里的OOP是指Ordinary Object Pointer(普通对象指针),看起来像个指针实际上是藏在指针里的对象。而 klass 则包含 元数据和方法信息,用来描述 Java 类。它在64位虚拟机开启压缩指针的环境下占用 32bits 空间。 -
如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。
3.2、实例数据(Instance Data)
3.3、对其填充(Padding)
由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。
3.4、预估对象大小
Class A {
int i;
byte b;
String str;
}
4、对象访问
对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
1.句柄访问对象
2.直接指针访问对象。(Sun HotSpot使用这种方式)
4.1、句柄访问
4.2、直接指针
5、JVM 内存区域
5.1、虚拟机栈
-
线程请求的栈深度大于虚拟机允许的栈深度,将抛出 StackOverflowError -
虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出 OutOfMemory异常 -
拓展link: 栈帧
5.2、本地方法栈
5.3、程序计数器
-
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。 -
程序计数器的主要作用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行 -
程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域 ,所以这块区域也不需要进行 GC
5.4、本地内存
-
线程共享区域,Java 8 中,本地内存,也是我们通常说的堆外内存,包括元空间和方法区 -
主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限 -
所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM,有人说可以把永久代设置得足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。 -
所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响,后文会提到),也就不存在由于永久代限制大小而导致的 OOM 异常了(假设总内存2G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。 -
综上所述,在 Java 8 以后这一区域也不需要进行 GC -
拓展link: 堆外内存回收
5.5、堆
-
对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收。 -
java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和 逃逸分析技术 的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。 -
堆细分:新生代(Eden,survior)和老年代
6、对象存活判断
-
引用计数 -
可达性分析
6.1、引用计数
6.2、可达性分析
GC Roots 对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 JNI(即一般说的 Native 方法)中引用的对象。
该类所有实例都被回收(Java 堆中没有该类的对象)。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方利用反射访问该类。
6.3、finalize
6.4、对象引用类型
-
强引用 -
软引用(SoftReference) -
弱引用(WeakReference) -
虚引用(PhantomReference)
6.4.1、强引用
6.4.2、软引用
6.4.3、弱引用
6.4.4、虚引用
拓展
7、垃圾回收算法
-
标记-清除算法 -
标记-整理算法 -
复制算法 -
分代收集算法
7.1、标记-清除
-
1、效率问题,标记和清除两个过程的效率都不高。 -
2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
7.2、标记-整理
-
1、相对标记清除算法,解决了内存碎片问题。 -
2、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
-
1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。
7.3、复制算法
-
1、效率高,没有内存碎片。
-
1、浪费一半的内存空间。 -
2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
7.4、分代算法
8、安全点
8.1、安全点
-
循环的末尾 (防止大循环的时候一直不进入 Safepoint ,而其他线程在等待它进入 Safepoint )。 -
方法返回前。 -
调用方法的 Call 之后。 -
抛出异常的位置。
8.2、安全区域
9、JVM 垃圾回收器
9.1、Serial (新生代)
最基本的单线程垃圾收集器。使用一个CPU或一条收集线程去执行垃圾收集工作。
工作时会Stop The World,暂停所有用户线程,造成卡顿。适合运行在Client模式下的虚拟机。
用作新生代收集器,复制算法。
9.2、ParNew(新生代)
Serial收集器的多线程版本,和Serial的唯一区别就是使用了多条线程去垃圾收集。
除了Serial,只有它可以和CMS搭配使用的收集器。
用作新生代收集器,复制算法。
9.3、Parallel Scavenge(新生代)
9.4、Serial Old(老年代)
Serial收集器的老年代版本,单线程,标记-整理 算法。
一般用于Client模式的虚拟机。
当虚拟机是Server模式时,有2个用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用 ,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
9.5、Parallel Old(老年代)
Parallel Scavenge收集器的老年代版本,使用多线程和 标记-整理 算法。在JDK 1.6中开始提供。在注重吞吐量的场合,配合Parallel Scavenge收集器使用。
9.6、CMS(Concurrent Mark Sweep)(老年代)
一种以获取最短回收停顿时间为目标的收集器。适合需要与用户交互的程序,良好的响应速度能提升用户体验。
基于 标记—清除 算法。适合作为老年代收集器。
收集过程分4步:
初始标记(CMS initial mark):只是标记一下GC Roots能直接关联到的对象,速度很快,会Stop The World。
并发标记(CMS concurrent mark):进行GC Roots Tracing(可达性分析)的过程。
重新标记(CMS remark):会Stop The -World。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般比初始标记阶段稍长些,但远比并发标记的时间短。
并发清除(CMS concurrent sweep):回收内存。
耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以时并发执行的。
缺点:
并发阶段,虽然不会导致用户线程暂停,但会占用一部分线程(CPU资源),导致应用变慢,吞吐量降低。默认启动收集线程数是(CPU数量+3)/4。即当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
无法清除浮动垃圾。并发清除阶段,用户线程还在运行,还会产生新垃圾。这些垃圾不会在此次GC中被标记,只能等到下次GC被回收。
标记-清除 算法会产生大量不连续内存,导致分配大对象时内存不够,提前触发Full GC。
9.7、G1
-XX:G1HeapRegionSize
E:eden区,新生代
S:survivor区,新生代
O:old区,老年代
H:humongous区,用来放大对象。当新建对象大小超过region大小一半时,直接在新的一个或多个连续region中分配,并标记为H
可预测的停顿时间:估算每个region内的垃圾可回收的空间以及回收需要的时间(经验值),记录在一个优先列表中。收集时,优先回收价值最大的region,而不是在整个堆进行全区域回收。这样提高了回收效率,得名:Garbage-First。G1中有2种GC:
young GC:新生代eden区没有足够可用空间时触发。存活的对象移到survivor区或晋升old区。mixed GC:当old区对象很多时,老年代对象空间占堆总空间的比值达到阈值(-XX:InitiatingHeapOccupancyPercent默认45%)会触发,它除了回收年轻代,也回收 部分 老年代(回收价值高的部分region)。
mixed GC回收步骤:
初始标记(Initial Marking):只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这阶段需要停顿线程(STW),但耗时很短,共用YGC的停顿,所以一般伴随着YGC发生。
并发标记(Concurrent Marking):进行可达性分析,找出存活对象,耗时长,但可与用户线程并发执行。
最终标记(Final Marking):修正并发标记阶段用户线程运行导致的变动记录。会STW,但可以并行执行,时间不会很长。
筛选回收(Live Data Counting and Evacuation):根据每个region的回收价值和回收成本排序,根据用户配置的GC停顿时间开始回收。
当对象分配过快,mixed GC来不及回收,G1会退化,触发Full GC,它使用单线程的Serial收集器来回收,整个过程STW,要尽量避免这种情况。
当内存很少的时候(存活对象占用大量空间),没有足够空间来复制对象,会导致回收失败。这时会保留被移动过的对象和没移动的对象,只调整引用。失败发生后,收集器认为存活对象被移动了,有足够空间让应用程序使用,于是用户线程继续工作,等待下一次触发GC。如果内存不够,就会触发Full GC。
9.8、ZGC
ZGC主要新增了两项技术,
着色指针Colored Pointer,
读屏障Load Barrier。
并发、基于区域(region)、增量式压缩
的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。
处理阶段:
标记(Marking);
重定位(Relocation)/压缩(Compaction);
重新分配集的选择(Relocation set selection);
引用处理(Reference processing);
弱引用的清理(WeakRefs Cleaning);
字符串常量池(String Table)和符号表(Symbol Table)的清理;
类卸载(Class unloading)
着色指针Colored Pointer
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。
相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于Java术语当中的引用。
在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。
由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),
读屏障Load Barrier
若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。
最后
看完本篇文章,相信你对JVM重新有了一定的认识,如果觉得有收获的话,可以帮我点一个在看,谢谢你的支持。
▐ JVM
小编微信|619531440
每天分享技术干货
视频 | 电子书 | 面试题 | 开发经验
以上是关于整理JVM知识点大梳理的主要内容,如果未能解决你的问题,请参考以下文章