四、Java 之 JVMGC

文章目录

    • 1 JVM
        • 1.0 Java 内存模型 以及 各部分作用
          • 1.0.1 程序计数器
          • 1.0.2 Java虚拟机栈
          • 1.0.3 本地方法栈
          • 1.0.4 堆
          • 1.0.5 方法区
          • 1.0.6 运行时常量池
        • 1.1 谈谈你对Java的理解
        • 1.2 平台无关性如何实现
        • 1.3 什么是Java虚拟机
        • 1.4 JVM如何加载.class文件
        • 1.5 Java 反射
        • 1.6 谈谈ClassLoader
        • 1.7 谈谈类加载器的双亲委派机制
        • 1.8 类的加载方式
        • 1.9 loadClass 和 forName的区别
        • 1.10 Java内存模型——线程
        • 1.11 Java内存模型——元空间与永久代的区别
        • 1.12 Java内存模型——Java堆(Heap)
        • 1.13 Java内存模型——Java栈(Stack)
        • 1.14 Java内存模型——JVM三大性能调优参数 -Xms, -Xmx, -Xss的含义
        • 1.15 Java内存模型——堆和栈的区别
        • 1.16 面试官可能会问一些不同版本的JDK之间的区别
        • 1.17 对象的访问定位
        • 1.8 类文件结构
    • 2 GC
        • 2.1 垃圾回收之标记算法
        • 2.2 谈谈你了解的垃圾回收算法
        • 2.3 分代收集算法
        • 2.4 常见的垃圾收集器 ( CMS、G1)
        • 2.5 JDK 监控和故障处理工具总结
        • 2.6 CMS 什么情况会触发 Full GC?
        • 面试题1、Object的 finalize() 方法的作用是否与 C++的析构函数作用相同?
        • 面试题2、Java中的强引用,软引用,弱引用,虚引用有什么用
        • 面试题3、如何判断一个常量是废弃常量?
        • 面试题4、如何判断一个类是无用的类?

Github地址

CSDN地址

1 JVM

1.0 Java 内存模型 以及 各部分作用

在这里插入图片描述

1.0.1 程序计数器
  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.0.2 Java虚拟机栈

Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
1.0.3 本地方法栈

本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
1.0.4 堆

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

JDK 8以前分为:年轻代 + 老年代 + 永久代
JDK 8以后分为:年轻代 + 老年代 + 元空间

元空间替代永久代的好处

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难易确定,给永久代的大小指定带来困难.(太小永久代溢出,太大老年代溢出)
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  • 方便HotSpot与其他JVM(如Jrockit)的集成
1.0.5 方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

1.0.6 运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)

JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

  • JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  • JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
  • JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

1.1 谈谈你对Java的理解

可以从以下几个方面展开

  • 平台无关: 即编译一次,可以运行在任何平台上
  • GC(垃圾回收机制)
  • 语言特性: 泛型,反射,lambda表达式
  • 面向对象: 封装,继承,多态
  • 类库: 数据库,IO
  • 异常处理

1.2 平台无关性如何实现

答: 使用javac命令将.java文件编译成JVM可识别的字节码文件, 在你所要运行的平台上,安装上对应JVM,java命令就可以运行这个字节码文件.也就是一次编译,任何地方可运行.
总结:Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令.

扩展:
javap 命令可反编译字节码文件
②为什么要编译成字节码,为什么不直接编译成机器指令呢?
答: 如果直接编译成机器指令,源码更换平台后,需要因为不同操作系统重新编译机器指令,重新检查语法,语义等重复性操作.所以-

1.3 什么是Java虚拟机

答: JVM(Java Virtual Machine),是一种抽象化的计算机机,通过在实际的计算机上仿真模拟计算机的各种功能来实现的,有自己完整硬件架构,如处理器,堆栈,寄存器和相应的指令系统.JVM屏蔽了平台操作系统的相关信息,使得java程序只需生成可以在JVM上运行的目标代码,即字节码,就可不加修改的在多种平台上运行.

