关于java:大厂必问的Java虚拟机面试题

7次阅读

共计 30935 个字符,预计需要花费 78 分钟才能阅读完成。

本文目录:

  • 讲一下 JVM 内存构造?

    • 程序计数器
    • 虚拟机栈
    • 本地办法栈
    • 办法区
    • 运行时常量池
    • 间接内存
  • Java 对象的定位形式
  • 说一下堆栈的区别?
  • 什么状况下会产生栈溢出?
  • 类文件构造
  • 什么是类加载?类加载的过程?
  • 什么是双亲委派模型?
  • 为什么须要双亲委派模型?
  • 什么是类加载器,类加载器有哪些?
  • 类的实例化程序?
  • 如何判断一个对象是否存活?
  • 可作为 GC Roots 的对象有哪些?
  • 什么状况下类会被卸载?
  • 强援用、软援用、弱援用、虚援用是什么,有什么区别?
  • Minor GC 和 Full GC 的区别?
  • 内存的调配策略?
  • Full GC 的触发条件?
  • 垃圾回收算法有哪些?
  • 有哪些垃圾回收器?
  • 罕用的 JVM 调优的命令都有哪些?
  • 对象头理解吗?
  • 如何排查 OOM 的问题?
  • GC 是什么?为什么要 GC?

本文曾经收录到 github 仓库,此仓库用于分享互联网大厂高频面试题、Java 外围常识总结,包含 Java 根底、并发、MySQL、Springboot、MyBatis、Redis、RabbitMQ 等等,面试必备!欢送大家 star!
github 地址:https://github.com/Tyson0314/…

讲一下 JVM 内存构造?

JVM 内存构造分为 5 大区域,程序计数器 虚拟机栈 本地办法栈 办法区

程序计数器

线程公有的,作为以后线程的行号指示器,用于记录以后虚拟机正在执行的线程指令地址。程序计数器次要有两个作用:

  1. 以后线程所执行的字节码的行号指示器,通过它实现 代码的流程管制,如:程序执行、抉择、循环、异样解决。
  2. 在多线程的状况下,程序计数器用于 记录以后线程执行的地位,当线程被切换回来的时候可能晓得它上次执行的地位。

程序计数器是惟一一个不会呈现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创立而创立,随着线程的完结而死亡。

虚拟机栈

Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都领有:局部变量表 操作数栈 动静链接 办法进口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用完结后,都会有一个栈帧被弹出。

局部变量表是用于寄存办法参数和办法内的局部变量。

每个栈帧都蕴含一个指向运行时常量池中该栈所属办法的符号援用,在办法调用过程中,会进行动静链接,将这个符号援用转化为间接援用。

  • 局部符号援用在类加载阶段的时候就转化为间接援用,这种转化就是动态链接
  • 局部符号援用在运行期间转化为间接援用,这种转化就是动静链接

Java 虚拟机栈也是线程公有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创立而创立,随着线程的死亡而死亡。Java 虚拟机栈会呈现两种谬误:StackOverFlowErrorOutOfMemoryError

能够通过 -Xss 参数来指定每个线程的虚拟机栈内存大小:

java -Xss2M

本地办法栈

虚拟机栈为虚拟机执行 Java 办法服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。Native 办法个别是用其它语言(C、C++ 等)编写的。

本地办法被执行的时候,在本地办法栈也会创立一个栈帧,用于寄存该本地办法的局部变量表、操作数栈、动静链接、进口信息。

堆用于寄存对象实例,是垃圾收集器治理的次要区域,因而也被称作 GC 堆。堆能够细分为:新生代(Eden空间、From SurvivorTo Survivor空间)和老年代。

通过 -Xms设定程序启动时占用内存大小,通过 -Xmx 设定程序运行期间最大可占用的内存大小。如果程序运行须要占用更多的内存,超出了这个设置值,就会抛出 OutOfMemory 异样。

java -Xms1M -Xmx2M

办法区

办法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。

对办法区进行垃圾回收的次要指标是 对常量池的回收和对类的卸载

永恒代

办法区是 JVM 的标准,而永恒代 PermGen 是办法区的一种实现形式,并且只有 HotSpot 有永恒代。对于其余类型的虚拟机,如 JRockit 没有永恒代。因为办法区次要存储类的相干信息,所以对于动静生成类的场景比拟容易呈现永恒代的内存溢出。

元空间

JDK 1.8 的时候,HotSpot的永恒代被彻底移除了,应用元空间代替。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是应用间接内存。

为什么要将永恒代替换为元空间呢?

永恒代内存受限于 JVM 可用内存,而元空间应用的是间接内存,受本机可用内存的限度,尽管元空间仍旧可能溢出,然而相比永恒代内存溢出的概率更小。

运行时常量池

运行时常量池是办法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动静生成的常量,如 String 类的 intern()办法,也会被放入运行时常量池。

图片起源:https://blog.csdn.net/soonfly

间接内存

间接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机标准中定义的内存区域,然而这部分内存也被频繁地应用。而且也可能导致 OutOfMemoryError 谬误呈现。

NIO 的 Buffer 提供了 DirectBuffer,能够间接拜访零碎物理内存,防止堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer 间接调配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限度,不受最大堆内存的限度。

间接内存的读写操作比堆内存快,能够晋升程序 I / O 操作的性能。通常在 I / O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于须要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都倡议存储到间接内存。

Java 对象的定位形式

Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的拜访形式由虚拟机实现而定,目前支流的拜访形式有应用句柄和间接指针两种:

  • 如果应用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中蕴含了对象实例数据与类型数据各自的具体地址信息。应用句柄来拜访的最大益处是 reference 中存储的是稳固的句柄地址,在对象被挪动时只会扭转句柄中的实例数据指针,而 reference 自身不须要批改。
  • 间接指针。reference 中存储的间接就是对象的地址。对象蕴含到对象类型数据的指针,通过这个指针能够拜访对象类型数据。应用间接指针拜访形式最大的益处就是拜访对象速度快,它节俭了一次指针定位的工夫开销,虚拟机 hotspot 次要是应用间接指针来拜访对象。

说一下堆栈的区别?

  1. 堆的 物理地址调配 是不间断的,性能较慢;栈的物理地址调配是间断的,性能绝对较快。
  2. 堆寄存的是 对象的实例和数组 ;栈寄存的是 局部变量,操作数栈,返回后果 等。
  3. 堆是 线程共享 的;栈是 线程公有 的。

什么状况下会产生栈溢出?

  • 当线程申请的栈深度超过了虚拟机容许的最大深度时,会抛出 StackOverFlowError 异样。这种状况通常是因为办法递归没终止条件。
  • 新建线程的时候没有足够的内存去创立对应的虚拟机栈,虚构机会抛出 OutOfMemoryError 异样。比方线程启动过多就会呈现这种状况。

类文件构造

Class 文件构造如下:

