JVM面试知识点1

内存结构(掌握内存结构划分、熟知各区域结构功能)

经典的JVM内存结构:

按照线程是否共享来划分:

Heap (堆区)

1. 堆区的介绍

堆是 OOM 故障最主要的发生区域。它是内存区域中最大的一块区域,被所有线程共享。存放的是实例化的对象信息;

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。

还可以细分为:新生代和老年代。再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

2. 堆区的调整

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。

通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M,其中 -X 这个字母代表它是 JVM 运行时参数,ms 是 memory start 的简称,中文意思就是内存初始值,mx 是 memory max 的简称,意思就是最大内存。

3. 堆的默认空间分配

创建一个新对象内存分配流程

Metaspace 元空间

在 HotSpot JVM 中,永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如 Class 和 Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。

Java 8 时,类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如 java/lang/Object 类元信息、静态属性 System.out、整形常量 100000 等。

元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

Java 虚拟机栈

对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致,并且是线程私有的。除了 Native 方法以外,Java 方法都是通过 Java 虚拟机栈来实现调用和执行过程的。

所以 Java 虚拟机栈是虚拟机执行引擎的核心之一。而 Java 虚拟机栈中出栈入栈的元素就称为「栈帧」。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

栈对应线程,栈帧对应方法

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间。是线程私有的。

程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。此区域也不会发生内存溢出异常。

垃圾回收机制(掌握垃圾判定的逻辑以及常见判定方法、掌握常见的垃圾回收算法、并熟知其回收线程)

1.背景:

一般来讲,在我们编程的过程中是会不断的往内存中写入数据的,而这些数据用完了要及时从内存中清理,否则会引发OutOfMemory(内存溢出) ,所以每个编程者都必须遵从这一原则。

Java比较好,因为JVM存在GC机制,也就是说JVM会帮我们自动清理垃圾

2. 两种回收机制

2.1 引用计数

什么是引用计数呢?打个比方A a = new A(),代码中 A 对象被引用 a 所持有,此时引用计数就会 +1 ,如果 a 将引用置为 null 即a = null此时对象 A 的引用计数就会变为 0 ,GC算法检测到 A 对象引用计数为 0 就会将其回收。

优点:执行效率高;缺点:无法解决互相引用问题;

2.2 可达性分析

在JVM中,会将一些特殊的引用作为 GcRoot ,如果通过 GcRoot 可以访达的对象不会被当作垃圾对象。换种方式说就是,一个对象被 GcRoot 直接 或 间接持有,那么该对象就不会被当作垃圾对象。用一张图表示大概就是这个样子:

优点:解决了引用计数法的互相引用问题;

图中A、B、C、D可以被 GcRoot 访达,所以不会被回收。E、F不能被 GcRoot 访达,所以会被标记为垃圾对象。最典型的是G、H,虽说相互引用,但不能被 GcRoot 访达,所以也会被标记为垃圾对象。综上所述: 可达性分析 可以解决 引用计数 中 对象相互引用 不能被回收的问题。

什么类型的引用可作为 GcRoot 呢。 大概有如下四种:

  • 栈中局部变量
  • 方法区中静态变量
  • 方法区中常量
  • 本地方法栈JNI的引用对象

3. 回收算法

3.1. 标记清除算法

获取所有的 GcRoot 遍历内存中所有的对象,如果可以被 GcRoot 就加个标记,剩下所有的对象都将视为垃圾被清除。

  • 优点:实现简单,执行效率高
  • 缺点:容易产生 内存碎片(可用内存分布比较分散),如果需要申请大块连续内存可能会频繁触发 GC
3.2. 复制算法

将内存分为两块,每次只是用其中一块。首先遍历所有对象,将可用对象复制到另一块内存中,此时上一块内存可视为全是垃圾,清理后将新内存块置为当前可用。如此反复进行

  • 优点:解决了内存碎片的问题
  • 缺点:需要按顺序分配内存,可用内存变为原来的一半。
3.3. 标记压缩算法