注: JVM是一个内存中虚拟机

1.4 JVM如何加载.class文件

在这里插入图片描述

  1. 加载
  • 通过全类名获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
  1. 链接
    在这里插入图片描述
  2. 准备阶段
    正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
  3. 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  4. 初始化
    是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法 ()方法的过程。
  • 仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 设置的初始值"通常情况"下是数据类型默认的零值
  1. 卸载
    需要满足3个要求:
  • 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  • 该类没有在其他任何地方被引用
  • 该类的实例已被GC

扩展:如何初始化一个对象
在这里插入图片描述
图片详细讲解

1.5 Java 反射

  • 定义:Java反射机制是在运行状态中.①对于任意一个类,都能够知道这个类的所有属性和方法;②对于任意一个对象,都能够调用它的任意方法和属性;③这种动态获取信息以及动态调用对象方法的功能 称为java语言的反射机制.
  • 举个栗子
    被测试类中定义了:私有变量,公有方法,私有方法
public class Robot {private String name;public void sayHi(String helloSentence){System.out.println(helloSentence+" "+name);}private String throwHello(String tag){return "Hello " + tag;}
}

测试类中使用反射,访问私有变量,公有方法,私有方法

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class ReflectSample {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {Class rc = Class.forName("com.interview.javabasic.reflect.Robot");Robot r = (Robot) rc.newInstance();System.out.println("Class name is "+rc.getName());//获取类中私有方法Method getHello = rc.getDeclaredMethod("throwHello", String.class);getHello.setAccessible(true);Object str = getHello.invoke(r, "Bob");System.out.println("getHello result is "+ str);//获取类中共有方法Method sayHi = rc.getMethod("sayHi", String.class);sayHi.invoke(r, "Welcome");//获取类中私有属性Field name = rc.getDeclaredField("name");name.setAccessible(true);name.set(r, "Alic");sayHi.invoke(r, "Welcome");}
}

输出结果

Class name is com.interview.javabasic.reflect.Robot
getHello result is Hello Bob
Welcome null
Welcome Alic

上面两个类,简单演示了反射中的一些函数。
详细的反射类讲解参考这个Java 反射由浅入深;反射的作用及应用场景,优缺点

1.6 谈谈ClassLoader

  • 介绍
    ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流,它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作.

  • ClassLoader种类

名称 说明 备注
BootStrapClassLoader 用户不可见,C/C++编写,加载核心库java.* (可查看源码ClassLoader.java第1019行,为native类)–>若真想看BootstrapClassLoader源码需要去openJDK查看
ExtClassLoader 用户可见,Java编写,加载扩展库javax.*
AppClassLoader 用户可见,Java编写,加载程序所在目录
自定义ClassLoader Java编写,定制化加载
  • 自定义ClassLoader实现
    ①新建一个被测试类
public Class Hello(){static{System.out.println("This is Hello");}    
}

②实现一个简单的ClassLoader,重写比较重要的findClass方法和实现loadClassData加载类数据方法,二进制方式加载

import java.io.*;
public class MyClassLoader extends ClassLoader{private String path;private String classLoaderName;public MyClassLoader(String path, String classLoaderName) {this.path = path;this.classLoaderName = classLoaderName;}//用于寻找类文件@Overridepublic Class findClass(String name){byte[] b = loadClassData(name);return defineClass(name, b, 0, b.length);}//用于加载类文件private byte[] loadClassData(String name) {name = path+name+".class";InputStream in = null;ByteArrayOutputStream out = null;try{in = new FileInputStream(new File(name));out = new ByteArrayOutputStream();int i = 0;while((i=in.read())!= -1){out.write(i);}} catch (Exception e) {e.printStackTrace();} finally {try {out.close();in.close();} catch (Exception e) {e.printStackTrace();}}return out.toByteArray();}
}

③测试自定义加载器

public class ClassLoaderChecker {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {MyClassLoader m = new MyClassLoader("E:\\workspace\\java\\","myClassLoader");Class c = m.loadClass("Hello");System.out.println(c.getClassLoader());c.newInstance();}
}

④打印结果

com.interview.javabasic.reflect.MyClassLoader@677327b6
This is Hello

1.7 谈谈类加载器的双亲委派机制

从1.6我们知道不同的加载器加载不同的类,它们各司其职,逻辑清楚.使它们互相协作看起来像一个整体的机制就是双亲委派机制.

原理图:
加载器原理图
上面图可以对应得源码位于ClassLoader.java第401行,如下所示

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 这一行就是图左边,一直向上寻找c = parent.loadClass(name, false);} else {// 到了最顶层,就要进行图右边c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();// 如果还有找到,使用自定义的findClass方法寻找c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

检1.6的MyClassLoaderCheker中添加

System.out.println(c.getClassLoader().getParent());//预测打印结果为AppClassLoader
System.out.println(c.getClassLoader().getParent().getParent());//预测打印结果为ExtensionClassLoader
System.out.println(c.getClassLoader().getParent().getParent().getParent());//预测打印结果为null, 不是BootstrapClassLoader(不可见)

打印结果不出所料

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7f31245a
null

总结: 为什么要使用双亲委派机制去加载类->从上面可以知道主要就是为了避免同样字节码被重复加载。也保证了 Java 的核心 API 不被篡改。

1.8 类的加载方式

①隐式加载: new
②显示加载:loadClass, forName等

1.9 loadClass 和 forName的区别

我们首先需要知道类的装载过程:①加载②链接③初始化

  • ①加载:负责通过ClassLoader加载class文件字节码,生成Class对象
  • ②链接:负责校验(检查加载的class的正确性和安全性);准备(为类变量分配存储空间并设置类变量初始值);解析(JVM将常量池内的 符号引用 转换为 直接引用).
  • ③初始化;执行类变量赋值和静态代码块

查看源码得到:
Class.forName得到的class是已经初始化完成的(Class源码的433行)
Classloader.loadClass得到的class是还没有链接的(ClassLoader源码的264行)
可以写一个类,里面包含一个静态代码块,分别用获取forName,CLassLoader,看是否能打印出静态代码块中的内容

两者的作用又有什么区别呢:
最常见的就是引入mysql的jar包,我们需要Class.forName("com.mysql.jdbc.Driver"),查看这个Driver类,里面就是一个静态代码块,所以我们需要使用Class.forName来加载数据库驱动
在SpringIOC中,资源加载器要获取所需要的资源时,即读取bean配置,如果使用classpath的方式,就会调用ClassLoader.loadClass,会进行延时加载,以便提高加载速度(因为这不会有类初始化,类链接这样的步骤).

1.10 Java内存模型——线程

还是前面的这张图JVM构成
Java运行时数据区域
Java运行时数据区域
线程私有:程序计数器,虚拟堆栈,本地方法栈
在这里插入图片描述
程序计数器:

  • 当前线程所执行的字节码行号指示器(逻辑)
  • 改变计数器的值来选取下一条需要执行的字节码指令
  • 和线程是一对一的关系即"线程私有"
  • 对Java方法计数,如果是Native方法则计数器值为Undefined
  • 不会发生内存泄漏

Java虚拟机栈(Stack):
Java虚拟机栈

  • Java方法执行的内存模型
  • 包含多个栈帧
  • 局部变量表:包含方法执行过程中所有变量
  • 操作数栈:入栈,出栈,复制,交换,产生消费变量
  • 举个例子分析一下它们之间的关系
public class ByteCodeSample {public static int add(int a, int b){int c = 0;c = a + b;return c;}
}

//使用命令 javac编译得到class文件
//再使用javap -verbose ByteCodeSample.class文件进行反编译,得到下面的信息
//只关注,最后的add方法

  public static int add(int, int);descriptor: (II)Iflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=20: iconst_01: istore_22: iload_03: iload_14: iadd5: istore_26: iload_27: ireturnLineNumberTable:line 5: 0line 6: 2line 7: 6
}
SourceFile: "ByteCodeSample.java"