ClassFile {
    u4             magic; // 类文件的标记
    u2             minor_version;// 小版本号
    u2             major_version;// 大版本号
    u2             constant_pool_count;// 常量池的数量
    cp_info        constant_pool[constant_pool_count-1];// 常量池
    u2             access_flags;// 类的拜访标记
    u2             this_class;// 以后类的索引
    u2             super_class;// 父类
    u2             interfaces_count;// 接口
    u2             interfaces[interfaces_count];// 一个类能够实现多个接口
    u2             fields_count;// 字段属性
    field_info     fields[fields_count];// 一个类会能够有个字段
    u2             methods_count;// 办法数量
    method_info    methods[methods_count];// 一个类能够有个多个办法
    u2             attributes_count;// 此类的属性表中的属性数
    attribute_info attributes[attributes_count];// 属性表汇合
}

主要参数如下:

魔数 class 文件标记。

文件版本:高版本的 Java 虚拟机能够执行低版本编译器生成的类文件,然而低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。

常量池 :寄存字面量和符号援用。字面量相似于 Java 的常量,如字符串,申明为final 的常量值等。符号援用蕴含三类:类和接口的全限定名,办法的名称和描述符,字段的名称和描述符。

拜访标记 :辨认类或者接口的访问信息,比方这个Class 是类还是接口,是否为 public 或者 abstract 类型等等。

以后类的索引:类索引用于确定这个类的全限定名。

什么是类加载?类加载的过程?

类的加载指的是将类的 class 文件中的二进制数据读入到内存中,将其放在运行时数据区的办法区内,而后在堆区创立一个此类的对象,通过这个对象能够拜访到办法区对应的类信息。

加载

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的动态存储构造转换为办法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为办法区类信息的拜访入口

验证

确保 Class 文件的字节流中蕴含的信息合乎虚拟机标准,保障在运行后不会危害虚拟机本身的平安。次要包含四种验证:文件格式验证,元数据验证,字节码验证,符号援用验证

筹备

为类变量分配内存并设置类变量初始值的阶段。

解析

虚拟机将常量池内的符号援用替换为间接援用的过程。符号援用用于形容指标,间接援用间接指向指标的地址。

初始化

开始执行类中定义的 Java 代码,初始化阶段是调用类结构器的过程。

什么是双亲委派模型?

一个类加载器收到一个类的加载申请时,它首先不会本人尝试去加载它,而是把这个申请 委派 给父类加载器去实现,这样层层委派,因而所有的加载申请最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈本人无奈实现这个加载申请时,子加载器才会尝试本人去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 办法运行过程如下:先查看类是否曾经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试本人去加载。源码如下:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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.
                    c = findClass(name);
                }
            }
            if (resolve) {resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
    }
}

为什么须要双亲委派模型?

双亲委派模型的益处:能够避免内存中呈现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,零碎中将会呈现多个不同的 Object 类,那么类之间的比拟后果及类的唯一性将无奈保障。

什么是类加载器,类加载器有哪些?

  • 实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

    次要有一下四品种加载器:

    • 启动类加载器:用来加载 Java 外围类库,无奈被 Java 程序间接援用。
    • 扩大类加载器:它用来加载 Java 的扩大库。Java 虚拟机的实现会提供一个扩大库目录。该类加载器在此目录外面查找并加载 Java 类。
    • 零碎类加载器 :它依据利用的类门路来加载 Java 类。可通过ClassLoader.getSystemClassLoader() 获取它。
    • 自定义类加载器 :通过继承java.lang.ClassLoader 类的形式实现。

类的实例化程序?

  1. 父类中的 static 代码块,以后类的 static 代码块
  2. 父类的一般代码块
  3. 父类的构造函数
  4. 以后类一般代码块
  5. 以后类的构造函数

如何判断一个对象是否存活?

对堆垃圾回收前的第一步就是要判断那些对象曾经死亡(即不再被任何路径援用的对象)。判断对象是否存活有两种办法:援用计数法和可达性剖析。

援用计数法

给对象中增加一个援用计数器,每当有一个中央援用它,计数器就加 1;当援用生效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被应用的。

这种办法很难解决对象之间互相循环援用的问题。比方上面的代码,obj1obj2 相互援用,这种状况下,援用计数器的值都是 1,不会被垃圾回收。

public class ReferenceCount {
    Object instance = null;
    public static void main(String[] args) {ReferenceCount obj1 = new ReferenceCount();
        ReferenceCount obj2 = new ReferenceCount();
        obj1.instance = obj2;
        obj2.instance = obj1;
        obj1 = null;
        obj2 = null;
    }
}

可达性剖析

通过 GC Root 对象为终点,从这些节点向下搜寻,搜寻所走过的门路叫援用链,当一个对象到 GC Root 没有任何的援用链相连时,阐明这个对象是不可用的。

可作为 GC Roots 的对象有哪些?

  1. 虚拟机栈中援用的对象
  2. 本地办法栈中 Native 办法援用的对象
  3. 办法区中类动态属性援用的对象
  4. 办法区中常量援用的对象

什么状况下类会被卸载?

须要同时满足以下 3 个条件类才可能会被卸载:

  • 该类所有的实例都曾经被回收。
  • 加载该类的类加载器曾经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

虚拟机能够对满足上述 3 个条件的类进行回收,但不肯定会进行回收。

强援用、软援用、弱援用、虚援用是什么,有什么区别?

强援用 :在程序中普遍存在的援用赋值,相似Object obj = new Object() 这种援用关系。只有强援用关系还存在,垃圾收集器就永远不会回收掉被援用的对象。

软援用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间有余了,就会回收这些对象的内存。

// 软援用
SoftReference<String> softRef = new SoftReference<String>(str);

弱援用:在进行垃圾回收时,不论以后内存空间足够与否,都会回收只具备弱援用的对象。

// 弱援用
WeakReference<String> weakRef = new WeakReference<String>(str);

虚援用 :虚援用并不会决定对象的生命周期。如果一个对象仅持有虚援用,那么它就和没有任何援用一样,在任何时候都可能被垃圾回收。 虚援用次要是为了能在对象被收集器回收时收到一个零碎告诉

Minor GC 和 Full GC 的区别?

  • Minor GC:回收新生代,因为新生代对象存活工夫很短,因而 Minor GC会频繁执行,执行的速度个别也会比拟快。
  • Full GC:回收老年代和新生代,老年代的对象存活工夫长,因而 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存的调配策略?

对象优先在 Eden 调配

大多数状况下,对象在新生代 Eden 上调配,当 Eden 空间不够时,触发 Minor GC

大对象间接进入老年代

大对象是指须要间断内存空间的对象,最典型的大对象有长字符串和大数组。能够设置 JVM 参数 -XX:PretenureSizeThreshold,大于此值的对象间接在老年代调配。

长期存活的对象进入老年代