获取所有的 GcRootGcRoot 开始从遍历内存中所有的对象,将可用对象压缩到另一端,再将垃圾对象清除。实则是牺牲时间复杂度来降低空间复杂度

  • 优点:解决了标记清除的 内存碎片 ,也不需要复制算法中的 内存分块
  • 缺点:仍需要将对象进行移动,执行效率略低。

4. 分代回收策略

在JVM中 垃圾回收器 是很繁忙的,如果一个对象存活时间较长,避免重复 创建/回收垃圾回收器 进一步造成负担。

JVM制定了 分代回收策略 为每个对象设置生命周期 ,堆内存会划分不同的区域,来存储各生命周期的对象。一般情况下对象的生命周期有 新生代、老年代、永久代(java 8已废弃)

4.1. 新生代

新生代内存工作流程:

  • 当一个对象刚被创建时会放到 Eden 区域,当 Eden 区域即将存满时做一次垃圾回收,将当前存活的对象复制到 SurvivorA ,随后将 Eden 清空
  • Eden 下一次存满时,再做一次垃圾回收,先将存活对象复制到 SurvivorB ,再把 EdenSurvivorA 所有对象进行回收,
  • Eden 再一次存满时,再做一次垃圾回收,将存活对象复制到 SurvivorA,再把 EdenSurvivorB对象进行回收。如此反复进行大概 15 次,将最终依旧存活的对象放入到老年代区域。

新生代工作流程与 复制算法 应用场景较为吻合,都是以复制为核心,所以会采用复制算法

4.2. 老年代

当一个对象存活时间较久会被存入到 老年代 区域。 老年代 区即将被存满时会做一次垃圾回收。

所以 老年代 区域特点是存活对象多、垃圾对象少,采用标记压缩 算法时移动少、也不会产生内存碎片。所以老年代 区域可以选用 标记压缩 算法进一步提升效率。

5. 四大引用

JVM为我们提供了四种对象引用方式:强引用、软引用、弱引用、虚引用 供我们选择,下面我用一张表格来做一下类比。

类的加载流程:(使用自己的理解进行阐述)

一、引言

当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。

二、类的加载、链接、初始化

1、加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。

1.1、加载的class来源
  • 从本地文件系统内加载class文件
  • 从JAR包加载class文件
  • 通过网络加载class文件
  • 把一个java源文件动态编译,并执行加载。
2、类的链接

通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。

2.1、验证

验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。

2.2、准备

类准备阶段负责为类的类变量分配内存,并设置默认初始值。

2.3、解析

解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。

简单来说就是通过类加载器加载当前类引用的其他对象;

3、类的初始化

类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用< clinit>方法,进行类变量的初始化。

java类中对类变量进行初始化的两种方式:

  1. 在定义时初始化
  2. 在静态初始化块内初始化
3.1、< clinit>方法相关
  • 虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。
  • 如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。
  • 接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。
  • 虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。
3.2、类初始化时机
  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
  3. 当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;
  4. 子类初始化过程会触发父类初始化;
  5. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;
  6. 使用反射API对某个类进行反射调用时,初始化这个类;
  7. Class.forName()会触发类的初始化
3.3、final定义的初始化

对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。

如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。

3.4、ClassLoader只会对类进行加载,不会进行初始化

ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。

三、类加载器

类加载器负责将.class文件(不管是jar,还是本地磁盘,还是网络获取等等)加载到内存中,并为之生成对应的java.lang.Class对象。一个类被加载到JVM中,就不会第二次加载了。

那怎么判断是同一个类呢?

每个类在JVM中使用全限定类名(包名+类名)与类加载器联合为唯一的ID,所以如果同一个类使用不同的类加载器,可以被加载到虚拟机,但彼此不兼容。

1、JVM类加载器分类
1.1、Bootstrap ClassLoader

Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有 C++实现的。

根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。

1.2 、Extension ClassLoader

Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。

1.3、 Appliaction ClassLoader

System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。

1.4 自定义类加载器

四、类加载机制

1.1、JVM主要的类加载机制。
  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。
  2. 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。
  3. 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。

五、创建并使用自定义类加载器

1、自定义类加载分析

除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。

ClassLoader类有两个关键的方法:

  1. protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。
  2. protected Class findClass(String name) :根据指定类名来查找类。

所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部回调用findClass方法。