下面这张图是add方法具体操作过程,从左至右,基本都是栈的出栈,入栈操作在这里插入图片描述
线程共享:MetaSpace(元空间),Java堆
在这里插入图片描述

1.11 Java内存模型——元空间与永久代的区别

JDK1.8之后
元空间使用本地内存,而永久代使用的是JVM的内存

MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难易确定,给永久代的大小指定带来困难.(太小永久代溢出,太大老年代溢出)
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  • 方便HotSpot与其他JVM(如Jrockit)的集成

1.12 Java内存模型——Java堆(Heap)

  • ①对象实例的分配区域
    Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例 。

  • ②GC管理的主要区域
    由于现在收集器基本采用分代回收算法,所以Java堆还可细分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。

  • ③动态扩展
    在JVM中占据大量空间,其本身可以不连续,但逻辑上必须连续.在实现上,既可以实现固定大小的,也可以是扩展的。

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

1.13 Java内存模型——Java栈(Stack)

在栈内存中保存的是堆内存空间的访问地址,或者说栈中的变量指向堆内存中的变量(Java中的指针)。
Java栈是Java方法执行的内存模型每个方法在执行的同时都会创建一个栈帧的用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机中入栈和出栈的过程.

1.14 Java内存模型——JVM三大性能调优参数 -Xms, -Xmx, -Xss的含义