通过参数 -XX:MaxTenuringThreshold 能够设置对象进入老年代的年龄阈值。对象在 Survivor 区每通过一次 Minor GC,年龄就减少 1 岁,当它的年龄减少到肯定水平,就会被降职到老年代中。

动静对象年龄断定

并非对象的年龄必须达到 MaxTenuringThreshold 能力降职老年代,如果在 Survivor 中雷同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象能够间接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。

空间调配担保

在产生 Minor GC 之前,虚拟机先查看老年代最大可用的间断空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是平安的。如果不成立的话虚构机会查看 HandlePromotionFailure 的值是否容许担保失败。如果容许,那么就会持续查看老年代最大可用的间断空间是否大于历次降职到老年代对象的均匀大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值为不容许担保失败,那么就要进行一次 Full GC

Full GC 的触发条件?

对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件绝对简单,有以下状况会产生 full GC:

调用 System.gc()

只是倡议虚拟机执行 Full GC,然而虚拟机不肯定真正去执行。不倡议应用这种形式,而是让虚拟机治理内存。

老年代空间有余

老年代空间有余的常见场景为前文所讲的大对象间接进入老年代、长期存活的对象进入老年代等。为了防止以上起因引起的 Full GC,该当尽量不要创立过大的对象以及数组。除此之外,能够通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还能够通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

空间调配担保失败

应用复制算法的 Minor GC 须要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

JDK 1.7 及以前的永恒代空间有余

在 JDK 1.7 及以前,HotSpot 虚拟机中的办法区是用永恒代实现的,永恒代中寄存的为一些 Class 的信息、常量、动态变量等数据。当零碎中要加载的类、反射的类和调用的办法较多时,永恒代可能会被占满,在未配置为采纳 CMS GC 的状况下也会执行 Full GC。如果通过 Full GC 依然回收不了,那么虚构机会抛出 java.lang.OutOfMemoryError。

垃圾回收算法有哪些?

垃圾回收算法有四种,别离是 标记革除法、标记整顿法、复制算法、分代收集算法

标记革除算法

首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记完结后对立将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会 产生大量不间断的空间碎片

复制革除算法

半区复制,用于新生代垃圾回收。将内存分为大小雷同的两块,每次应用其中的一块。当这一块的内存应用完后,就将还存活的对象复制到另一块去,而后再把应用的空间一次清理掉。

特点:实现简略,运行高效,但可用内存放大为了原来的一半,节约空间。

标记整顿算法

依据老年代的特点提出的一种标记算法,标记过程依然与 标记 - 革除 算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉边界以外的内存。

分类收集算法

依据各个年代的特点采纳最适当的收集算法。

个别将堆分为新生代和老年代。

  • 新生代应用复制算法
  • 老年代应用标记革除算法或者标记整顿算法

在新生代中,每次垃圾收集时都有少量对象死去,只有大量存活,应用复制算法比拟适合,只须要付出大量存活对象的复制老本就能够实现收集。老年代对象存活率高,适宜应用标记 - 清理或者标记 - 整顿算法进行垃圾回收。

有哪些垃圾回收器?

垃圾回收器次要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

这 7 种垃圾收集器的特点:

收集器 串行、并行 or 并发 新生代 / 老年代 算法 指标 实用场景
Serial 串行 新生代 复制算法 响应速度优先 单 CPU 环境下的 Client 模式
ParNew 并行 新生代 复制算法 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后盾运算而不须要太多交互的工作
Serial Old 串行 老年代 标记 - 整顿 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的后备预案
Parallel Old 并行 老年代 标记 - 整顿 吞吐量优先 在后盾运算而不须要太多交互的工作
CMS 并发 老年代 标记 - 革除 响应速度优先 集中在互联网站或 B / S 零碎服务端上的 Java 利用
G1 并发 both 标记 - 整顿 + 复制算法 响应速度优先 面向服务端利用,未来替换 CMS

Serial 收集器

单线程收集器,应用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其余所有的工作线程(Stop The World),直到它收集完结。

特点:简略高效;内存耗费小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

ParNew 收集器

Serial收集器的 多线程版本,除了应用多线程进行垃圾收集外,其余行为、参数与 Serial 收集器基本一致。

Parallel Scavenge 收集器

新生代收集器 ,基于 复制革除算法 实现的收集器。特点是 吞吐量优先 ,可能并行收集的多线程收集器,容许多个垃圾回收线程同时运行,升高垃圾收集工夫,进步吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的工夫与 CPU 总耗费工夫的比值( 吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫))。Parallel Scavenge 收集器关注点是 吞吐量,高效率的利用 CPU 资源 CMS 垃圾收集器关注点更多的是 用户线程的进展工夫

Parallel Scavenge收集器提供了两个参数用于 准确管制吞吐量 ,别离是管制最大垃圾收集进展工夫的-XX:MaxGCPauseMillis 参数以及间接设置吞吐量大小的 -XX:GCTimeRatio 参数。

  • -XX:MaxGCPauseMillis参数的值是一个大于 0 的毫秒数,收集器将尽量保障内存回收破费的工夫不超过用户设定值。
  • -XX:GCTimeRatio参数的值大于 0 小于 100,即垃圾收集工夫占总工夫的比率,相当于吞吐量的倒数。

Serial Old 收集器

Serial 收集器的老年代版本,单线程收集器,应用 标记整顿算法

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,应用 标记整顿算法

CMS 收集器

Concurrent Mark Sweep,并发标记革除,谋求获取 最短进展工夫 ,实现了让 垃圾收集线程与用户线程基本上同时工作

CMS 垃圾回收基于 标记革除算法 实现,整个过程分为四个步骤:

  • 初始标记:暂停所有用户线程(Stop The World),记录间接与 GC Roots 间接相连的对象。
  • 并发标记:从 GC Roots 开始对堆中对象进行可达性剖析,找出存活对象,耗时较长,然而不须要进展用户线程。
  • 从新标记:在并发标记期间对象的援用关系可能会变动,须要从新进行标记。此阶段也会暂停所有用户线程。
  • 并发革除:革除标记对象,这个阶段也是能够与用户线程同时并发的。

在整个过程中,耗时最长的是并发标记和并发革除阶段,这两个阶段垃圾收集线程都能够与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

长处:并发收集,进展工夫短。

毛病

  • 标记革除算法导致收集完结有 大量空间碎片
  • 产生浮动垃圾 ,在并发清理阶段用户线程还在运行,会一直有新的垃圾产生,这一部分垃圾呈现在标记过程之后,CMS 无奈在当次收集中回收它们,只好等到下一次垃圾回收再解决;

G1 收集器

G1 垃圾收集器的指标是在不同利用场景中 谋求高吞吐量和低进展之间的最佳均衡

G1 将整个堆分成雷同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old 和 Humongous。分区的大小取值范畴为 1M 到 32M,都是 2 的幂次方。分区大小能够通过 -XX:G1HeapRegionSize 参数指定。Humongous区域用于存储大对象。G1 规定只有大小超过了一个分区容量一半的对象就认为是大对象。