loadClass加载方法流程:

  1. 判断此类是否已经加载;
  2. 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;
  3. 如果前面都没加载成功,则使用findClass方法进行加载。

所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。

2、实现自定义类加载器

ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。

垃圾回收器:

CMS垃圾回收的过程:

CMS(Concurrent Mark-Sweep)收集器是 Java 虚拟机中的一种老年代(Old Generation)垃圾收集器,它主要目标是减少垃圾收集时的应用程序停顿(STW)时间。

CMS 使用并发(Concurrent)的方式来执行垃圾收集,使用的是“标记-清理”算法,尽量减少在垃圾收集过程中应用程序的暂停时间。

CMS收集器整体流程

其执行过程分为初始标记、并发标记、重新标记和并发清理等过程

初始标记(预先标记):

只是标记GC Roots能直接关联到的对象,但需要“Stop The World”停顿,即在此期间暂停所有应用线程。(用于标记直接被GCRoot获取的对象,会造成stw);

并发标记:

从GC Roots出发标记上所有和GC Root相连的存活对象,不会stw;(用于标记在GC链路上的对象)

重新标记:

修正一下GC线程和用户线程同时跑时产生的错标和漏标;也需要stw,但是停顿时间较短;

并发清除:

GC线程会清除不再被引用的对象,并回收他们占用的内存空间。(多GC线程,用户线程和GC线程同时跑),不会stw;

G1 GC(垃圾优先收集器)是JDK 9以后的默认回收器

1.1、G1收集器的堆空间内存划分

在1.7之后,我们可以通过参数-XX:+UseG1GC装配它

堆中的内存区域被划为了一个个的Region区。

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。在运行时,每个分区都会被打上唯一的分区标识。 不过在G1收集器中,年轻代Eden区、幸存区Survivor、老年代Old区这些概念依旧还存在,但却成为了逻辑上的概念,这样做的好处在于:也可以复用之前分代框架的逻辑,同时也满足了Java对象朝生夕死的特性。

不过在HotSpot的源码TARGET_REGION_NUMBER定义了Region区的数量限制为2048个(实际上允许超过这个值,但是超过这个数量后,堆空间会变的难以管理)。

一般Region区的大小等于堆空间的总大小除以2048,比如目前的堆空间总大小为8GB,就是8192MB/2048=4MB,那么最终每个Region区的大小为4MB,当然也可以用参数-XX:G1HeapRegionSize强制指定每个Region区的大小,但是不推荐;

默认新生代对堆内存的初始占比是5%;可以通过-XX:G1NewSizePercent设置新生代初始占比。

最多新生代的占比不会超过堆空间总大小的60%,可以通过-XX:G1MaxNewSizePercent调整。

G1中的年老代晋升条件和之前的无差,达到年龄阈值的对象会被转入年老代的Region区中,不同的是对于大对象的分配,在G1中不会让大对象进入年老代,在G1中由专门存放大对象的Region区叫做Humongous区,如果在分配对象时,判定出一个对象属于大对象,那么则会直接将其放入Humongous区存储。

在G1中,判定一个对象是否为大对象的方式为:对象大小是否超过单个普通Region区的50%,如果超过则代表当前对象为大对象,那么该对象会被直接放入Humongous区。

当堆空间发生全局GC(FullGC)时,除开回收新生代和年老代之外,也会对Humongous区进行回收。

G1收集器具备如下特性:
● ①与CMS收集器一样,能够与用户线程同时执行,完成并发收集。
● ②GC过程会有整理内存的过程,不会产生内存碎片,并且整理空闲内存速度更快。
● ③GC发生时,停顿时间可控,可以让程序更大程度上追求低延迟。
● ④追求低延迟的同时,尽可能会保证高吞吐量。
● ⑤对于堆的未使用内存可以返还给操作系统。
1.2、G1收集器GC类型

G1中主要存在YoungGC、MixedGC以及FullGC三种GC类型,这三种GC类型分别会在不同情景下被触发。

1.2.1、YoungGC

所以YoungGC并非说Eden区放满了就会立马被触发,在G1中,当新生代区域被用完时,G1首先会大概计算一下回收当前的新生代空间需要花费多少时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么不会触发YoungGC,而是会继续为新生代增加新的Region区用于存放新分配的对象实例。