-Xss:规定了每个线程虚拟机栈(堆栈)的大小
-Xms:堆的初始值
-Xmx:堆能达到的最大值

1.15 Java内存模型——堆和栈的区别

  • 内存分配策略
    ①静态存储:编译时确定每个数据目标在运行时的存储空间需求
    ②栈式存储:数据区需求在编译时未知,运行时模块入口前确定
    ③堆式存储:编译时或运行时模块入口都无法确定,动态分配

  • 堆和栈之间的联系
    引用对象、数组时,栈里定义变量用以保存堆中目标的首地址.如下图所示
    在这里插入图片描述
    总结以上得到堆和栈的区别
    ①管理方式: 栈自动释放,堆需要GC
    ②空间大小:栈比堆小,堆相对比较大
    ③碎片相关:栈产生的碎片远小于堆
    ④分配方式:栈支持静态和动态分配,而堆仅支持动态分配
    ⑤效率:栈的效率比堆高

举的例子: 从内存角度看元空间,堆,线程独占部分之间的联系

public class HelloWorld(){private String name;public void sayHello(){System.out.println("Hello " + name);}public void setName(String name){this.name = name;}public static void main(String[] args){int a = 1;HelloWorld hw = new HelloWorld();hw.setName("test");hw.sayHello();}
}

在这里插入图片描述

1.16 面试官可能会问一些不同版本的JDK之间的区别

例如字符串中intern()方法

String s = new String("abc");
s.intern()
版本 区别
JDK6 当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用.否则,将字符串对象添加到字符串常量池中,并且返回该字符串对象的引用.
JDK6+ 当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用.否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用.

1.17 对象的访问定位

Java 程序通过栈上的 reference 数据来操作堆上的具体对象。访问方式有①使用句柄和②直接指针两种:

  • 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

  • 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

使用句柄来访问的优点是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

1.8 类文件结构

查看

2 GC

Java垃圾回收机制,GC,ZGC等机制
常见的调优参数

命令 说明
-XX:SurvivorRatio Eden和Survivor的比值,默认8:1
-XX:NewRatio 老年代的和年轻代内存大小的比例
-XX:MaxTenuringThreshold 对象从年轻代晋升到老年代经过GC次数最大的阈值
-XX:+PretenuerSizeThreshold 新生成的对象可直接变为老年代的阈值