G1 收集器对各个分区回收所取得的空间大小和回收所需工夫的经验值进行排序,失去一个优先级列表,每次依据用户设置的最大回收进展工夫,优先回收价值最大的分区。

特点 :能够由用户 指定 冀望的垃圾收集进展工夫。

G1 收集器的回收过程分为以下几个步骤:

  • 初始标记。暂停所有其余线程,记录间接与 GC Roots 间接相连的对象,耗时较短。
  • 并发标记 。从GC Roots 开始对堆中对象进行可达性剖析,找出要回收的对象,耗时较长,不过能够和用户程序并发执行。
  • 最终标记。需对其余线程做短暂的暂停,用于解决并发标记阶段对象援用呈现变动的区域。
  • 筛选回收。对各个分区的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,而后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全副空间。这里的操作波及存活对象的挪动,会暂停用户线程,由多条收集器线程并行实现。

罕用的 JVM 调优的命令都有哪些?

jps:列出本机所有 Java 过程的 过程号

罕用参数如下:

  • -m 输入 main 办法的参数
  • -l 输入齐全的包名和利用主类名
  • -v 输入 JVM 参数
jps -lvm
//output
//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8

jstack:查看某个 Java 过程内的 线程堆栈信息 。应用参数-l 能够打印额定的锁信息,产生死锁时能够应用 jstack -l pid 察看锁持有状况。

jstack -l 4124 | more

输入后果如下:

"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

WAITING (parking)指线程处于挂起中,在期待某个条件产生,来把本人唤醒。

jstat:用于查看虚拟机各种 运行状态信息(类装载、内存、垃圾收集等运行数据)。应用参数 -gcuitl 能够查看垃圾回收的统计信息。

jstat -gcutil 4124
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  67.21  19.20  96.36  94.96     10    0.084     3    0.191    0.275

参数阐明:

  • S0Survivor0区以后应用比例
  • S1Survivor1区以后应用比例
  • EEden区应用比例
  • O:老年代应用比例
  • M:元数据区应用比例
  • CCS:压缩应用比例
  • YGC:年老代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收耗费工夫
  • GCT:垃圾回收耗费总工夫

jmap:查看 堆内存快照 。通过jmap 命令能够取得运行中的堆内存的快照,从而能够对堆内存进行离线剖析。

查问过程 4124 的堆内存快照,输入后果如下:

>jmap -heap 4124
Attaching to process ID 4124, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4238344192 (4042.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1412431872 (1347.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 327155712 (312.0MB)
   used     = 223702392 (213.33922576904297MB)
   free     = 103453320 (98.66077423095703MB)
   68.37795697725736% used
From Space:
   capacity = 21495808 (20.5MB)
   used     = 0 (0.0MB)
   free     = 21495808 (20.5MB)
   0.0% used
To Space:
   capacity = 23068672 (22.0MB)
   used     = 0 (0.0MB)
   free     = 23068672 (22.0MB)
   0.0% used
PS Old Generation
   capacity = 217579520 (207.5MB)
   used     = 41781472 (39.845916748046875MB)
   free     = 175798048 (167.65408325195312MB)
   19.20285144484187% used

27776 interned Strings occupying 3262336 bytes.

对象头理解吗?

Java 内存中的对象由以下三局部组成:对象头 实例数据 对齐填充字节

而对象头由以下三局部组成:mark word指向类信息的指针 数组长度(数组才有)。

mark word蕴含:对象的哈希码、分代年龄和锁标记位。

对象的实例数据就是 Java 对象的属性和值。

对齐填充字节:因为 JVM 要求对象占的内存大小是 8bit 的倍数,因而前面有几个字节用于把对象的大小补齐至 8bit 的倍数。

内存对齐的次要作用是:

  1. 平台起因:不是所有的硬件平台都能拜访任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异样。
  2. 性能起因:通过内存对齐后,CPU 的内存访问速度大大晋升。
  3. main 办法执行过程

    以下是示例代码:

    public class Application {public static void main(String[] args) {Person p = new Person("大彬");
            p.getName();}
    }
    
    class Person {
        public String name;
    
        public Person(String name) {this.name = name;}
    
        public String getName() {return this.name;}
    }

    执行 main 办法的过程如下:

    1. 编译 Application.java 后失去 Application.class 后,执行这个 class 文件,零碎会启动一个 JVM 过程,从类门路中找到一个名为 Application.class 的二进制文件,将 Application 类信息加载到运行时数据区的办法区内,这个过程叫做类的加载。
    2. JVM 找到 Application 的主程序入口,执行 main 办法。
    3. main办法的第一条语句为 Person p = new Person("大彬") ,就是让 JVM 创立一个 Person 对象,然而这个时候办法区中是没有 Person 类的信息的,所以 JVM 马上加载 Person 类,把 Person 类的信息放到办法区中。
    4. 加载完 Person 类后,JVM 在堆中分配内存给 Person 对象,而后调用构造函数初始化 Person 对象,这个 Person 对象持有 指向办法区中的 Person 类的类型信息 的援用。
    5. 执行 p.getName() 时,JVM 依据 p 的援用找到 p 所指向的对象,而后依据此对象持有的援用定位到办法区中 Person 类的类型信息的办法表,取得 getName() 的字节码地址。
    6. 执行 getName() 办法。
  4. 对象创立过程

    1. 类加载查看:当虚拟机遇到一条 new 指令时,首先查看是否能在常量池中定位到这个类的符号援用,并且查看这个符号援用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。
    2. 分配内存:在类加载查看通过后,接下来虚拟机将为对象实例分配内存。
    3. 初始化。调配到的内存空间都初始化为零值,通过这个操作保障了对象的字段能够不赋初始值就间接应用,程序能拜访到这些字段的数据类型所对应的零值。
    4. 设置对象头Hotspot 虚拟机的对象头包含:存储对象本身的运行时数据(哈希码、分代年龄、锁标记等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    5. 依照 Java 代码进行初始化

如何排查 OOM 的问题?

排查 OOM 的办法:

  • 减少 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 产生时主动 dump 堆内存信息到指定目录;
  • jstat 查看监控 JVM 的内存和 GC 状况,评估问题大略出在什么区域;
  • 应用 MAT 工具载入 dump 文件,剖析大对象的占用状况。

GC 是什么?为什么要 GC?

GC 是垃圾收集的意思(Gabage Collection)。内存解决是编程人员容易呈现问题的中央,遗记或者谬误的内存回收会导致程序的不稳固甚至解体,Java 提供的 GC 性能能够主动监测对象是否超过作用域从而达到主动回收内存的目标。

参考资料

  • 周志明. 深刻了解 Java 虚拟机 [M]. 机械工业出版社

<!– START doctoc generated TOC please keep comment here to allow auto update –>
<!– DON’T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE –>

本文目录:

  • 讲一下 JVM 内存构造?

    • 程序计数器
    • 虚拟机栈
    • 本地办法栈
    • 办法区
    • 运行时常量池
    • 间接内存
  • Java 对象的定位形式
  • 说一下堆栈的区别?
  • 什么状况下会产生栈溢出?
  • 类文件构造
  • 什么是类加载?类加载的过程?
  • 什么是双亲委派模型?
  • 为什么须要双亲委派模型?
  • 什么是类加载器,类加载器有哪些?
  • 类的实例化程序?
  • 如何判断一个对象是否存活?
  • 可作为 GC Roots 的对象有哪些?
  • 什么状况下类会被卸载?
  • 强援用、软援用、弱援用、虚援用是什么,有什么区别?
  • Minor GC 和 Full GC 的区别?
  • 内存的调配策略?
  • Full GC 的触发条件?
  • 垃圾回收算法有哪些?
  • 有哪些垃圾回收器?
  • 罕用的 JVM 调优的命令都有哪些?
  • 对象头理解吗?
  • 如何排查 OOM 的问题?
  • GC 是什么?为什么要 GC?

<!– END doctoc generated TOC please keep comment here to allow auto update –>

我将大厂常见的高频面试题整顿成 PDF 了,不便大家浏览,须要的小伙伴能够自行下载(复制链接到浏览器关上):

链接:https://pan.baidu.com/s/16GnV…
提取码:6666

讲一下 JVM 内存构造?

JVM 内存构造分为 5 大区域,程序计数器 虚拟机栈 本地办法栈 办法区

程序计数器

线程公有的,作为以后线程的行号指示器,用于记录以后虚拟机正在执行的线程指令地址。程序计数器次要有两个作用:

  1. 以后线程所执行的字节码的行号指示器,通过它实现 代码的流程管制,如:程序执行、抉择、循环、异样解决。
  2. 在多线程的状况下,程序计数器用于 记录以后线程执行的地位,当线程被切换回来的时候可能晓得它上次执行的地位。

程序计数器是惟一一个不会呈现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创立而创立,随着线程的完结而死亡。

虚拟机栈

Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都领有:局部变量表 操作数栈 动静链接 办法进口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用完结后,都会有一个栈帧被弹出。

局部变量表是用于寄存办法参数和办法内的局部变量。

每个栈帧都蕴含一个指向运行时常量池中该栈所属办法的符号援用,在办法调用过程中,会进行动静链接,将这个符号援用转化为间接援用。

  • 局部符号援用在类加载阶段的时候就转化为间接援用,这种转化就是动态链接
  • 局部符号援用在运行期间转化为间接援用,这种转化就是动静链接

Java 虚拟机栈也是线程公有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创立而创立,随着线程的死亡而死亡。Java 虚拟机栈会呈现两种谬误:StackOverFlowErrorOutOfMemoryError

能够通过 -Xss 参数来指定每个线程的虚拟机栈内存大小:

java -Xss2M

本地办法栈

虚拟机栈为虚拟机执行 Java 办法服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。Native 办法个别是用其它语言(C、C++ 等)编写的。

本地办法被执行的时候,在本地办法栈也会创立一个栈帧,用于寄存该本地办法的局部变量表、操作数栈、动静链接、进口信息。

堆用于寄存对象实例,是垃圾收集器治理的次要区域,因而也被称作 GC 堆。堆能够细分为:新生代(Eden空间、From SurvivorTo Survivor空间)和老年代。

通过 -Xms设定程序启动时占用内存大小,通过 -Xmx 设定程序运行期间最大可占用的内存大小。如果程序运行须要占用更多的内存,超出了这个设置值,就会抛出 OutOfMemory 异样。

java -Xms1M -Xmx2M

办法区

办法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。

对办法区进行垃圾回收的次要指标是 对常量池的回收和对类的卸载

永恒代

办法区是 JVM 的标准,而永恒代 PermGen 是办法区的一种实现形式,并且只有 HotSpot 有永恒代。对于其余类型的虚拟机,如 JRockit 没有永恒代。因为办法区次要存储类的相干信息,所以对于动静生成类的场景比拟容易呈现永恒代的内存溢出。

元空间

JDK 1.8 的时候,HotSpot的永恒代被彻底移除了,应用元空间代替。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是应用间接内存。

为什么要将永恒代替换为元空间呢?

永恒代内存受限于 JVM 可用内存,而元空间应用的是间接内存,受本机可用内存的限度,尽管元空间仍旧可能溢出,然而相比永恒代内存溢出的概率更小。

运行时常量池

运行时常量池是办法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动静生成的常量,如 String 类的 intern()办法,也会被放入运行时常量池。

图片起源:https://blog.csdn.net/soonfly

间接内存

间接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机标准中定义的内存区域,然而这部分内存也被频繁地应用。而且也可能导致 OutOfMemoryError 谬误呈现。

NIO 的 Buffer 提供了 DirectBuffer,能够间接拜访零碎物理内存,防止堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer 间接调配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限度,不受最大堆内存的限度。

间接内存的读写操作比堆内存快,能够晋升程序 I / O 操作的性能。通常在 I / O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于须要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都倡议存储到间接内存。

Java 对象的定位形式

Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的拜访形式由虚拟机实现而定,目前支流的拜访形式有应用句柄和间接指针两种:

  • 如果应用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中蕴含了对象实例数据与类型数据各自的具体地址信息。应用句柄来拜访的最大益处是 reference 中存储的是稳固的句柄地址,在对象被挪动时只会扭转句柄中的实例数据指针,而 reference 自身不须要批改。
  • 间接指针。reference 中存储的间接就是对象的地址。对象蕴含到对象类型数据的指针,通过这个指针能够拜访对象类型数据。应用间接指针拜访形式最大的益处就是拜访对象速度快,它节俭了一次指针定位的工夫开销,虚拟机 hotspot 次要是应用间接指针来拜访对象。

说一下堆栈的区别?

  1. 堆的 物理地址调配 是不间断的,性能较慢;栈的物理地址调配是间断的,性能绝对较快。
  2. 堆寄存的是 对象的实例和数组 ;栈寄存的是 局部变量,操作数栈,返回后果 等。
  3. 堆是 线程共享 的;栈是 线程公有 的。

什么状况下会产生栈溢出?

  • 当线程申请的栈深度超过了虚拟机容许的最大深度时,会抛出 StackOverFlowError 异样。这种状况通常是因为办法递归没终止条件。
  • 新建线程的时候没有足够的内存去创立对应的虚拟机栈,虚构机会抛出 OutOfMemoryError 异样。比方线程启动过多就会呈现这种状况。

类文件构造

Class 文件构造如下:

ClassFile {
    u4             magic; // 类文件的标记
    u2             minor_version;// 小版本号
    u2             major_version;// 大版本号
    u2             constant_pool_count;// 常量池的数量
    cp_info        constant_pool[constant_pool_count-1];// 常量池
    u2             access_flags;// 类的拜访标记
    u2             this_class;// 以后类的索引
    u2             super_class;// 父类
    u2             interfaces_count;// 接口
    u2             interfaces[interfaces_count];// 一个类能够实现多个接口
    u2             fields_count;// 字段属性
    field_info     fields[fields_count];// 一个类会能够有个字段
    u2             methods_count;// 办法数量
    method_info    methods[methods_count];// 一个类能够有个多个办法
    u2             attributes_count;// 此类的属性表中的属性数
    attribute_info attributes[attributes_count];// 属性表汇合
}

主要参数如下:

魔数 class 文件标记。

文件版本:高版本的 Java 虚拟机能够执行低版本编译器生成的类文件,然而低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。

常量池 :寄存字面量和符号援用。字面量相似于 Java 的常量,如字符串,申明为final 的常量值等。符号援用蕴含三类:类和接口的全限定名,办法的名称和描述符,字段的名称和描述符。

拜访标记 :辨认类或者接口的访问信息,比方这个Class 是类还是接口,是否为 public 或者 abstract 类型等等。

以后类的索引:类索引用于确定这个类的全限定名。

什么是类加载?类加载的过程?

类的加载指的是将类的 class 文件中的二进制数据读入到内存中,将其放在运行时数据区的办法区内,而后在堆区创立一个此类的对象,通过这个对象能够拜访到办法区对应的类信息。

加载

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的动态存储构造转换为办法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为办法区类信息的拜访入口

验证

确保 Class 文件的字节流中蕴含的信息合乎虚拟机标准,保障在运行后不会危害虚拟机本身的平安。次要包含四种验证:文件格式验证,元数据验证,字节码验证,符号援用验证

筹备

为类变量分配内存并设置类变量初始值的阶段。

解析

虚拟机将常量池内的符号援用替换为间接援用的过程。符号援用用于形容指标,间接援用间接指向指标的地址。

初始化

开始执行类中定义的 Java 代码,初始化阶段是调用类结构器的过程。

什么是双亲委派模型?

一个类加载器收到一个类的加载申请时,它首先不会本人尝试去加载它,而是把这个申请 委派 给父类加载器去实现,这样层层委派,因而所有的加载申请最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈本人无奈实现这个加载申请时,子加载器才会尝试本人去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 办法运行过程如下:先查看类是否曾经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试本人去加载。源码如下:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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.
                    c = findClass(name);
                }
            }
            if (resolve) {resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
    }
}