1.2.2、MixedGC

当整个堆中年老代的区域占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值后触发MixedGC,发生该类型GC后,会回收所有新生代Region区、部分年老代Region区(会根据期望的GC停顿时间选择合适的年老代Region区优先回收)以及大对象Humongous区。

1.2.3、FullGC

当整个堆空间中的空闲Region不足以支撑拷贝对象或由于元数据空间满了等原因触发,在发生FullGC时,G1首先会停止系统所有用户线程,然后采用单线程进行标记、清理和压缩整理内存,以便于清理出足够多的空闲Region来供下一次MixedGC使用。但该过程是单线程串行收集的,因此这个过程非常耗时的(ShenandoahGC中采用了多线程并行收集)。

1.3、G1收集器垃圾回收过程

G1收集器一般在发生GC时执行过程大致会分为四个步骤(主要指MixedGC):

  • ①初始标记(InitialMark):先触发STW,然后使用单条GC线程快速标记GCRoots直连的对象。
  • ②并发标记(ConcurrentMarking):与CMS的并发标记过程一致,采用多条GC线程与用户线程共同执行,根据Root根节点标记所有对象。
  • ③最终标记(Remark):同CMS的重新标记阶段,主要是为了纠正并发标记阶段因用户操作导致的错标、误标、漏标对象。
  • ④筛选回收(Cleanup):先对各个Region区的回收价值和成本进行排序,找出「回收价值最大」的Region优先回收。

在G1中不管是新生代还是年老代,回收算法都是采用复制算法,在GC发生时都会将一个Region区中存活的对象复制到另外一个Region区内。同比之前的CMS收集器采用的标-清算法而言,这种方式不会造成内存碎片,因此也不需要花费额外的成本整理内存。

一款源自于JDK11的性能魔兽 - ZGC

它也是一款基于分区概念的内存布局GC器,这款GC器是真正意义上的不分代收集器,因为它无论是从逻辑上,还是物理上都不再保留分代的概念。

Java引入ZGC的目的主要有如下四点:
● ①奠定未来GC特性的基础。
● ②为了支持超大级别堆空间(TB级别),最高支持16TB。
● ③在最糟糕的情况下,对吞吐量的影响也不会降低超过15%。
● ④GC触发产生的停顿时间不会偏差10ms。
2.1、ZGC堆空间内存划分

在ZGC中,也会把堆空间划分为一个个的Region区域,但ZGC中的Region区不存在分代的概念,它仅仅只是简单的将所有Region区分为了大、中、小三个等级,如下:

  • 小型区/页(Small):固定大小为2MB,用于分配小于256KB的对象。
  • 中型区/页(Medium):固定大小为32MB,用于分配>=256KB ~ <=4MB的对象。
  • 大型区/页(Large):没有固定大小,容量可以动态变化,但是大小必须为2MB的整数倍,专门用于存放>4MB的巨型对象。但值得一提的是:每个Large区只能存放一个大对象,也就代表着你的这个大对象多大,那么这个Large区就为多大,所以一般情况下,Large区的容量要小于Medium区,并且需要注意:Large区的空间是不会被重新分配的(稍后分析)。
2.2、ZGC回收过程

ZGC收集器在发生GC时,其实主要操作只有三个:标记、转移与重定位。

  • 标记:从根节点出发标记所有存活对象。
  • 转移:将需要回收区域中的存活对象转移到新的分区中。
  • 重定位:将所有指向转移前地址的指针更改为指向转移后的地址。

ZGC中的一次垃圾回收过程会被分为十个步骤:初始标记、并发标记、再次标记、并发转移准备:[非强引用并发标记、重置转移集、回收无效页面(区)、选择目标回收页面、初始化转移集(表)]、初始转移、并发转移,其中只有初始标记、再次标记、初始转移阶段会存在短暂的STW,其他阶段都是并发执行的。

2.2.1、ZGC的核心 - 染色指针(colored pointers)技术

ZGC的核心技术之一,在此之前所有的GC信息保存在对象头中,但ZGC中的GC信息保存在指针内。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值