2.1 垃圾回收之标记算法

(1)对象被判定为垃圾的标准
没有被其他对象引用的就是"垃圾"
(2)判定对象是否为垃圾的算法有两种:引用计数算法可达性分析算法

引用计数算法:

  • 通过判断对象的引用数量来决定对象是否可以被回收; 每个对象实例都有一个引用计数器,被引用则+1,完成引用-1; 任何引用计数器为0的对象实例,都可以被当作垃圾收集
  • 优点:执行效率高,程序执行受影响较小
  • 缺点:无法检测出循环引用的情况,导致内存泄漏(例如,两个对象互相引用)

可达性分析算法:

  • 通过判断对象的引用链是否可达来决定对象是否可以被回收; 可以想象这个程序中所有的引用都是从一个GC Root出发,链接起来的引用图,如果在图中,说明对象还在被引用,如果没有被引用则可以判定为垃圾
  • 可以作为GC Root的对象:
  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用对象
  • 方法区中的类静态属性引用对象
  • 本地方法栈中JNI(Native方法)的引用对象
  • 活跃线程的引用对象

2.2 谈谈你了解的垃圾回收算法

(1) 标记-清除算法(Mark and Sweep)

  • 标记:从根集合进行扫描,对存活的对象进行标记; 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
  • 产生的后果:容易导致内存碎片化

(2) 复制算法(Copying)

  • 将一块可用的内存按比例或者容量分为对象面空闲面,对象在对象面上创建,存活的对象被从对象面复制到空闲面,将对象面所有内存清除
  • 优点:解决碎片化问题,顺序分配内存,简单高效,适用于对象存活率低的场景(尤其适用于年轻代中)

(3) 标记-整理算法(Compacting)

  • 标记:从根集合进行扫描,对存活的对象进行标记; 清除:移动所有存活的对象,且按照内存地址依次排列,然后将末端内存地址以后的内存全部回收.
  • 优点:解决了标记-清除算法中产生的碎片化问题,避免了内存的不连续;不用设置两块内存互换;适用于存活率高的场景(如老年代中)
  • 缺点:成本更高;

(4) 分代收集算法(Generational Collector)
可以理解为是垃圾回收算法的组合, 其按照对象生命周期的不同划分在不同堆中,对不同的堆采用不同的垃圾回收算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  • 目的: 为了提高JVM垃圾回收效率
  • 分带收集算法中GC的分类: MinorGC,Full GC

2.3 分代收集算法

在这里插入图片描述
(1) 在年轻代中使用复制算法

年轻代:存放那些生命周期短的对象,其分为Eden区和两个Survivor区(分别叫做from,to),这三个区的大小比例为8:1:1.

创建新的对象是在Eden区中,在进行GC时,使用复制算法将Eden中存活对象和from区中存活对象复制到to区中,年龄+1,清空Eden区和from区.此时,from区变为了to区,to区变为from区,下一次触发GC时,依旧是使用复制算法将Eden中存活对象和from区中存活对象复制到to区中,年龄+1,清空Eden区和from区.

当某个存活对象的年龄达到某个值(比如15时,其可以通过-XX:MaxTenuringThreshold设置),就会被移动到老年代中.

(2) 对象如何晋升到老年代

  • ①首先在经历一定的Minor次数依旧存活的对象(也就是年龄达到要求的)
  • ②Survivor区中存放不下的对象
  • ③新生成的大对象(-XX:+PretenuerSizeThreshold)

(3) 在老年代中使用标记-清除算法标记-整理算法
老年代:存放生命周期较长的对象

(4) 关键词

  1. Stop-the-World
  • JVM由于要执行GC而停止应用程序的执行
  • 任何一种GC算法都会发生
  • 多数GC优化就是通过减少Stop-the-World
  1. Safepoint
  • 分析过程中对象引用关系不会发生变化的点
  • 产生Safepoint的地方:方法调用、循环跳转、异常跳转等
  • 安全点数量要适中