为什么须要双亲委派模型?

双亲委派模型的益处:能够避免内存中呈现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,零碎中将会呈现多个不同的 Object 类,那么类之间的比拟后果及类的唯一性将无奈保障。

什么是类加载器,类加载器有哪些?

  • 实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

    次要有一下四品种加载器:

    • 启动类加载器:用来加载 Java 外围类库,无奈被 Java 程序间接援用。
    • 扩大类加载器:它用来加载 Java 的扩大库。Java 虚拟机的实现会提供一个扩大库目录。该类加载器在此目录外面查找并加载 Java 类。
    • 零碎类加载器 :它依据利用的类门路来加载 Java 类。可通过ClassLoader.getSystemClassLoader() 获取它。
    • 自定义类加载器 :通过继承java.lang.ClassLoader 类的形式实现。

类的实例化程序?

  1. 父类中的 static 代码块,以后类的 static 代码块
  2. 父类的一般代码块
  3. 父类的构造函数
  4. 以后类一般代码块
  5. 以后类的构造函数

如何判断一个对象是否存活?

对堆垃圾回收前的第一步就是要判断那些对象曾经死亡(即不再被任何路径援用的对象)。判断对象是否存活有两种办法:援用计数法和可达性剖析。

援用计数法

给对象中增加一个援用计数器,每当有一个中央援用它,计数器就加 1;当援用生效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被应用的。

这种办法很难解决对象之间互相循环援用的问题。比方上面的代码,obj1obj2 相互援用,这种状况下,援用计数器的值都是 1,不会被垃圾回收。

public class ReferenceCount {
    Object instance = null;
    public static void main(String[] args) {ReferenceCount obj1 = new ReferenceCount();
        ReferenceCount obj2 = new ReferenceCount();
        obj1.instance = obj2;
        obj2.instance = obj1;
        obj1 = null;
        obj2 = null;
    }
}

可达性剖析

通过 GC Root 对象为终点,从这些节点向下搜寻,搜寻所走过的门路叫援用链,当一个对象到 GC Root 没有任何的援用链相连时,阐明这个对象是不可用的。

可作为 GC Roots 的对象有哪些?

  1. 虚拟机栈中援用的对象
  2. 本地办法栈中 Native 办法援用的对象
  3. 办法区中类动态属性援用的对象
  4. 办法区中常量援用的对象

什么状况下类会被卸载?

须要同时满足以下 3 个条件类才可能会被卸载:

  • 该类所有的实例都曾经被回收。
  • 加载该类的类加载器曾经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

虚拟机能够对满足上述 3 个条件的类进行回收,但不肯定会进行回收。

强援用、软援用、弱援用、虚援用是什么,有什么区别?

强援用 :在程序中普遍存在的援用赋值,相似Object obj = new Object() 这种援用关系。只有强援用关系还存在,垃圾收集器就永远不会回收掉被援用的对象。

软援用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间有余了,就会回收这些对象的内存。

// 软援用
SoftReference<String> softRef = new SoftReference<String>(str);

弱援用:在进行垃圾回收时,不论以后内存空间足够与否,都会回收只具备弱援用的对象。

// 弱援用
WeakReference<String> weakRef = new WeakReference<String>(str);

虚援用 :虚援用并不会决定对象的生命周期。如果一个对象仅持有虚援用,那么它就和没有任何援用一样,在任何时候都可能被垃圾回收。 虚援用次要是为了能在对象被收集器回收时收到一个零碎告诉

Minor GC 和 Full GC 的区别?

  • Minor GC:回收新生代,因为新生代对象存活工夫很短,因而 Minor GC会频繁执行,执行的速度个别也会比拟快。
  • Full GC:回收老年代和新生代,老年代的对象存活工夫长,因而 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存的调配策略?

对象优先在 Eden 调配

大多数状况下,对象在新生代 Eden 上调配,当 Eden 空间不够时,触发 Minor GC

大对象间接进入老年代

大对象是指须要间断内存空间的对象,最典型的大对象有长字符串和大数组。能够设置 JVM 参数 -XX:PretenureSizeThreshold,大于此值的对象间接在老年代调配。

长期存活的对象进入老年代

通过参数 -XX:MaxTenuringThreshold 能够设置对象进入老年代的年龄阈值。对象在 Survivor 区每通过一次 Minor GC,年龄就减少 1 岁,当它的年龄减少到肯定水平,就会被降职到老年代中。

