JVM完全解析(☆)

文章目录

一、JVM概述

0.JVM作用:

①:将.class字节码文件生成为操作系统指令,传递给操作系统后通过计算机运行。

1.jvm的位置

在这里插入图片描述

2.Jvm的整体结构(HotSpot虚拟机)

在这里插入图片描述

3.Java代码的执行流程

在这里插入图片描述

  1. 首先java程序经过前端编译器生成.class字节码文件;
  2. 虚拟机将字节码文件生成为对应的操作系统指令
  3. 操作系统处理成计算机能够运行的指令

4.jvm的运行周期

  1. 启动:通过类引导加载器创建一个初始类来完成;
  2. 执行:程序开始执行的时候,Jvm启动,程序执行结束的时候,Jvm就停止;
  3. 停止
    1. 程序正常执行结束;
    2. 程序因为异常终止;
    3. 程序因为操作系统的异常而终止
    4. 自己调用了System类中的exit()方法使程序退出;

二、类加载子系统(☆)

1.作用:

将.class文件加载到内存,并对数据进行检验、解析及初始化,最终形成虚拟机能够直接使用的java类型,加载的类信息存放在方法区中。

注:只负责加载,能不能运行,由执行引擎决定。

2.类加载分为哪些过程:

  1. 加载
  2. 连接:验证,准备以及解析
  3. 初始化

3.类加载各阶段的作用:

  1. 加载
    1. 通过一个类的全限定名来获取此类的二进制字节流;
    2. 将该二进制字节流所代表的的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的数据访问入口。
  2. 连接
    1. 验证:确保Class文件的字节流(二进制)中包含的信息符合规范,保证这些信息被当做代码运行后不会对虚拟机造成危害。分为:文件格式验证元数据验证字节码验证符号引用验证四个方面。
    2. 准备:正式为类的静态变量分配内存并设置初始值(隐式初始化->相当于赋默认值)。
    3. 解析:Java虚拟机将常量池中的符号引用替换为直接引用。分为:类或接口的解析、字段解析及方法解析、接口方法解析
  3. 初始化:在准备阶段,静态变量已经赋过一次系统要求的初始零值,而在初始化阶段,会根据程序员通过程序编码指定的主观计划去初始化静态变量和其他资源。

4.简单描述一下()方法?

  1. 定义:()方法是由编译器自动收集类中的所有的静态变量的赋值和静态代码块语句合并产生的,没有的话则不会产生。
  2. 顺序:编译器收集的顺序是由语句在源文件中出现的顺序决定,静态语句块中只能访问到定义在静态语句块之前的静态变量。
  3. 其他:
    1. java虚拟机会保证在子类的()执行前,父类的()方法已经执行完毕。
    2. 接口中不能使用静态代码块,但是仍然有赋值初始化操作,执行接口的()方法不需要先执行父接口的()方法。
    3. .Java虚拟机必须保证一个类的()方法在多线程的环境中被同步加锁。如果多个线程同时去初始化一个类,必须只能其中一个去执行这个类()方法。
补充:Java中(静态)代码块、(静态)变量的执行顺序
public class Parent {
    static {
        System.out.println("Parent static block");
    }

    {
        System.out.println("Parent non static block");
    }

    final static Value i = new Value("Parent static value");

    Value j = new Value("Parent non static value");

    Parent() {
        System.out.println("Parent Constructor");
    }
}

public class Child extends Parent {
    static {
        System.out.println("Child static block");
    }

    final static Value i = new Value("Child static value");

    {
        System.out.println("Child non static block");
    }

    Value j = new Value("Child non static value");

    Child() {
        System.out.println("Child Constructor");
    }
}