2.4 常见的垃圾收集器 ( CMS、G1)

中间有了连线的可以搭配使用
在这里插入图片描述
(1)年轻代常见的垃圾收集器

  1. Serial (-XX:+UseSerialGC,复制算法)
  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
  • 简单高效,Client模式下默认的年轻代收集器
  1. ParNew (-XX:+UseParNewGC,复制算法)
  • 多线程,其余和Serial一样
  • 单核执行效率不如Serial,多核下优势较大
  1. Parallel Scavenage (-XX:+UseParallelGC,复制算法)
  • 多线程,更关注系统吞吐量
  • 多核下优势较大,Server模式下默认的年轻代收集器

(2)老年代常见的垃圾收集器

  1. Serial Old(-XX:+UseSerialOldGC,标记-整理算法)
  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
  • 简单高效,Client模式下默认的老年代收集器
  1. Parallel Old (-XX:+UseParallelOldGC,标记-整理算法)
  • 多线程,吞吐量优先
  • 多核下优势较大
  1. CMS (-XX:+UseConcMarkSweepGC,标记-清除算法

步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;STW
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短. STW
  • 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

优点:并发收集、低停顿。
缺点

  • 对 CPU 资源敏感;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

(3)可年轻代又可老年代 G1(Garbage First-XX:UseG1GC复制 标记-整理算法
将整个Java堆内存化为为多个大小相等的Region,年轻代和老年代不在物理隔离(仅保留其概念)。
优点:并行并发、分代收集、空间整合、可预测停顿

步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

(4)JDK11: Epsilon GCZGC

2.5 JDK 监控和故障处理工具总结

详细查看这个
JDK 命令行工具

  • jps:查看所有 Java 进程
  • jstat: 监视虚拟机各种运行状态信息
  • jinfo: 实时地查看和调整虚拟机各项参数
  • jmap:生成堆转储快照
  • jhat: 分析 heapdump 文件
  • jstack :生成虚拟机当前时刻的线程快照

JDK 可视化分析工具

  • JConsole:Java 监视与管理控制台

2.6 CMS 什么情况会触发 Full GC?

  • System.gc()方法的调用
  • 老年代空间不足
  • 方法区空间不足
  • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
  • 堆中分配很大的对象

面试题1、Object的 finalize() 方法的作用是否与 C++的析构函数作用相同?

  • 与C++的析构函数不同,析构函数调用确定,而它不是确定的
  • 将未被引用的对象放置于F-Queue队列
  • 优先级低,方法可随时会被终止
  • 给予对象最后一次重生的机会

面试题2、Java中的强引用,软引用,弱引用,虚引用有什么用

  1. 强引用(Strong Reference)
  • 最普遍的引用:Object obj = new Object()
  • 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
  • 通过将对象设置为null来弱化引用,使其被回收
  1. 软引用 (Soft Reference)
  • 对象处在有用但非必须的状态
  • 只有当内存空间不足时,GC会回收该引用的对象的内存
  • 可以用来实现高速缓存
  1. 弱引用 (Weak Reference)
  • 非必须的对象,比软引用更弱一些
  • GC时会被回
  • 被回收的概率也不大,因为GC线程优先级比较低
  • 适用于引用偶尔被使用且不影响垃圾收集的对象
  1. 虚引用 ( Phantom Reference)
  • 不会决定对象的生命周期
  • 任何时候都可能被垃圾收集器回收
  • 跟踪对象被垃圾收集器回收的活动,起哨兵作用
  • 必须和引用队列ReferenceQueue联合使用
  1. 补充引用队列 (ReferenceQueue)
  • 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
  • 存储关联的且被GC的软引用,弱引用,虚引用

在这里插入图片描述

面试题3、如何判断一个常量是废弃常量?

假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。

面试题4、如何判断一个类是无用的类?

满足3 个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注