动静对象年龄断定

并非对象的年龄必须达到 MaxTenuringThreshold 能力降职老年代,如果在 Survivor 中雷同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象能够间接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。

空间调配担保

在产生 Minor GC 之前,虚拟机先查看老年代最大可用的间断空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是平安的。如果不成立的话虚构机会查看 HandlePromotionFailure 的值是否容许担保失败。如果容许,那么就会持续查看老年代最大可用的间断空间是否大于历次降职到老年代对象的均匀大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值为不容许担保失败,那么就要进行一次 Full GC

Full GC 的触发条件?

对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件绝对简单,有以下状况会产生 full GC:

调用 System.gc()

只是倡议虚拟机执行 Full GC,然而虚拟机不肯定真正去执行。不倡议应用这种形式,而是让虚拟机治理内存。

老年代空间有余

老年代空间有余的常见场景为前文所讲的大对象间接进入老年代、长期存活的对象进入老年代等。为了防止以上起因引起的 Full GC,该当尽量不要创立过大的对象以及数组。除此之外,能够通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还能够通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

空间调配担保失败

应用复制算法的 Minor GC 须要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

JDK 1.7 及以前的永恒代空间有余

在 JDK 1.7 及以前,HotSpot 虚拟机中的办法区是用永恒代实现的,永恒代中寄存的为一些 Class 的信息、常量、动态变量等数据。当零碎中要加载的类、反射的类和调用的办法较多时,永恒代可能会被占满,在未配置为采纳 CMS GC 的状况下也会执行 Full GC。如果通过 Full GC 依然回收不了,那么虚构机会抛出 java.lang.OutOfMemoryError。

垃圾回收算法有哪些?

垃圾回收算法有四种,别离是 标记革除法、标记整顿法、复制算法、分代收集算法

标记革除算法

首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记完结后对立将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会 产生大量不间断的空间碎片

复制革除算法

半区复制,用于新生代垃圾回收。将内存分为大小雷同的两块,每次应用其中的一块。当这一块的内存应用完后,就将还存活的对象复制到另一块去,而后再把应用的空间一次清理掉。

特点:实现简略,运行高效,但可用内存放大为了原来的一半,节约空间。

标记整顿算法

依据老年代的特点提出的一种标记算法,标记过程依然与 标记 - 革除 算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉边界以外的内存。

分类收集算法

依据各个年代的特点采纳最适当的收集算法。

个别将堆分为新生代和老年代。

  • 新生代应用复制算法
  • 老年代应用标记革除算法或者标记整顿算法

在新生代中,每次垃圾收集时都有少量对象死去,只有大量存活,应用复制算法比拟适合,只须要付出大量存活对象的复制老本就能够实现收集。老年代对象存活率高,适宜应用标记 - 清理或者标记 - 整顿算法进行垃圾回收。

有哪些垃圾回收器?

垃圾回收器次要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

这 7 种垃圾收集器的特点:

收集器 串行、并行 or 并发 新生代 / 老年代 算法 指标 实用场景
Serial 串行 新生代 复制算法 响应速度优先 单 CPU 环境下的 Client 模式
ParNew 并行 新生代 复制算法 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后盾运算而不须要太多交互的工作
Serial Old 串行 老年代 标记 - 整顿 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的后备预案
Parallel Old 并行 老年代 标记 - 整顿 吞吐量优先 在后盾运算而不须要太多交互的工作
CMS 并发 老年代 标记 - 革除 响应速度优先 集中在互联网站或 B / S 零碎服务端上的 Java 利用
G1 并发 both 标记 - 整顿 + 复制算法 响应速度优先 面向服务端利用,未来替换 CMS

Serial 收集器

单线程收集器,应用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其余所有的工作线程(Stop The World),直到它收集完结。

特点:简略高效;内存耗费小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

ParNew 收集器

Serial收集器的 多线程版本,除了应用多线程进行垃圾收集外,其余行为、参数与 Serial 收集器基本一致。

Parallel Scavenge 收集器

新生代收集器 ,基于 复制革除算法 实现的收集器。特点是 吞吐量优先 ,可能并行收集的多线程收集器,容许多个垃圾回收线程同时运行,升高垃圾收集工夫,进步吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的工夫与 CPU 总耗费工夫的比值( 吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫))。Parallel Scavenge 收集器关注点是 吞吐量,高效率的利用 CPU 资源 CMS 垃圾收集器关注点更多的是 用户线程的进展工夫

Parallel Scavenge收集器提供了两个参数用于 准确管制吞吐量 ,别离是管制最大垃圾收集进展工夫的-XX:MaxGCPauseMillis 参数以及间接设置吞吐量大小的 -XX:GCTimeRatio 参数。

  • -XX:MaxGCPauseMillis参数的值是一个大于 0 的毫秒数,收集器将尽量保障内存回收破费的工夫不超过用户设定值。
  • -XX:GCTimeRatio参数的值大于 0 小于 100,即垃圾收集工夫占总工夫的比率,相当于吞吐量的倒数。

Serial Old 收集器

Serial 收集器的老年代版本,单线程收集器,应用 标记整顿算法

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,应用 标记整顿算法

CMS 收集器

Concurrent Mark Sweep,并发标记革除,谋求获取 最短进展工夫 ,实现了让 垃圾收集线程与用户线程基本上同时工作

CMS 垃圾回收基于 标记革除算法 实现,整个过程分为四个步骤:

  • 初始标记:暂停所有用户线程(Stop The World),记录间接与 GC Roots 间接相连的对象。
  • 并发标记:从 GC Roots 开始对堆中对象进行可达性剖析,找出存活对象,耗时较长,然而不须要进展用户线程。
  • 从新标记:在并发标记期间对象的援用关系可能会变动,须要从新进行标记。此阶段也会暂停所有用户线程。
  • 并发革除:革除标记对象,这个阶段也是能够与用户线程同时并发的。

在整个过程中,耗时最长的是并发标记和并发革除阶段,这两个阶段垃圾收集线程都能够与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

长处:并发收集,进展工夫短。

毛病

  • 标记革除算法导致收集完结有 大量空间碎片
  • 产生浮动垃圾 ,在并发清理阶段用户线程还在运行,会一直有新的垃圾产生,这一部分垃圾呈现在标记过程之后,CMS 无奈在当次收集中回收它们,只好等到下一次垃圾回收再解决;

G1 收集器

G1 垃圾收集器的指标是在不同利用场景中 谋求高吞吐量和低进展之间的最佳均衡

G1 将整个堆分成雷同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old 和 Humongous。分区的大小取值范畴为 1M 到 32M,都是 2 的幂次方。分区大小能够通过 -XX:G1HeapRegionSize 参数指定。Humongous区域用于存储大对象。G1 规定只有大小超过了一个分区容量一半的对象就认为是大对象。