public class Value {
    public Value(String value) {
        System.out.println(value);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

结果

Parent static block
Parent static value
Child static block
Child static value
Parent non static block
Parent non static value
Parent Constructor
Child non static block
Child non static value
Child Constructor

分析总结

  1. 执行父类静态代码块和静态变量
  2. 执行子类静态代码块和静态变量
  3. 执行父类非静态代码块和非静态变量
  4. 执行父类构造函数
  5. 执行子类非静态代码块和非静态变量
  6. 执行子类构造函数

> 可以得出顺序:父类静态 →→ 子类静态 →父类非静态 → 父类构造函数 → 子类非静态 →子类构造函数

注意:以上流程3和5步骤中具体的执行顺序,是由代码书写顺序决定的

5.类加载器是什么?

在类的加载阶段中 “通过一个类的全限定名来获取类的二进制字节流” 实现这个动作的代码称为类加载器

6.类加载器的分类:

  1. 启动类加载器(引导类加载-Bootstrap ClassLoader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应有程序类加载器(系统类加载器-Application ClassLoader)
  4. 自定义加载器(Custom ClassLoder)

7.双亲委派模型

1.双亲委派模型的工作过程:

在这里插入图片描述
在这里插入图片描述

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在父类加载器,则会继续向上委托,请求最终达到顶层的引导类加载器;
  3. 如果父类引导器可以完成加载任务,则成功返回;否则子加载器会尝试自己执行,这就是双亲委派机制。

双亲委派:一个java类加载堇JVM内存的过程

  1. 每个类加载器对它加载过的类都有一个缓存
  2. 向上未委托查找,向下委托加载。
2.双签委派模型的优势:
  1. 避免类的重复加载
  2. 沙箱安全机制:保护程序安全,防止核心类库被随意更改(举例:自定义一个java.lang.String类

8.破坏双亲委派模型

1.双亲委任模型时如何实现的?

面试官:谈谈类加载器吧,你有没有看过类加载器的源码

在这里插入图片描述

逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass方法, 如父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass方法进行加载。

2.破坏双亲委派模型
  1. 第一次“破坏”
    双亲委派机制是在jdk1.2时出现的,因此在jdk1.2之前不满足双亲委派机制,即java.lang.ClassLoader抽象类,用户编写子类,然后能够重写loadClass()方法,这样就破坏了双亲委派机制;
  2. 第二次“破坏”
    1. 缺陷:越基础的类由越上层的类加载器进行加载,但是越基础的类型又需要去调用用户的代码,用户的代码一般由系统类加载器进行加载,这样引导类加载器无法调。如:一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢?
    2. 解决:线程上下文类加载器,启动类加载器想要调用我们的用户代码时,首先委托给线程上下文类加载器,然后线程上下文类加载器再去调用用户的程序代码。
3.Tomcat 的类加载器是怎么设计的?
1.Tomcat 如果使用默认的类加载机制行不行?
  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。

  3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

  5. 第一个问题:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份

  6. 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。第三个问题和第一个问题一样

  7. 我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

2.Tomcat 如何实现自己独特的类加载机制?

在这里插入图片描述

CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

三、运行时数据区(☆)

1.概述:

在这里插入图片描述

其中:方法区是线程共享的;程序计数器本地方法栈及虚拟机栈都是每个线程一份

2.程序计数器

1.程序计数器作用

用于存储下一条字节码指令的地址,执行引擎执行完当前指令,根据程序计数器的地址执行下一条指令。

2.程序技术器的特点?
  1. 线程私有:每个线程都有自己的程序计数器;
  2. 当前方法:任何时间一个线程都只有一个方法在执行,程序计数器会存储线程当前执行方法的Jvm指令的地址。
  3. 程序计数器的特点唯一一个没有OOM的内存区域
3.为什么需要使用程序计数器来记录当前线程的地址?
  1. CPU需要不断切换不同的线程,切换回来需要知道从哪条指令开始。
  2. 字节码解释器需要知道下一条执行的执行地址。
4.为什么要将程序计数器设计为线程私有的?

为了能够准确记录各个线程的当前正在执行的指令地址,最好的办法就是为每一个线程设置一个程序计数器

在这里插入图片描述

3.Java虚拟机栈

作用

  • 每个线程运行时所需要的内存空间,则称为虚拟机栈
  • 每个栈都由多个栈帧(Frame)组成,对应着每次方法调用时所占有的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

虚拟机栈线程私有,生命周期与线程相同。

1.栈帧(Frame)

在这里插入图片描述

  • 局部变量表(Local Variable Table) 存放局部变量的列表
    • 一个局部变量表可以保存类型为:boolean,byte,char,short float ,reference和returnAddress的数据
    • 两个局部变量表可以保存一个类型为long和double的数据
    • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
  • 操作数栈(Operand Stack)
    • 随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
      底层是由数组来实现的栈,不能通过索引来操作具体的数据,只能是由栈的操作,即出栈和入栈操作。
  • 动态连接(Dynamic Linking) 简单理解为指向运行时的常量池引用
    • Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
  • 方法返回地址(Return Address)
    • 无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
2.虚拟机栈面试题
  1. 垃圾回收是否涉及到虚拟机栈?
    不会,栈内存是一次次的方法调用产生的栈帧内存,一次方法调用后就会弹出栈,就会被自动的回收掉,所以根本不需要垃圾回收来管理栈内存,垃圾回收只是回收堆内存中的无用对象和方法区的常量。
  2. 方法中定义的局部变量是否是线程安全的?
    具体问题具体分析:
  • 线程安全的情况:如果是单个线程或者说在方法内部创建内部消亡的话,则是线程安全的,

  • 线程不安全的情况:多个线程共享这个局部变量时有可能存在线程安全问题。

4.本地方法栈

  1. 虚拟机栈与本地方法栈的区别:
    虚拟机栈用于管理Java方法的调用,而本地方法栈用于本地方法的调用。
  2. 本地方法
    本地方法是有C、C++等语言编写的,是Java的拓展方法库。
  3. 本地方法是有C、C++等语言编写的,是Java的拓展方法库。
    本地方法栈是线程私有的,Hot Spot虚拟机中将其和虚拟机栈合二

5.堆(Heap)

  • 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块

  • 应用系统对象都保存在Java堆中

  • 所有线程共享Java堆

  • 对分代GC来说,堆也是分代的

  • GC管理的主要区域

  • 堆随着JVM的启动而创建,堆大小一旦创建完成就不能更改;

  • 堆内存的大小是可以调节的。 java -Xmx(设置堆最大小) java -Xms(设置初始化堆大小)

  • 堆在物理上是不连续的,但是在逻辑上连续的;

注:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

1.与垃圾回收的关系

是垃圾回收的重点区域;

在方法结束后,堆中的对象不会马上被清除,需要GC进行判断。

2.堆内存细分
  • jdk1.7之前将堆分为:新生代+老年代+永久代;
  • jdk1.8之后将堆从逻辑上分为:新生代+老年代+元空间

在这里插入图片描述

3.设置堆内存的大小及OOM问题
1.设置堆内存大小
  1. .-Xms:设置堆空间(新生代+老年代)的起始内存大小
  2. -Xmx:设置堆空间(新生代+老年代)的最大内存大小
  3. 默认的堆空间的起始内存大小:电脑运行内存的大小/64;
  4. 默认的堆空间的最大内存大小:电脑运行内存的大小/4;
4.新生代与老年代
1.什么是年轻代?什么是老年代?

存储在堆中的Java对象可以分类两类,一类是新生代,一类是老年代;

新生代:一类是声明周期较短的瞬时对象,这类对象从创建到消亡时间较短暂;新生代包括伊甸园区,两个幸存区

老年代:一类对象的声明周期比较长,在极端情况下还可能与JVM的声明周期一致;

在这里插入图片描述

2.新生代与老年代的默认内存比例

默认的新生代与老年代的比例是1:2

3.HotSpot虚拟机中,新生代与Survivor的比例是多少?

通过-XX:SurvivorRatio的值进行调节;默认的是8:1:1;实际使用时不为8:1:1,因为存在自适应内存分配策略(默认开启)

1.几乎所有的Java对象都是在Eden区被new出来(如果对象查过Eden大小,则直接进入老年代);绝大部分的java对象销毁都发生在新生代;
5.对象内存分配的一般过程(垃圾回收的一般过程)

在这里插入图片描述

  1. new出来的对象会被存储在伊甸园区,然后当伊甸园区的空间被占满后,此时会YGC,将没有被其他对象所引用的对象进行回收,然后将伊甸园区剩余对象加载到Survivor0区(from区),并将年龄标记为1,再加载新的对象放在伊甸园区。
  2. 如果再次触发YGC垃圾回收,上次放在幸存者0区的对象会放在幸存者1区,年龄标记+1.然后伊甸园区幸存下来的对象会被放到幸存者1区,年龄标记为1,然后幸存者1区变为from区,幸存者0区变为to区;
  3. 如果再次发生YGC,则会重复此过程,当from区的幸存对象的标记年龄超过15后,则会移动到老年代中;如果存在着大于伊甸园区的对象,则会直接进入到老年代中;当老年代内存不足时,会触发GC,进行老年代回收;

注: 用默认的垃圾回收器才是15,1.8默认开启(
Parallel scavenge+parallel old收集算法),G1收集器则为6

总结:对幸存者1区2区:复制后交换,谁空谁为to区;频繁在新生代收集,较少在老年代收集,几乎不在永久代/元空间收集

6.对象内存分配特殊情况(当出现超大对象的情况时)

在这里插入图片描述

7.Minor GC和Major GC和Full GC
三者区别
1.部分收集(Partial GC):不是完整收集整个java堆的垃圾收集
  1. 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  2. 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
  3. 混合收集:收集整个新生代和部分老年代的垃圾收集

目前只有CMS垃圾收集器才有单独的老年代的收集

目前只有G1垃圾收集器具备混合收集(因为它是基于region的垃圾收集)

2.整堆收集(Full GC):完整收集java堆及方法区的垃圾收集
8.GC描述
1.Minor GC
  1. 触发条件:当伊甸园区满了,就会触发Minor GC,回收整个年轻代的垃圾(s0和s1区满不会触发Minor GC)
  2. 触发后果:当进行Minor GC时,会造成STW(Stop The World),会停止运行用户其他线程。
2.Major GC
  1. 触发条件:当老年代的内存空间不足时,会优先触发Minor GC,如果之后还不足,则会触发Major GC
  2. 触发后果:Major GC比Minor GC的速度要慢十倍以上,造成更长时间的STW。
3.Full GC

触发条件

  1. 调用System.gc()时,系统建议执行Full GC;
  2. 老年代的空间不足
  3. 方法区的空间不足时
9.堆空间的分代思想
  1. 为什么分为新生代和老年代(面试重点)

不同对象的生命周期是不同的,70%-90%的对象的生命周期是短暂的,在新生代主要存放生命周期短暂的对象,在老年代存放新生代中经过多次垃圾回收之后依然没有没有被回收的对象,这样做主要是为了优化GC的性能,如果不分新生代和老年代,而是将对象都放在一起,则每次GC的时候都要遍历整个堆空间,会造成STW的时间较长,影响用户性能体验。

10.内存分配策略
  1. 优先分配到新生代
  2. 大对象直接分配到老年代
  3. 长期存活的对象(s0,s1区中年龄计算器值超过设定阈值或默认阈值15)分配到老年代;
  4. 动态对象年龄判断:如果Survivor区中相同年龄的对象的总和大于等于Survivor区的一半,则大于等于该年龄的对象直接进入到老年代中。

补充:大对象直接进入到老年代代码(创建大小为20M的数组,设定堆空间的初始内存和最大内存都为60M,然后设置新生代与老年代的比例为1:2,即老年代40M,新生代20M,然后设定伊甸园区与幸存者区比例为8:1:1,则伊甸园区16M,不足以放下20M的数组)

在这里插入图片描述

11.TLAB(Thread Local Allocation Buffer:线程私有缓存区)
1.什么是TLAB?

jvm在伊甸园区为每一个线程开辟了一块私有的缓存区,每个TLAB只占伊甸园区的1%,JVM将TLAB作为内存分配的首选

在这里插入图片描述

2.为什么要有TLAB?

因为堆空间是线程共享的,如果存在多个线程同时访问堆空间的某个对象,则可能会造成线程安全问题,可以采用加锁机制实现同步处理,但是会影响分配速度,因此采用TLAB的方式解决

3.补充
  1. 尽管不是所有的对象都在TLAB分配内存,但是JVM会优先在TLAB为对象分配内存
  2. 当TLAB内存空间不足时,会采用同步加锁机制在伊甸园其他区域分配内存,确保数据操作的原子性
12.堆是分配对象存储的唯一选择吗

在这里插入图片描述

随着即时编译器的发展与逃逸分析技术的不断成熟,虚拟机栈上分配等技术的出现,使得对象分配在堆上就没有那么绝对了。

如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,则有可能会被优化成栈上分配。

如:
在这里插入图片描述

1.简述逃逸分析(只知道什么逃逸分析即可)?
  1. 什么是逃逸分析?
    1. 如果将堆上的对象分配到栈的话,需要使用逃逸分析技术。
    2. 虚拟机能够通过分析来判断对象的引用范围是否在方法内部,从而决定是否将对象份分配到堆上。
  2. 如何判断是否发生了逃逸?
    1. 如果一个对象在方法中被定义后,对象只在方法内部进行使用,则认为没有发生逃逸。
    2. 如果一个对象在方法内被定义后,对象被外部方法所引用,则认为发生了逃逸

JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。

6.方法区

1.堆、栈和方法区的交互关系
  1. 内存层面

在这里插入图片描述

运行时数据区(Runtime Data Region)
主要包括虚拟机栈方法区(jdk1.8后称为元空间)、程序技术器本地方法栈。

  1. 线程共享层面
    在这里插入图片描述
  • 线程共享:堆和方法区
  • :GC的重点回收区,如果内存不足会报出OOM
  • 方法区:GC也会对此部分进行回收,回收的频率极低;内存不足时也会报出OOM
  • 线程私有:程序计数器、虚拟机栈及本地方法栈
  • 程序奇数器:无GC、无异常;
  • 虚拟机栈:无GC,存在异常:StackOverFlow(栈溢出)
  • 本地方法栈:无GC,存在异常:StackOverFlow(栈溢出)
  1. 创建对象层面(☆)

在这里插入图片描述

如上图所示:Person类信息会存储在方法区当中,而person变量如果定义在方法内,则会存储在虚拟机栈的栈帧的局部变量表中,而右侧的new出来的对象则会存放在堆中;

下面这个图为:虚拟机栈中存储着对象的引用地址,指向位于堆空间对象的实例,而堆空间中对象实例数据中包含指向存储在方法区的对象类型数据的指针。

2.方法区的理解
1.jdk1.8前后的不同
  1. jdk1.8之前为永久代;jdk1.8之后称为元空间(MetaSpace)
  2. 永久代处于虚拟机中的内存中,而元空间不在虚拟机设置的内存中,使用本地内存

在这里插入图片描述

2.方法区补充说明
  1. 作用:保存类信息相关信息;
  2. 声明周期:随着JVM的创建而创建,随着JVM的关闭而死亡;
  3. 异常:方法区的大小决定了系统可以创建多少个类,如果创建过多的类,导致方法区溢出,就会出现OOM;
  4. 线程:是多个线程共享的区域;
3.调节方法区的大小和OOM
元空间的默认大小和设置方式
  1. 默认大小:初始默认大小为:21M;最大显示为 -1,即没有限制
  2. 设置:初始内存大小:-XX:MetaspaceSize=想设置的值;最大内存大小:MaxMetaspaceSize=设置值;
如何解决OOM问题

在这里插入图片描述

4.方法区的内部结构

在这里插入图片描述

1.方法区中都存储哪些信息?
  1. 类信息
    1. 类的名称(包名.类名)
    2. 父类名称
    3. 修饰符信息
    4. 接口的有序列表
  2. 常量
  3. 静态变量
    1. jdk1.7 被static修饰的变量或者静态代码块
    2. jdk1.8静态变量存放在堆空间中
  4. 即时编译器代码缓存
    1. 将热点代码存储在方法区中,解释为机器指令后并不执行,以做备用
  5. 域信息(属性等)
    1. 域的修饰符、类型及域的名称等信息
  6. 方法信息(方法的)
    1. 方法的名称、修饰符、返回值类型、参数的类型及个数、异常表信息及字节码信息等
2.运行时常量池
  1. 什么是运行时常量池?
    .class字节码文件中的常量池用于存放编译后的各种字面量及符号引用,在加载类和接口到虚拟机后,这部分内容就转为了运行时常量池。
  2. 为什么需要常量池?
    常量池中包含了类中各种字面量及对类型、方法及域的各种符号引用,这样可以有效减少字节码文件的大小。
3.方法区在jdk6和jdk7及jdk8的不同(重点)?

在这里插入图片描述

  1. jdk1.6:静态变量存放在永久代上,字符串常量池存放在运行时常量池中

在这里插入图片描述

  1. jdk1.7:逐渐已经放弃“永久代”,静态变量字符串常量池存放在
    在这里插入图片描述

  2. jdk1.8及以后,永久代被元空间取代,整体从JVM设置内存中移动到本地内存中,但是静态变量和字符串常量池依然保存在堆中。(在jdk1.7的基础上将方法区拿到本地内存中为元空间)。
    在这里插入图片描述

4.永久代替换为元空间的好处有哪些?
  1. 永久代的大小比较有限,而元空间是基于本地内存,相对更大,因此OOM的发生的几率就越小;
  2. 对永久代的调优更加困难,因此当方法区空间不足时,会引起Full GC,类型的卸载条件相当苛刻,因此尽量保证不发生Full GC。
5.方法区的垃圾回收
1.回收的主要对象(内容)是什么?

常量池中废弃的常量(包括字面量和符号引用)及不再使用的类型;

2.常量的回收策略?

常量池中的常量没有被任何对象所引用,则会被回收

3.判定类不再被使用的条件?
  1. 该类的所有实例都已经被回收
  2. 加载该类的类加载器已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用
6.对象的实例化与访问
1.对象的实例化
  1. 对象的创建方式
    在这里插入图片描述

除了clone和反序列化外都会调用构造函数

  1. 对象的创建/实例化的步骤(Object obj=new Object()的整个过程)(面试重点):
    在这里插入图片描述
2.对象在内存布局

在这里插入图片描述

1.普通对象的生成,分为4块
  • markword 8个字节,存放一些描述信息
    主要包含对象的哈希值GC分代年龄(from区向to区移动,分代年龄+1)锁信息
  • Class pointer(类指针) 4个字节,原本应该是8个字节,jvm对该块进行了压缩,压缩成4个字节 eg:T t = new T(); t->T.class
  • instance data(实例数据) 根据实际来计算大小
  • padding(对齐) jvm默认64位,一次读8个字节,一次读8个字节效率是最高的,如果生成的对象不足8字节,8字节对齐

在这里插入图片描述

2.数组的生成,分为5块
  • markword 8个字节,存放一些描述信息
  • Class pointer(类指针) 4个字节,数组指向的什么类型,Intger,Long等
  • length(数组长度)
  • instance data(实例数据) 根据实际来计算大小
  • padding(对齐) jvm默认64位,一次读8个字节,一次读8个字节效率是最高的,如果生成的对象不足8字节,8字节对齐
3.举例子说明该过程(定义一个Customer类,然后里面有属性,代码块,调用另外一个类来创建类实例,然后创建该类的对象/实例)

在这里插入图片描述

内存分布情况

在这里插入图片描述

3.对象的访问定位

对象的访问定位方式有几种(JVM是如何通过虚拟机栈中的栈帧中的局部变量表中的地址值访问到对象的实例数的呢?),两种方式的优缺点?

在这里插入图片描述

  1. 直接指针(Hotspot虚拟机采用)
    通过局部变量表中的引用地址值找到位于堆空间的对象的实例数据,实例数据中的对象头中包含指向方法区(元空间)的类元信息的类型指针

    优点和缺点与句柄访问相反。

  2. 句柄方式
    通过局部变量表中的地址值找到堆空间中的句柄池,句柄池中存在两个指针,一个指针指向堆空间的对象实例数据;另一个指针指向方法区(元空间)中对象的类元信息。

    优点:在GC环节,from区到to区对象会频繁移动,此时只需改变句柄池中的指针即可,虚拟栈中的地址不需改动;
    缺点:句柄池占用堆空间;对象的访问效率相对降低

四、执行引擎(☆)

1.执行引擎概述

java是半编译半解释型语言
1.作用

将字节码指令解释/编译为对应平台上的本地机器指令。(因为本地机器指令无法识别字节码指令)

2.结构
  1. 解释器
    (java代码->java字节码)->c++代码->机器码
  2. 即时编译器(JIT-(Just In Time))
    java字节码->机器码
  3. GC
    在这里插入图片描述
3.执行过程
  1. 首先通过程序计数器找到执行的字节码指令
  2. 通过虚拟机栈栈帧中的 局部变量中找到堆空间中的对象实例数据
  3. 通过对象中的类型指针找到方法区类元信息

2.java代码的编译和执行的过程

1.java代码编译和执行的过程

在这里插入图片描述

  1. 橙色部分:前端编译,编译器将java源文件编译为字节码文件
  2. 绿色部分:解释器将字节码文件解释成本地的机器指令,并逐行执行
  3. 蓝色部分:JIT(即时编译器)将字节码指令编译为本地机器指令,但是不执行(解释热点代码,热点代码放在方法区中),内部有模板编译器。
2.为什么java是半编译半解释语言

在这里插入图片描述

1.原因

前端编译(javac),后端执行

编译指的是javac编译生成的字节码文件.class,但为什么是半呢,是因为生成的这个.class文件操作系统不能直接执行,需要解释器进行解释后(机器码),才可能运行,所以才把java叫做半编译半解释型语言。

2.什么是编译型语言,什么是解释型语言?
  • 编译型语言:编写好程序以后,首先需要编译器进行编译,统一转化成机器码,然后这个编译完的文件,可以放在操作系统直接执行
  • 解释型语言: 程序是边运行边进行机器码转化(转化完后cpu执)
3.什么是模板解释器、字节码解释器(前提都是直接读取的.class文件)
  • 字节码解释器:读取字节码,转换为c++代码,再由c++代码转换为硬编码,以此类推。其字节码解释器主要的缺点是执行比较慢(Java字节码->c++代码->硬编码);
  • 模板解释器 (JIT的一部分):刚开始的运行原理和字节码解释器一样,只不过模板解释器里面有个特殊的机制-即时编译(即时编译底层原理):
    1、申请一块内存:可读可写可执行(mac系统不支持,所以说mac系统不支持jit)
    2、将处理new字节码的硬编码拿过来
    3、将处理new字节码的硬编码写入申请的内存
    4、申请一个函数指针,用这个函数指针执行这块内存
    5、调用的时候,直接通过这个函数指针调用就可以了(Java字节码->硬编码)
4.执行引擎执行字节码的3种方式
  • -Xint 纯字节码解释器(1)
  • -Xcomp 纯模板解释器(2)
  • -Xmixed 字节码解释器 + 模板解释器(3)

注: VM默认是混合模式,我们可以执行下java -version查看下,可以通过java -Xint version来设置JVM的运行模式为纯字节码解释器。上面三个中执行模式中性能排比是什么呢,321或者是231,直接影响2和3的性能因素是,程序的大小。如果是大程序的话,可以直接采用混合模式,启动时间较快,编译优化器可以根据热点代码等进行优化。

在这里插入图片描述

5.即时编译器(JIT,即时编译器生成的代码就是给模板解释器用的)

HotSpot虚拟机内置了两个即时编译器,分别称为Client Compiler和Server Compiler,习惯上将前者称为C1,后者称为C2。

  • C1
    • 需要收集的较少
    • 编译优化比较浅
  • C2
    • 触发条件比较严格,一般来说,程序运行了一段时间以后才会触发
    • 优化比较深(优化汇编指令
    • 编译生成的代码执行效率较C1更高
  • 混合编译
    • 程序运行初期触发C1编译器
    • 程序运行一段时间后触发C2编译器
    • Client 编译器模式下,N 默认的值 1500(N表示热点代码的次数)
    • Server 编译器模式下,N 默认的值则是 10000
6.即时编译触发的条件:热点代码(存放在方法区)

在程序运行期间,根据对热点字节码的探测(运行次数超过某个阀值的代码),将这部分热点代码进行特别的优化,将其直接编译为本地机器码执行并缓存。其使用的 定期清理算法是LRU,最近最久未使用算法

7.LRU算法(缓存淘汰算法)

//TODO待补充

8.即时编译器是如何运行的呢?
  • 将即时编译任务(即函数弹出栈的次数)写入一个队列中;
  • VM_THREAD 读取任务,并运行

注:所以即时编译是一个异步的操作

9.基于逃逸分析,JVM开发了三种优化技术
  • 栈上分配:逃逸分析如果是开启的,栈上分配就是存在的(不发生gc的情况下,查看堆上的对象个数,如果是程序中创建的个数,就存在栈上分配)
    • -XX:+DoEscapeAnalysis 开启逃逸分析
    • -XX:-DoEscapeAnalysis 关闭逃逸分析
    • -XX:+PrintEscapeAnalysis 显示逃逸分析结果
  • 标量替换:标量:不可再分,java中的基本类型就是标量
public class ScalarSubstitution {
    public static void main(String[] ars) {
        Point point = new Point();
        System.out.println(point.x); //编译器会替换成System.out.println(0);
        System.out.println(point.y); //同上
    }
}

@Data
class Point {
    public int x;
    public int y;
}

  • 锁消除
public void test(){
    synchronized (new Object()){ //编译器判定这个对象是个局部变量,是线程私有的,所以就没必要加锁,会直接把锁去掉
        System.out.println("zong");
    }
}

3.解释器

1. 为什么需要字节码文件

可以实现跨语言,使其他的语言生成自己码指令,JVM也能执行。

2.解释器作用:

根据程序计数器的指令地址,逐条将字节码指令“翻译”为本地机器指令,以便程序能够运行。

4.即时编译器(JIT)

将整个函数体编译成机器码(当该函数执行次数达到一定次数的时候触发,变为热点代码),每次函数执行时,只编译机器码即可,还可以将常用的机器码进行缓存在方法区(元空间)中,从而提高效率。

5.解释器与即时编译器的优缺点

1.解释器:

优点:程序一开始执行的时候,解释器就能够逐条执行,省去编译的时间。

缺点:相对于即时编译器而言,逐条进行翻译效率较低

2.即时编译器(JIT)

优点:因为是提前编译好的机器指令,因此效率更高;

缺点:程序一开始执行的时候,需要事先对字节码指令进行编译,需要一定的时间。

综上:HotSpot虚拟机采用两者兼容的方式,虚拟机开始启动后,解释器可以立即发挥作用,不需要等到即时编译器编译完后再去执行,节省不必要的编译时间,随着时间的推移,即时编译器编译完成之后,采用即时编译器效率更高。

6.StringTable(字符串)

1.String基本特性
  1. 存储结构的变更
    jdk1.8时采用char[] 进行存储;jdk1.9之后采用byte[] 进行存储;
  2. 不可变特性
    通过字面量的方式给一个字符串赋值,此时的字符串位于堆空间的常量池中,

  1. 字符串常量池中不会存储相同的字符串
    常量池中的字符串均是唯一的,如果两个字符串变量相等,则两个变量指向字符串常量池中的同一个地址。
    String Pool(常量池) 底层是一个固定大小的HashTable,默认长度为1009,可通过-XX:StringTableSize设置
2.String内存分配
  1. jdk1.6时,字符串常量池位于永久代内
  2. jdk1.7之后,字符串常量池位于堆空间;

移动的原因/好处:

永久代几乎不进行来及回收将其移动到堆空间后,更方便进行垃圾回收

3.详解见String笔记

五、垃圾回收(☆)

1.垃圾回收概述

1.什么是垃圾?

没有任何引用指向的一个对象或者多个对象(循环引用)

2.为什么需要GC?
  1. 如果不断进行内存分配而不进行垃圾回收,内存迟早会被消耗完;
  2. GC可以解决内存中的碎片化问题,从而能够为较大的对象分配足够的内存空间;
  3. 随着应用程序的不断完善,用户越来越多,更需要GC来进行性能优化。
3.垃圾回收的主要区域
  1. 方法区:主要回收常量池中废弃的常量(字面量及符号引用)及不再使用的类型
  2. 堆空间:回收垃圾(没有任何引用指向的一个对象或者多个对象(循环引用))

2.垃圾回收相关算法

判断对象存活方式:引用计数算法可达性分析算法
1.标记阶段(表明什么对象需要回收)
1.引用计数算法

原理:每个对象都有一个引用计数器,来记录对象被引用的次数,每增加一次引用计数器+1;每减少一次引用计数器-1,当引用计数器记录值为0时,就表名该对象没有被任何对象引用,则表名该对象为垃圾。

优缺点

优点:实现简单,垃圾对象便于识别;回收效率高;

缺点:出现循环引用的情况

  1. 无法处理循环引用的情况;从而造成内存泄露;
    在这里插入图片描述

  2. 空间:因为需要计数器,所以造成额外的空间开销;

  3. 时间:需要频繁的加1或者减1操作,造成一定的时间开销;

举例:
在这里插入图片描述
在这里插入图片描述

2.可达性分析算法(根搜索算法)

GC Roots:一组活跃引用的根对象集合

基本原理:以根对象为起始点,从上到下搜索被根对象所连接的目标是否可以到达,内存中的存活对象都直接或者间接地与根对象集合相连,搜索所走过的路径被称为引用链,如果目标对象没有被任何引用链相连,则为不可到达,即为内存中的垃圾

优点:可以解决循环引用的问题;

缺点:相较于引用计数算法来说,回收效率稍慢一些。

在这里插入图片描述

3. 可以被当做GC Roots的元素有哪些?(重点是堆外保存堆内对象的地址的那些区域)
  1. 虚拟机栈中引用的对象;如:各个线程中被调用的方法的局部变量等
  2. JNI(本地方法栈中引用的对象)
  3. 方法区(元空间)静态属性引用的对象:如静态变量
  4. 方法区(元空间)中常量引用的对象。如字符串常量池中引用的对象
  5. 所有被synchronized持有的对
  6. 基本数据类型对应的Class对象、异常类对象及系统类加载器
  7. 还需要一些“临时性”对象加入GC Roots结合中,如只针对新生代回收,则堆中新生代以外的引用对象也需要加入GC Roots集合中。
2.标记阶段(表明什么对象需要回收)
1.什么是对象的finalization机制

在gc 回收某个对象之前,会先调用对象的finalize()方法。Object类中的finalize()方法没有任何方法体,对象类可以重写这个方法。可以在对象销毁之前做一些操作。

finalize()的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作.

2.虚拟机中的对象有哪几种可能的状态(生存还是死亡)?
  1. 可触及的:能够与引用连相连接的对象;
  2. 可复活的:没有任何引用的对象,但是可以在finalize()中被复活的对象;
  3. 不可触及的:对象的finalize()被调用,但是没有被复活的对象。**finalize()**方法只能被调用一次。
3. 判断一个对象是否可以被回收所经历的两次标记过程(如何判断一个对象是否可以被回收?)(面试重点)
  1. 如果对象到GC Root没有引用链连接或者引用计数,则进行第一次标记;

  2. 进行筛选,判断是否有必要调用对象的 finalize() 方法

    1. 对象类没有重写finalize()方法或者已经调用过了finalize()方法,则为不可触及的, 直接回收
    2. 对象重写了该方法但是还没调用过该方法,则回收前会先调用此方法;
  3. 执行二次标记:如果执行finalize()方法后,该对象与引用链上的任何一个对象建立连接,则为复活状态,否则判定为不可达状态。

3.清除阶段(如何回收垃圾)
1.标记-清除算法
  1. 原理:当需要进行GC时,会停止整个程序(STW),然后整个GC过程分为标记阶段和清除阶段
    标记:从引用根对象开始遍历,标记所有被引用的对象,一般在对象头中记录为可达对象。
    清除:对堆内存进行从头到尾的遍历,如果发现某个对象的对象头没有被标记为可达对象,则进行回收

在这里插入图片描述

  1. 优点:最基本的垃圾回收算法

  2. 缺点

    1. 因为需要遍历,所以执行效率不高;
    2. 在进行GC时,需要STW,效率不高的话,会影响用户体验;
    3. 产生内存碎片化问题
2.复制算法(适合在新生代)
  1. 原理:将内存空间分为大小相等的两块,每次只使用其中的一块,垃圾收集时,将正在使用的内存中的存活的对象复制到另一块内存中,然后将正在使用的内存清空。交换两个内存的角色,完成垃圾回收。在堆中新生代分为eden,s0,s1,其中s0,s1就是用的复制算法

在这里插入图片描述

  1. 优点

    1. 执行效率高,省去了清除中的遍历问题
    2. 解决了内存碎片化的问题
  2. 缺点

    1. 空间浪费较明显,始终有一块内存无法使用;
    2. 对于G1这种分成很多region的垃圾回收器来说,复制意味着需要维持region之间对象的引用关系,空间和时间的开销比较大。
  3. 适合场景

    1. 比较适合于垃圾对象较多,存活对象较少的区域,如新生代中的Survivor0区和Survivor1区。不适合老年代中垃圾回收。

在这里插入图片描述

3.标记-整理(压缩)
  1. 原理
    1. 第一阶段和标记-清除算法的标记阶段一样,从根结点开始标记所有被引用的对象
    2. 第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,然后清除边界以外的内存空间

在这里插入图片描述

  1. 优点

    1. 解决了标记-清除内存碎片化的问题
    2. 解决了标记-复制空间浪费一半的问题
  2. 缺点

    1. 整理过程中需要移动对象,如果对象被其他对象所引用,则需要不断调整引用的地址
    2. 效率相对另外两种算法较低;
  3. 适用场景:适合于老年代中的垃圾收集

4.三种垃圾回收算法对比
算法标记-清除复制标记-压缩(整理)
执行效率中等最快最慢
空间开销少(存在内存碎片化大(浪费一半空间)小(不会产生内存碎片化
移动对象
5.分代收集理论

目的:不同声明周期的对象可以采用不用的回收算法,以提高整体的回收效率。

HotSpot虚拟机中的回收策略

  1. 新生代

    1. 新生代特点:区域相对老年代小,对象声明周期短,回收频繁
    2. 针对这种情况应当采用复制算法,回收效率高,针对于空间利用率不高的问题,采用两个Survivor区得以缓解;
  2. 老年代

    1. 老年代特点:相对于新生代大,对象的声明周期长,回收不频繁。
    2. 针对这种情况采用标记-清除+标记-整理相结合的方法,首先标记阶段还是采用两者中的标记方法,即从根对象开始,依次标记所有的存活对象,然后采用清除算法,暂时容忍碎片化问题,等到碎片化问题影响到内存分配时,再采用整理算法,整理碎片化内存。
6.增量收集算法(实际就是每次只收集一部分)

如果一次性将所有垃圾进行回收,需要造成系统长时间的停顿,影响用户体验,可以让垃圾收集线程和用户线程交替执行,每次垃圾收集线程只收集一部分的内存空间,接着切换到应用程序,依次反复,直到垃圾收集完成。

优点:低延迟,用户体验更优;

缺点:吞吐量下降

7.分区收集算法

将整个堆空间划分为连续不等的小区间,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间。每一个小区间都独立使用,独立回收。
在这里插入图片描述

3.垃圾回收相关概念

1. System.gc()

会显示触发Full GC,但是无法保证对垃圾收集器的调用

在这里插入图片描述

2.内存溢出与内存泄露
1. 内存溢出(OOM-Out-Of-Memory)

没有空闲内存,并且垃圾收集器也无法提供更多内存。

2.没有空闲内存的原因
  1. 设置的堆内存太小,此时可以通过-Xms:和-Xmx来设置堆空间的起始大小和最大大小;
  2. 代码中创建了大对象,并且长时间不能被垃圾回收器所收集;
3.内存泄露:(非常重要)
  1. 严格意义对象不会被程序用到,但是GC又没有办法回收掉该对象,此时就称为发生了内存泄露
  2. 宽泛意义:实际情况中,一些不好的编程实践造成了对象的声明周期变得很长甚至导致OOM,(如本来可以定义为方法内部的局部变量,定义为类的成员变量甚至定义为静态变量(随着类的加载而加载,随着类的消亡而消亡)造成变量的声明周期加长,本来方法弹栈后可能就会被释放(没有发生逃逸))。

在这里插入图片描述

  1. 举例说明内存泄露(举出关于内存泄露的例子)
    1. 单例的生命周期和程序一样长,单例程序中如果存在对外部对象的引用,则外部对象是没有办法被回收的,会造成内存泄露
    2. 一些提供 close() 的资源未关闭而导致内存泄露(如数据库的连接必须手动close,否则不能被回收)
3.STW

无论那种垃圾收集器,在进行垃圾收集的过程中,都会使程序停顿,这称为STW,被STW中断的程序在完成GC后会自动恢复。

目的:为了保证数据的一致性(不能统计垃圾的时候还一边造垃圾)

4.垃圾回收并行与并发
1.程序的并行与并发
  1. 并发:从一段时间来看,有多个任务在执行,从单一的时间点上来看,只有一个任务在执行,时间上是CPU在快速切换任务交替执行;
  2. 并行:当系统有多个CPU时,一个cpu可以执行一个进程,另一个cpu可以执行另一个进程,两个进程不会互相抢cpu资源,可以同时进行,此称为并行。
  3. 对比并发指的是同一个时间段,多个任务发生了,且抢占cpu资源;并行指的是在用一个时间点,多个任务发生了,不抢占cpu资源
2.垃圾回收的并行与并发
  1. 并行:多条垃圾回收线程并行执行,此时需要停顿用户线程
    在这里插入图片描述

  2. 串行:只有一条垃圾回收线程,当内存不够时,程序暂停,启动垃圾回收,回收完,再启动程序的线程。
    在这里插入图片描述

用户线程与垃圾回收线程同时执行,垃圾回收线程在执行时,不会停顿用户程序的执行。

用户程序在继续进行,而垃圾收集线程运行在另一个CPU,如:CMS和G1。

5.强引用(Strong Reference)
  1. 定义:在java程序中通过new创建了一个对象,并将其赋值给一个变量,该变量就称为指向该对象的一个强引用
  2. 适用场景:99%以上的都是强引用。
  3. 垃圾回收:强引用的对象都是可触及的,GC不会回收掉被强引用的对象(强引用,不回收)。
    在这里插入图片描述
6. 软引用(Soft Reference)
  1. 定义:描述的是一些还在用,但是非必须的对象。
  2. 适用场景:通常用来实现内存敏感的缓存,如高速缓存就用到软引用
  3. 垃圾回收:内存不足即回收
    在这里插入图片描述
7. 弱引用(Weak Reference)
  1. 定义:弱引用也是描述那些非必须的对象,与软引用的区别在于GC时,对于软引用来说需要判断当前内存是否不足,不足的话才进行回收,而对于弱引用不需要进行判断直接回收
  2. 适用场景:保存那些可有可无的缓存数据。
  3. 垃圾回收:发现即回收
    在这里插入图片描述

补充面试题:你用过weakHashMap吗?

采用weakHashMap进行存储后,当内存不足时,能够对该部分进行回收,因为内存的Entry<k,v>类继承了弱引用类。
在这里插入图片描述

8.虚引用(Phantom Reference)
  1. 定义:所有引用中最弱的一个,为一个对象设置虚引用关联的目的是跟踪垃圾回收的过程,被回收后可以发出相应的通知
  2. 适用场景:实现跟踪对象的垃圾回收过程
  3. 垃圾回收:跟踪对象回收过程

4.垃圾回收器

1.垃圾回收主要性能指标
  1. 吞吐量:用户程序运行时间/用户程序运行时间+垃圾回收时间
    1. 吞吐量越高越好,这样会提升用户体验,认为只有应用程序在执行。
  2. 暂停时间:执行垃圾收集时,程序被暂停的时间。
    1. 暂停时间越低越好,对于交互式应用程序,暂停时间越长,越容易出现卡顿现象,影响用户体验。
  3. 现在标准:在最大吞吐量优先的情况下,降低停顿时间。
2.垃圾回收器概述(分类)

串行 (STW时只有一个垃圾回收线程):SerialSerial Old

并行(STW时有多个垃圾回收线程):ParNewParallel ScavengeParallel Old (其中old指的是老年代)

并发(垃圾回收线程和用户线程并发执行):CMSG1

在这里插入图片描述

3. 垃圾回收器的组合关系

新生代Serial, ParNew Parallel Scavenge

老年代Serial Old Parallel OldCMS

新生代和老年代G1

在这里插入图片描述

组合关系

在这里插入图片描述

补充

  1. jdk9中移除了Serial+CMSParNew+Serial Old这两种组合,上图红色虚线部分
  2. jdk14中,删除了CMS垃圾回收器。

目前组合

目前的组合:Serial +Serial OldParallel Scavenge+Parallel OldG1

4. Serial垃圾回收器(串行回收)
1.Serial垃圾回收器

采用复制算法串行收集Stop The World的方式来对新生代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的新生代垃圾回收器

2.Serial Old垃圾回收器

采用标记-整理算法串行收集Stop The World的方式来对老年代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的老年代垃圾回收器

3.回收过程

在这里插入图片描述

4.回收优势

因为是单线程,因此没有线程交换的开销,相对于其他垃圾收集器的单线程相比,简单而高效。

5.参数设置
-XX:useSerialGC/useSerialOldGC设置
5.ParNew垃圾回收器(并行回收)
1.回收模式

采用复制算法并行回收和STW的机制进行垃圾回收,是JVM在服务端下的默认新生代垃圾回收器

2.组合搭配

可以和Serial Old搭配使用(JDK9中移除);可以和CMS配合使用(JDK14中删除)

在这里插入图片描述

3.优势

对于新生代,回收次数频繁,因此采用并行方式更加高效。

4.参数设置
-XX:useParNewGC
6.Parallel垃圾回收器(吞吐量优先)
1.Parallel Scavenge垃圾回收器(新生代)

采用复制算法并行回收STW的机制进行垃圾收集。

2.Parallel Scavenge垃圾回收器与ParNew的区别

Parallel Scavenge的目标是达到一个可控吞吐量,而且具备自适应调节策略。

3.Parallel Old垃圾回收器(老年代)

采用标记-整理并行回收STW的机制进行垃圾收集,在JDK8中,默认Parallel Scavenge+Parallel Old为垃圾回收器。

在这里插入图片描述

4.适用场景

高吞吐量则可以高效的利用cpu的时间,来快速实现计算。主要适合在后台运行而不需要太多交互的任务

7.CMS(Concurrent Mark Sweep)垃圾回收器(低延时,老年代)
1.概念

CMS(Concurrent Mark Sweep)垃圾回收器是HotSpot虚拟机第一款并发垃圾回收器,实现了垃圾收集线程和用户线程同时工作。是基于标记-清除算法,并发回收的老年代垃圾收集器,只搭配ParNew和Serial使用,在jdk14时,CMS垃圾回收器被删除。

2.工作原理

在这里插入图片描述

  1. 初始标记:仅仅只是标记出GC Roots能够直接关联到的对象,存在STW机制(很短暂的STW)。
  2. 并发标记:从GC Root直接关联到的对象开始,遍历整个对象图的过程,该阶段不需要停顿用户线程。
  3. 重新标记:修正并发标记阶段,因用户线程运行而导致标记产生变动的那一部分对象的标记记录,存在STW机制,会产生浮动垃圾
  4. 并发清除:清除标记为已经死亡的对象,释放内存空间。
3.优缺点
  1. 优点:并发收集;低延迟;

  2. 缺点:

    1. 会产生内存碎片,当老年代需要为大对象分配内存时,不得不提前触发Full GC;
    2. 无法处理"浮动垃圾",可能导致"并发失败",从而引发Full GC
    3. 对CPU资源比较敏感:CMS默认的垃圾回收线程数为(处理器核心数+3)/4,对于处理器核心数比较少的情况,垃圾回收线程就占比较大,影响执行速度。

    浮动垃圾:因为在并发清理阶段,垃圾收集线程和用户线程在并发执行,用户线程运行过程中,会产生新的垃圾,而这部分垃圾是发生在标记阶段之后的,所以只能等到下一次GC时,才能够进行回收,这时候需要预留出一定的内存空间,在下图中,a=null后b和c就是垃圾了但是在这一轮垃圾回收中判定不是垃圾,但是会在下次垃圾回收中回收。

在这里插入图片描述

4.使用场景:适用于强交互的应用
5.参数设置
  1. -XX:+UseConcMarkSweepGC:设置使用CMS垃圾收集器;
  2. -XX:CMSinitialingOccupanyFraction:设置堆内存使用率阈值;jdk5,默认为68%;jdk6.默认为92%。
  3. -XX:ParallelCMSThreads:设置CMS的线程数量
6.关于CMS的面试题
  1. 为什么说CMS是一款低延时的垃圾回收器?

因为在初始标记阶段和重新标记阶段,会发生STW,但是此部分时间很短,而在并发标记和并发清除阶段,虽然占据的时间比较长,但是在此期间,垃圾回收线程和用户线程并发执行,因此为低延时垃圾回收器

  1. CMS会产生内存碎片化问题,为什么不用标记整理算法呢?

因为CMS在并发标记和并发清除阶段,用户线程和垃圾回收线程并发执行,而标记整理算法中存在对象在内存中的定向移动,用户线程在执行过程中,如果发生移动,会造成安全问题。因此无法采用标记-整理算法。

8.G1垃圾回收器
1.概述
  1. 目标是在停顿时间可控的情况下尽可能的提高吞吐量的垃圾收集器
  2. 将整个堆内存分为多个region区,跟踪各个Region区的垃圾堆积的价值大小,然后后台维护一个优先级列表每次根据允许的收集时间,优先回收价值大的region.
  3. 基于复制算法标记-整理算法的并行垃圾回收器,为jdk9之后默认的垃圾回收器
2.回收过程(整体)

整体分为年轻代GC(Young GC), 并发标记老年代混合回收过程,如果G1失效,Full GC作为后备机制

在这里插入图片描述

  1. 当伊甸园区满之后,开始进行年轻代回收,移动存活对象到Survivor区或者Old区;
  2. 当堆内存到达一定阈值(默认45%)时,启动并发标记老年代;
  3. 并发标记结束后,进行混合回收,收集老年代时,一次只需回收一部分老年代对象,(因为存在时间限制)
3.优缺点
1.优点
  1. 并发与并行

并行性:在G1垃圾回收期间,可以有多条垃圾回收线程同时进行回收,有效利用多核计算能力。

并发性:在G1并发标记阶段,允许垃圾收集线程和用户线程并发执行。

  1. 分代收集

G1同时兼顾了年轻代和老年代。因为G1垃圾收集器将堆空间分为不同的Region区,这些区域包含了逻辑上的新生代和老年代。

  1. 不存在内存碎片化问题

因为G1垃圾回收器以Region为基本内存回收单元,Region之间采用复制算法,但是从整体上来说,是采用标记-整理算法,因此可以避免内存碎片问题。

  1. 可预测的停顿时间模型(软实时)

使使用者明确在一个长度为M的时间片段内,用于垃圾回收的时间不超过N

主要是因为G1跟踪各个Region的垃圾堆积价值的大小,然后后台维护一个优先级列表,在有限的时间内,优先收集价值大的Region

2.缺点(不适合小内存应用)

由于垃圾收集产生的内存占用相对大,小内存应用上性能不如CMS。

4.面向服务端的垃圾收集器,主要针对配备多核CPU和大容量内存的机器(低延时,大内存)。
5.Region相关介绍
  1. 大小:G1将整个堆大小分为约2048个Region,每个Region大小相同,且为2的n次幂(一般在1M-32M之间),且在JVM的生命周期内不会改变。
  2. 分布:一个Region有可能属于伊甸园区,S区或者Old区。G1还在堆内存中存放了一个Humogous区,简称H区,当对象大小超过1.5倍的Region区时,将其称为大对象,放在H区。(问题:如果是1.2倍Region大的对象放在哪儿?)

在这里插入图片描述

  1. 设置H区的原因?(防止大对象直接进入老年代)

原因在于,如果不设置H区,那么大的对象则会直接进入到老年代,但是如果这个对象的声明周期比较短,则会长时间存在,并且占据着较大内存,所以将其存放在H区,如果对象大于一个H区大小,则会存在在连续的H区中。如果整个H区都装不下,则会触发Full GC.

  1. 内存分配

Region区采用指针碰撞的方式进行内存分配,并且每个Region内部存在TLAB(线程本地分配缓冲区)。

在这里插入图片描述

6.Remembered Set(记忆集)
  1. 存在必要性:因为存在跨Region引用的存在,判断存活时,如果挨个遍历每个Region的话,势必造成效率降低,于是引进了记忆集。
  2. 原理:每个Region都有一个记忆集,当进行引用数据写入时,先判断是否存在其他Region的引用关系,如果存在的话,则将该引用关系对应的对象写入记忆集的卡表(CardTable)中,然后在GC过程中将Rset加入到GC Roots中
7.回收过程(具体)

在这里插入图片描述

  1. 年轻代GC(与之前讲的一样,只不过是基于分区思想)

JVM优先分配对象到伊甸园区,当伊甸园区满了之后,开始进行年轻代GC,然后,存活下来的对象进入到S区,当S区的对象分代年龄达到阈值后,进入到老年代中,清理过程中的算法采用复制算法。

在这里插入图片描述

  1. 并发标记老年代(深入理解java虚拟机中此部分为G1的收集过程)

    1. 初始标记:只标记GC Roots直接关联到的对象,此过程需要STW(短暂STW)
    2. 并发标记:从GC Root关联到的对象开始,递归遍历整个对象图,此时垃圾收集线程和用户线程并发执行
    3. 最终标记(类似于CMS重新标记):由于在并发标记阶段,用户线程在执行,需要对标记结果进行修正,此时需要STW.
    4. 筛选回收:根据用户设置停顿时间,采用复制算法进行清理(将决定回收的Region中的存活对象复制到空的Region中,然后清空旧的Region空间),此过程需要STW

在这里插入图片描述

  1. 混合回收
    //TODO
8.垃圾回收器总结

在这里插入图片描述

9.如何选择垃圾回收器
  1. 优先调整堆的大小,让JVM能够适应;
  2. 如果是内存比较小,选择Serial +Serial Old垃圾回收器;
  3. 如果是单核,没有停顿时间要求,选择Serial +Serial Old垃圾回收器;
  4. 如果是多核CPU,需要高吞吐量,并且停顿时间不长,选择Parallel+Parallel Old垃圾回收器;
  5. 如果是多核CPU,低停顿时间
  6. 如果是jdk14以前,则可以选用ParNew+CMS或者G1;jdk14后,选择G1;

六、补充面试题总结

1.JVM调优

1.调优参数
  • -Xms:设置初始堆大小;
  • -Xmx:设置最大堆大小;
2.设置新生代和老年代的比例

-XX:NewRatio:设置年轻代和老年代的比例;

3.设置伊甸园区和幸存者区的比例

-XX:SurvivorRatio=n;

4.设置永久代/元空间的大小
  • -XX:permSize;-XX:permMax;
  • -XX:metaSpaceSize;-XX:metaSpaceMax;
5.设置垃圾回收器
  • -XX:useG1GC;
  • -XX:useCMSGC;
6.打印相关日志
  • -XX:+printGC:打印垃圾回收
  • -XX:+printGCDetails:打印垃圾回收细节信息;
2.调优目的

减少GC的频次和Full GC的次数

3.调优过程
  1. 首选需要监控GC的状态,查看当前的堆内存快照及gc日志,根据实际的各区域划分和GC执行时间,判断是否需要优化。
  2. 生成堆的dump文件
  3. 分析dump文件
  4. 可以通过eclipse的工具Memory Analyzer
  5. 分析结果,判断是否需要优化
  6. 调整GC的类型和内存分配
  7. -XX:use+垃圾回收器+GC;
  8. 不断分析和调整参数

2.OOM及其解决?

OOM场景有哪些?
1. java堆内存溢出

原因:堆内存设置小或者内存泄露问题

解决:对于内存泄露的话,可以使用内存监控查找程序中的内存泄露的地方 加以更正;

对于堆内存设置问题,可以通过-Xms或者-Xmx来进行设置;

2.方法区溢出

原因:出现大量的Class或者jsp页面,过多的常量等会导致方法区溢出;

解决:可以通过设置方法区的大小来调节

-XX:Permsize;-XX:MaxPermSize设置永久代的大小;

-XX:metaSpaceSize;-XX:MaxmetaSpace设置元空间的大小;

OOM的解决方法
  1. 首先采用内存映像分析工具如(MAT)对dump出来的堆转储快照进行分析,确认内存中的对象是否是必要的,查明到底是内存泄露(GC无法回收的对象,如未在close()方法释放的连接,以及单列模式下存在外部引用)还是内存溢出.
  2. 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,准确定位到泄漏代码的位置;
  3. 如果不存在泄漏的话,分析是堆内存溢出还是方法区溢出;
  4. 如果是堆内存溢出,则适当调整堆的大小:通过-Xms或者-Xmx来进行设置;
  5. 如果是方法区溢出,适当调整方法区的大小:通过-XX:Permsize;-XX:MaxPermSize设置永久代的大小;-XX:metaSpaceSize;-XX:MaxmetaSpace设置元空间的大小

3.jdk1.7和jdk1.8比较?

1.字符串放在什么位置?

均放在堆空间的字符串常量池中;

2.默认的垃圾回收器?

是一样的,都是Parallel scavenge+parallel old垃圾回收器。jdk1.9之后是G1垃圾回收器。

4.Full GC与Minor GC

1.Full GC触发条件

Full GC定义:完整收集整个堆和方法区的垃圾

  1. 老年代内存被占满
  2. 永久代/元空间内存被占满;
  3. System.gc()时会显式调用Full GC
  4. 空间担保失败(当老年代的剩余空间小于等于S区的平均对象大小时,表示空间担保失败)
2.Minor GC触发条件

Minor GC定义:只发生在新生代的垃圾回收

当新生代的伊甸园区满了之后,就会触发Minor GC,回收整个年轻代的垃圾

Minor GC出发结果:当进行Minor GC时,会产生STW,存活的对象移动到幸存者区

5.Full GC与Minor GC

1.CMS和G1垃圾回收器比较
1.CMS垃圾回收器(主打低延时)
1.介绍

是hotspot虚拟机第一款并发的垃圾回收器,垃圾回收线程和用户线程可以并发执行,不需要STW,因此能够提升用户体验,降低延迟性,适合于频繁交互的场景。

2.执行过程

在这里插入图片描述

  1. 初始标记:仅仅只是标记出GC Roots能够直接关联到的对象,存在STW机制(很短暂的STW)。
  2. 并发标记:从GC Root直接关联到的对象开始,遍历整个对象图的过程,该阶段不需要停顿用户线程。
  3. 重新标记:修正并发标记阶段,因用户线程运行而导致标记产生变动的那一部分对象的标记记录,存在STW机制,会产生浮动垃圾
  4. 并发清除:清除标记为已经死亡的对象,释放内存空间。
3.优缺点
  1. 优点:并发收集;低延迟;

  2. 缺点:

    1. 会产生内存碎片,当老年代需要为大对象分配内存时,不得不提前触发Full GC;
    2. 无法处理"浮动垃圾",可能导致"并发失败",从而引发Full GC
    3. 对CPU资源比较敏感:CMS默认的垃圾回收线程数为(处理器核心数+3)/4,对于处理器核心数比较少的情况,垃圾回收线程就占比较大,影响执行速度。

    浮动垃圾:因为在并发清理阶段,垃圾收集线程和用户线程在并发执行,用户线程运行过程中,会产生新的垃圾,而这部分垃圾是发生在标记阶段之后的,所以只能等到下一次GC时,才能够进行回收,这时候需要预留出一定的内存空间,在下图中,a=null后b和c就是垃圾了但是在这一轮垃圾回收中判定不是垃圾,但是会在下次垃圾回收中回收。

在这里插入图片描述

4.使用场景

主打低延时,用于频繁交互的场景

2.G1垃圾回收器
1.概述
  1. 目标是在停顿时间可控的情况下,尽可能提高吞吐量的垃圾回收器
  2. 将整个堆内存分为多个region区域,跟踪每个Region区的垃圾堆积的价值大小,然后后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值大的region
  3. 基于复制算法和标记整理的算法的并行垃圾回收器,为jdk1.9默认的垃圾回收器
2.G1的回收过程
  1. 初始标记:只标记GC Roots直接关联到的对象,此过程需要STW(短暂STW)
  2. 并发标记:从GC Root关联到的对象开始,递归遍历整个对象图,此时垃圾收集线程和用户线程并发执行
  3. 最终标记(类似于CMS重新标记):由于在并发标记阶段,用户线程在执行,需要对标记结果进行修正,此时需要STW.
  4. 筛选回收:根据用户设置停顿时间,采用复制算法进行清理(将决定回收的Region中的存活对象复制到空的Region中,然后清空旧的Region空间),此过程需要STW

在这里插入图片描述

3.优缺点

优点:

  1. 并发与并行

并行性:在G1垃圾回收期间,可以有多条垃圾回收线程同时进行回收,有效利用多核计算能力。

并发性:在G1并发标记阶段,允许垃圾收集线程和用户线程并发执行。

  1. 分代收集

G1同时兼顾了年轻代和老年代。因为G1垃圾收集器将堆空间分为不同的Region区,这些区域包含了逻辑上的新生代和老年代。

  1. 不存在内存碎片化问题

因为G1垃圾回收器以Region为基本内存回收单元,Region之间采用复制算法,但是从整体上来说,是采用标记-整理算法,因此可以避免内存碎片问题。

  1. 可预测的停顿时间模型(软实时)

使使用者明确在一个长度为M的时间片段内,用于垃圾回收的时间不超过N

主要是因为G1跟踪各个Region的垃圾堆积价值的大小,然后后台维护一个优先级列表,在有限的时间内,优先收集价值大的Region

缺点(不适合小内存应用)

由于垃圾收集产生的内存占用相对大,小内存应用上性能不如CMS。

3.应用场景

面向服务端的垃圾收集器,适用于配备多核CPU和大容量内存的机器。

2.其他垃圾回收器
1.serial及serial old垃圾回收器(串行回收)
1.Serial垃圾回收器

采用复制算法串行收集Stop The World的方式来对新生代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的新生代垃圾回收器

2.Serial Old垃圾回收器

采用标记-整理算法串行收集Stop The World的方式来对老年代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的老年代垃圾回收器

3.回收过程

在这里插入图片描述

4.回收优势

因为是单线程,因此没有线程交换的开销,相对于其他垃圾收集器的单线程相比,简单而高效。

5.参数设置
-XX:useSerialGC/useSerialOldGC设置
2.ParNew垃圾回收器(并行回收)
1.回收模式

采用复制算法并行回收和STW的机制进行垃圾回收,是JVM在服务端下的默认新生代垃圾回收器

2.组合搭配

可以和Serial Old搭配使用(JDK9中移除);可以和CMS配合使用(JDK14中删除)

在这里插入图片描述

3.优势

对于新生代,回收次数频繁,因此采用并行方式更加高效。

4.参数设置
-XX:useParNewGC
3.Parallel垃圾回收器(吞吐量优先)
1.Parallel Scavenge垃圾回收器(新生代)

采用复制算法并行回收STW的机制进行垃圾收集。

2.Parallel Scavenge垃圾回收器与ParNew的区别

Parallel Scavenge的目标是达到一个可控吞吐量,而且具备自适应调节策略。

3.Parallel Old垃圾回收器(老年代)

采用标记-整理并行回收STW的机制进行垃圾收集,在JDK8中,默认Parallel Scavenge+Parallel Old为垃圾回收器。

在这里插入图片描述

4.适用场景

高吞吐量则可以高效的利用cpu的时间,来快速实现计算。主要适合在后台运行而不需要太多交互的任务

3.为什么要分代?

因为java对象的生命周期是不同的,大部分对象的声明周期是比较短暂的,少部分对象的声明周期是比较长的,甚至是伴随着JVM的消亡而消亡,通过分为新生代和老年代两种内存区域,将声明周期短暂的对象放在新生代,将声明周期较长的对象放在老年代,在进行垃圾回收的时候,能够提高效率;如果不采用分代的话,在进行垃圾回收的时候,需要遍历整个堆空间,造成STW时间过长,影响用户体验

6.Full GC与Minor GC

1. 类加载子系统

详见第二章

2.运行时数据区
1.程序计数器

作用:记录虚拟机字节码指令的下一条地址,当一个线程再次获得CPU执行权的时候,能够保证下一条执行从哪儿继续;

2.虚拟机栈
1.作用

主管java程序的运行,虚拟机栈中存储着栈帧,对应着一个一个执行的方法,当方法执行的时候,会进行压栈操作,方法执行完后进行弹栈。

2.组成
  1. 局部变量表
    1. 存储方法的参数及局部变量,如果是基本数据类型,则会存储变量值,如果是引用数据类型的话,则存储的是引用地址值。
  2. 操作数栈
    1. 临时存储变量,作为变量计算的中间结果的临时存储区域
  3. 动态链接
    1. 将虚拟机栈中的符号引用转为方法区中的方法的直接引用;
  4. 方法返回地址
    1. 存储方法的返回地址,方便调用者能够获取到方法的返回值。
3.与本地方法栈的区别

虚拟机栈负责java程序的执行;本地方法栈作用类似于虚拟机栈,负责本地方法的执行,本地方法指的的用C/C++编写的程序,作为方法的扩展库;hotSpot虚拟机将两者合二为一。

3.堆

作用:绝大多数的创建的对象将会存储在堆中;jdk1.7之后静态变量及字符串常量存储在堆空间中

4.方法区

作用:存储类信息,域信息,方法信息,常量,即时编译器的代码缓存也就是热代码(JIT),jdk1.6及之前存储静态变量。

5.本地方法栈

作用:负责本地方法的运行,本地方法指的是用C/C++编写的程序,作为方法的扩展库

3.执行引擎

详情见执行引擎

本地方法接口/本地方法库

7.JVM类加载过程

  1. 加载
    1. 通过一个类的全限定名来获取此类的二进制字节流;
    2. 将该二进制字节流所代表的的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的数据访问入口。
  2. 连接
    1. 验证:确保Class文件的字节流(二进制)中包含的信息符合规范,保证这些信息被当做代码运行后不会对虚拟机造成危害。分为:文件格式验证元数据验证字节码验证符号引用验证四个方面。
    2. 准备:正式为类的静态变量分配内存并设置初始值(隐式初始化->相当于赋默认值)。
    3. 解析:Java虚拟机将常量池中的符号引用替换为直接引用。分为:类或接口的解析、字段解析及方法解析、接口方法解析
  3. 初始化:在准备阶段,静态变量已经赋过一次系统要求的初始零值,而在初始化阶段,会根据程序员通过程序编码指定的主观计划去初始化静态变量和其他资源。

8.JVM类加载过程

类加载器在加载类时采用的 loadClass() 方法使用了Synchronized关键字进行修饰。

9.双亲委派机制

1.定义

当一个类需要加载时,负责加载此类的加载器不会立即对其进行加载,而是递归的委托给其父类加载,父类收到这个加载任务后也会向上进行委托,直到引导类加载器,如果引导类加载器能够加载此类,则会进行加载,如果无法实现加载此类,则会返回给其子类进行加载。

2.好处
  1. 这样做可以有效地防止类被重复加载
  2. 能够保护java的核心类库,放置核心的API被随意篡改
3.各个类加载器加载的类的文件名
1.引导类加载器

java的核心类库都是使用引导类加载器加载的,加载的文件存放在<JAVA_HOME> /lib文件夹中;

2.扩展类加载器

扩展类加载器负责加载<JAVA_HOME>/lib/ext目录中的jar文件

3.应用程序类加载器

应用程序类加载器加载的是用户类路径上的类库

10.垃圾回收算法

1.标记-清除算法
  1. 原理:当需要进行GC时,会停止整个程序(STW),然后整个GC过程分为标记阶段和清除阶段
    标记:从引用根对象开始遍历,标记所有被引用的对象,一般在对象头中记录为可达对象。
    清除:对堆内存进行从头到尾的遍历,如果发现某个对象的对象头没有被标记为可达对象,则进行回收

在这里插入图片描述

  1. 优点:最基本的垃圾回收算法

  2. 缺点

    1. 因为需要遍历,所以执行效率不高;
    2. 在进行GC时,需要STW,效率不高的话,会影响用户体验;
    3. 产生内存碎片化问题
2.复制算法(适合在新生代)
  1. 原理:将内存空间分为大小相等的两块,每次只使用其中的一块,垃圾收集时,将正在使用的内存中的存活的对象复制到另一块内存中,然后将正在使用的内存清空。交换两个内存的角色,完成垃圾回收。在堆中新生代分为eden,s0,s1,其中s0,s1就是用的复制算法

在这里插入图片描述

  1. 优点

    1. 执行效率高,省去了清除中的遍历问题
    2. 解决了内存碎片化的问题
  2. 缺点

    1. 空间浪费较明显,始终有一块内存无法使用;
    2. 对于G1这种分成很多region的垃圾回收器来说,复制意味着需要维持region之间对象的引用关系,空间和时间的开销比较大。
  3. 适合场景

    1. 比较适合于垃圾对象较多,存活对象较少的区域,如新生代中的Survivor0区和Survivor1区。不适合老年代中垃圾回收。

在这里插入图片描述

3.标记-整理(压缩)
  1. 原理
    1. 第一阶段和标记-清除算法的标记阶段一样,从根结点开始标记所有被引用的对象
    2. 第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,然后清除边界以外的内存空间

在这里插入图片描述

  1. 优点

    1. 解决了标记-清除内存碎片化的问题
    2. 解决了标记-复制空间浪费一半的问题
  2. 缺点

    1. 整理过程中需要移动对象,如果对象被其他对象所引用,则需要不断调整引用的地址
    2. 效率相对另外两种算法较低;
  3. 适用场景:适合于老年代中的垃圾收集

4.三种垃圾回收算法对比
算法标记-清除复制标记-压缩(整理)
执行效率中等最快最慢
空间开销少(存在内存碎片化大(浪费一半空间)小(不会产生内存碎片化
移动对象

11.如何选择垃圾回收器

  1. 优先调整堆的大小,让jvm能够适应
  2. 如果是内存比较小,选择serial+serial Old垃圾回收器
  3. 如果是单核,没有停顿时间要求,选择serial+serial Old垃圾回收器
  4. 如果是多核CPU,需要高的吞吐量,选择parallel scanvenge+parallel Old垃圾回收器;
  5. 如果是多核cpu,低停顿时间
  6. 如果是jdk14以前,可以选用CMS+ParNew;jdk14后,G1垃圾回收器

12.对象的内存分配策略

  1. 优先分配到新生代(先分配到Eden区,再经过GC回收后进入S区)
  2. 大对象直接分配在老年代
  3. 长期存活的对象(新生代中的S区的对象年龄达到了设定的阈值,jdk1.8经过15此垃圾回收进入老年代,G1则经过6此进入老年代)分配到老年代
  4. 动态判断对象年龄:如果相同年龄的对象所占的空间大于等于S区的一半,则大于等于该年龄的对象则会被分配到老年代中。

13.垃圾判定方法

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值