G1 收集器对各个分区回收所取得的空间大小和回收所需工夫的经验值进行排序,失去一个优先级列表,每次依据用户设置的最大回收进展工夫,优先回收价值最大的分区。

特点 :能够由用户 指定 冀望的垃圾收集进展工夫。

G1 收集器的回收过程分为以下几个步骤:

  • 初始标记。暂停所有其余线程,记录间接与 GC Roots 间接相连的对象,耗时较短。
  • 并发标记 。从GC Roots 开始对堆中对象进行可达性剖析,找出要回收的对象,耗时较长,不过能够和用户程序并发执行。
  • 最终标记。需对其余线程做短暂的暂停,用于解决并发标记阶段对象援用呈现变动的区域。
  • 筛选回收。对各个分区的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,而后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全副空间。这里的操作波及存活对象的挪动,会暂停用户线程,由多条收集器线程并行实现。

罕用的 JVM 调优的命令都有哪些?

jps:列出本机所有 Java 过程的 过程号

罕用参数如下:

  • -m 输入 main 办法的参数
  • -l 输入齐全的包名和利用主类名
  • -v 输入 JVM 参数
jps -lvm
//output
//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8

jstack:查看某个 Java 过程内的 线程堆栈信息 。应用参数-l 能够打印额定的锁信息,产生死锁时能够应用 jstack -l pid 察看锁持有状况。

jstack -l 4124 | more

输入后果如下:

"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

WAITING (parking)指线程处于挂起中,在期待某个条件产生,来把本人唤醒。

jstat:用于查看虚拟机各种 运行状态信息(类装载、内存、垃圾收集等运行数据)。应用参数 -gcuitl 能够查看垃圾回收的统计信息。

jstat -gcutil 4124
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  67.21  19.20  96.36  94.96     10    0.084     3    0.191    0.275

参数阐明:

  • S0Survivor0区以后应用比例
  • S1Survivor1区以后应用比例
  • EEden区应用比例
  • O:老年代应用比例
  • M:元数据区应用比例
  • CCS:压缩应用比例
  • YGC:年老代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收耗费工夫
  • GCT:垃圾回收耗费总工夫

jmap:查看 堆内存快照 。通过jmap 命令能够取得运行中的堆内存的快照,从而能够对堆内存进行离线剖析。

查问过程 4124 的堆内存快照,输入后果如下:

>jmap -heap 4124
Attaching to process ID 4124, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4238344192 (4042.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1412431872 (1347.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 327155712 (312.0MB)
   used     = 223702392 (213.33922576904297MB)
   free     = 103453320 (98.66077423095703MB)
   68.37795697725736% used
From Space:
   capacity = 21495808 (20.5MB)
   used     = 0 (0.0MB)
   free     = 21495808 (20.5MB)
   0.0% used
To Space:
   capacity = 23068672 (22.0MB)
   used     = 0 (0.0MB)
   free     = 23068672 (22.0MB)
   0.0% used
PS Old Generation
   capacity = 217579520 (207.5MB)
   used     = 41781472 (39.845916748046875MB)
   free     = 175798048 (167.65408325195312MB)
   19.20285144484187% used

27776 interned Strings occupying 3262336 bytes.

对象头理解吗?

Java 内存中的对象由以下三局部组成:对象头 实例数据 对齐填充字节

而对象头由以下三局部组成:mark word指向类信息的指针 数组长度(数组才有)。

mark word蕴含:对象的哈希码、分代年龄和锁标记位。

对象的实例数据就是 Java 对象的属性和值。

对齐填充字节:因为 JVM 要求对象占的内存大小是 8bit 的倍数,因而前面有几个字节用于把对象的大小补齐至 8bit 的倍数。

内存对齐的次要作用是:

  1. 平台起因:不是所有的硬件平台都能拜访任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异样。
  2. 性能起因:通过内存对齐后,CPU 的内存访问速度大大晋升。
  3. main 办法执行过程

    以下是示例代码:

    public class Application {public static void main(String[] args) {Person p = new Person("大彬");
            p.getName();}
    }
    
    class Person {
        public String name;
    
        public Person(String name) {this.name = name;}
    
        public String getName() {return this.name;}
    }

    执行 main 办法的过程如下:

    1. 编译 Application.java 后失去 Application.class 后,执行这个 class 文件,零碎会启动一个 JVM 过程,从类门路中找到一个名为 Application.class 的二进制文件,将 Application 类信息加载到运行时数据区的办法区内,这个过程叫做类的加载。
    2. JVM 找到 Application 的主程序入口,执行 main 办法。
    3. main办法的第一条语句为 Person p = new Person("大彬") ,就是让 JVM 创立一个 Person 对象,然而这个时候办法区中是没有 Person 类的信息的,所以 JVM 马上加载 Person 类,把 Person 类的信息放到办法区中。
    4. 加载完 Person 类后,JVM 在堆中分配内存给 Person 对象,而后调用构造函数初始化 Person 对象,这个 Person 对象持有 指向办法区中的 Person 类的类型信息 的援用。
    5. 执行 p.getName() 时,JVM 依据 p 的援用找到 p 所指向的对象,而后依据此对象持有的援用定位到办法区中 Person 类的类型信息的办法表,取得 getName() 的字节码地址。
    6. 执行 getName() 办法。
  4. 对象创立过程

    1. 类加载查看:当虚拟机遇到一条 new 指令时,首先查看是否能在常量池中定位到这个类的符号援用,并且查看这个符号援用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。
    2. 分配内存:在类加载查看通过后,接下来虚拟机将为对象实例分配内存。
    3. 初始化。调配到的内存空间都初始化为零值,通过这个操作保障了对象的字段能够不赋初始值就间接应用,程序能拜访到这些字段的数据类型所对应的零值。
    4. 设置对象头Hotspot 虚拟机的对象头包含:存储对象本身的运行时数据(哈希码、分代年龄、锁标记等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    5. 依照 Java 代码进行初始化

如何排查 OOM 的问题?

排查 OOM 的办法:

  • 减少 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 产生时主动 dump 堆内存信息到指定目录;
  • jstat 查看监控 JVM 的内存和 GC 状况,评估问题大略出在什么区域;
  • 应用 MAT 工具载入 dump 文件,剖析大对象的占用状况。

GC 是什么?为什么要 GC?

GC 是垃圾收集的意思(Gabage Collection)。内存解决是编程人员容易呈现问题的中央,遗记或者谬误的内存回收会导致程序的不稳固甚至解体,Java 提供的 GC 性能能够主动监测对象是否超过作用域从而达到主动回收内存的目标。

参考资料

  • 周志明. 深刻了解 Java 虚拟机 [M]. 机械工业出版社

文章对你有用的话,点个赞,反对一下~

我是大彬,非科班转码,校招拿了多家互联网中大厂 offer,专一分享 Java 技术干货,欢送关注~

正文完
 0