1、JVM内存结构

1.1 JVM内存结构是怎样的?(运行时数据区是怎样的)?

主要分为以下几个区域:

  • :存放对象的实例,堆取按照年代划分还可以细分为新生代和老年代,其中新生代还可以细分成Eden、From Survivor、To Survivor区;
  • 方法区:JDK 1.8之前也被称为永久代,存放类相关信息、字符串常量和静态变量;JDK 1.8后取消了永久代,将类相关信息分流到了元空间(本地内存),字符串常量和静态变量分流到了堆中;
  • Java虚拟机栈:描述的是Java方法的内存模型,调用一个方法时会创建一个栈帧的结构,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;
  • 本地方法栈:描述的是native方法(非Java方法)的内存模型,其他同上;
  • 程序计数器:可以看成是当前线程所执行的字节码的行号指示器,程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的内存空间。

    1.2 运行时区域哪些是线程共享的?哪些是独享的?

  • 线程共享:堆、方法区;

  • 线程独享:程序计数器、Java虚拟机栈、本地方法栈。

    1.3 除了JVM运行时内存以外,还有什么区域可以用么?

    除了虚拟机运行时数据区域外,还有一部分内存也被频繁使用,它不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它就是直接内存。直接内存的分配不受Java堆大小的限制,但还是会受到机器总内存的限制。Native函数库可以直接分配堆外内存(直接内存),然后通过一个存储在Java堆中的对象来操作这块直接内存,避免了在Java堆和Native堆来回复制数据,提高性能,如下图所示:
    image.png

1.4 JVM内存结构里的堆和栈的区别是什么?

  • JVM堆区是线程共享的,主要存放对象实例;
  • JVM栈区是线程独享的,更多的时候指的是局部变量表,主要存放基本数据类型、对象的引用。

    1.5 Java中的数组是存储在堆上还是栈上?

    在Java中数组是对象,数组的实例是保存在堆上,数组对象的引用是保存在栈上。

    1.6 如何获取堆和栈的dump文件?

    Java Dump,是Java虚拟机运行时快照,将Java虚拟机运行时的状态和信息保存到该文件。可以在服务器上使用jmap命令来获取堆dump,使用jstack命令获取线程的调用栈dump。

    1.7 直接内存是什么?

    除了虚拟机运行时数据区域外,还有一部分内存也被频繁使用,它不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它就是直接内存。直接内存的分配不受Java堆大小的限制,但还是会受到机器总内存的限制。Native函数库可以直接分配堆外内存(直接内存),然后通过一个存储在Java堆中的对象来操作这块直接内存,避免了在Java堆和Native堆来回复制数据,如下图所示:
    image.png

    1.8 介绍一下字符串常量池?

    HotSpot虚拟机中的字符串常量池本质上是一个哈希表,存放的不是常量字符串实例本身,而是存放的指向常量字符串实例的引用,常量字符串实例本身是存放在堆中的。字符串常量池是存放在本地内存中,而字符串常量池里的引用指向的Interned Strings在JDK 1.8之前是存放在永久代中,在JDK 1.8之后值存放在堆中。

    1.9 介绍一下String类的intern方法?

    intern方法是String类的一个实例方法,该方法的作用如下:如果字符串常量池中已经有指向这个字符串的引用,则直接返回该引用;否则先在堆中创建一个字符串对象,再在字符串常量池中创建一个指向该字符串对象的引用,再将该引用返回。
    举例:

    1. public static void main(String args[]) {
    2. // 创建2个对象,str持有的是new创建的对象引用
    3. // 1)驻留(intern)在字符串常量池中的引用指向的在堆中的字符串常量对象
    4. // 2)new 创建的对象(也在堆中)
    5. String str = new String("joonwhee");
    6. // 字符串常量池中已经有了,返回字符串常量池中的引用
    7. String str2 = "joonwhee";
    8. // false,str为new创建的对象引用,str2为字符创常量池中的引用
    9. System.out.println(str == str2);
    10. // str修改为字符串常量池的引用,所以下面为true
    11. str = str.intern();
    12. System.out.println(str == str2);
    13. }

    1.10 内存溢出和内存泄漏的区别?

  • 内存溢出(**OutOfMemory**:指程序在申请内存时,没有足够的内存空间供其使用;

  • 内存泄漏(**Memeory Leak**:指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终会导致内存溢出。

    2、垃圾回收

    2.1 Java的引用类型有哪些?

  • 强引用类型:强引用类型就是传统意义上的引用,即类似Object obj = new Object()这种引用关系。无论何时,只要强引用关系还存在,垃圾收集器就永远不会回收掉被强引用的对象;

  • 软引用类型:只被软引用关联的对象,在系统将要发生内存溢出前,会将这些对象进行第二次回收,如果第二次回收还是没有足够的内存才会抛出内存溢出的异常。
  • 弱引用类型:被弱引用关联的对象只能生存到下一次Yong GC发生为止。当垃圾收集器开始工作,无论当前内存是否充足,都会回收掉只被弱引用关联的对象。
  • 虚引用类型:一个对象是否有虚引用的关联,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象实例,为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

    2.2 如何判断对象是否是垃圾?

  • 引用计数:在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零;

  • 可达性分析:主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

    2.3 有哪些GC算法?

  • 标记-清除算法:先通过可达性分析将需要回收的对象标记出,再将其用垃圾收集器回收。清除算法有两个问题,一是如果向新生代中有大量对象需要清除时,清除对象太多导致效率较低;还有一个问题是容易产生大量不连续的内存碎片,导致需要分配大对象时容易触发Full GC;

  • 标记-复制算法:为了解决内存碎片的问题提出了标记-复制算法,标记-复制算法将内存分成两块,每次仅使用一块,当这块内存用完时,就将这块内存里存活的对象复制到另一块备用的内存中(注意复制后的对应的内存是连续的,就不会有不连续的内存碎片了),然后将之前那块内存清空,这样周而复始来回倒。标记-复制算法虽然较好的地解决了内存碎片的问题,但是可使用的内存空间始终只有一半,内存利用率太低;且当要复制的对象很多是效率会较低。HotSpot虚拟机的Survivor两个区就是用的标记-复制算法,老年代由于对象较多不推荐使用这个算法;
  • 标记-整理算法:标记-整理算法那与标记清除算法类似,但不直接清理可回收的对象,而是让存活的对象都向内存空间的一端移动,然后清理掉边界以外的内存。

    2.4 你知道哪些垃圾收集器?

    参考:https://www.yuque.com/docs/share/4f8f19ff-1057-4d65-89de-23cb8ce4cd11?# 《JVM垃圾回收》

    2.5 介绍一下CMS垃圾收集器?

    参考:https://www.yuque.com/docs/share/4f8f19ff-1057-4d65-89de-23cb8ce4cd11?# 《JVM垃圾回收》

    2.6 介绍一下g1垃圾收集器?

    参考:https://www.yuque.com/docs/share/4f8f19ff-1057-4d65-89de-23cb8ce4cd11?# 《JVM垃圾回收》

    2.7 HotSpot虚拟机内存分配与回收策略?

    1. 对象优先在Eden区分配内存空间

大多数情况下对象在新生代的Eden区分配,当Eden区没有足够空间时将发起一次Minor GC。

  1. 大对象直接进入老年代

大对象指需要大量连续内存空间的对象,典型的是很长的字符串或者数量庞大的数组。HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 间来回复制大对象。

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

虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过 -XX:MaxTenuringThreshold 设置。

  1. 动态对象年龄判定

为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。

  1. 空间分配担保

MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。
如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC。

3、类加载

3.1 JVM一个类的加载过程?

JVM的类加载过程是指从虚拟机外部将类的Class文件加载到内存,并进行验证、准备、解析阶段,完成初始化,最终形成一个可以被虚拟机直接使用的Java类的过程。类加载过程分为7个部分:加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备和解析又叫连接。

  • 加载:根据一个类的全限定名将类的二进制字节流从外部读取到虚拟机内,在元空间生成类的相关信息,在堆内存中生成这个类的Class对象;
  • 验证:验证Class文件中的字节流包含的信息是否符合Java虚拟机规范,保证虚拟机安全;
  • 准备:类变量(static修饰的变量)赋默认初始值,int为0,long为0L,boolean为false,引用类型为null,常量赋正式值;
  • 解析:把符号引用翻译为直接引用;
  • 初始化:执行类的static代码块,并将程序代码中给静态变量的具体赋值进行赋值操作;
  • 使用:使用这个类;
  • 卸载:比如该类的所有实例都已经被gc,JVM中不存在该类的任何实例。

    3.2 继承时父子类的初始化顺序是怎样的?

    初始化部分如下顺序:
  1. 父类—静态变量;
  2. 父类—静态代码块;
  3. 子类—静态变量;
  4. 子类—静态代码块;
  5. 父类—变量;
  6. 父类—初始化代码块;
  7. 父类—构造器;
  8. 子类—变量;
  9. 子类—初始化代码块
  10. 子类—构造器。

    3.3 什么是类加载器?

    在类加载阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的代码称为“类加载器”,ClassLoader,类加载器可以是C++语言实现,也可以是Java语言实现,且该动作可以自定义实现。

    3.4 JVM有哪些类加载器?

  • BootStrap ClassLoader:根类加载器,使用C++语言实现,加载位于/jre/lib目录中的或者被参数-Xbootclasspath所指定的目录下的Java核心类库,是虚拟机自身的一部分;
  • Extension ClassLoader:拓展类加载器,使用Java语言实现,独立存在于虚拟机外部,并且全部都继承子抽象类java.lang.ClassLoader,加载位于/jre/lib/ext目录中的或者java.ext.dirs系统变量所指定的目录下的拓展类库;
  • Application ClassLoader:应用程序类加载器,使用Java语言实现,加载用户路径(ClassPath)上所指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下Application ClassLoader就是程序中默认的类加载器。比如在程序中我们自定义一个类MyClass类,该类就是由Application ClassLoader加载的。

    3.5 JVM不同的类加载器加载哪些文件?

  • BootStrap ClassLoader:根类加载器,加载位于/jre/lib目录中的或者被参数-Xbootclasspath所指定的目录下的Java核心类库,由C++实现,是虚拟机自身的一部分;

  • Extension ClassLoader:拓展类加载器,加载位于/jre/ext/lib目录中的或者java.ext.dirs系统变量所指定的目录下的拓展类库;
  • Application ClassLoader:应用程序类加载器,加载用户路径(ClassPath)上所指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下Application ClassLoader就是程序中默认的类加载器。比如在程序中我们自定义一个类MyClass类,该类就是由Application ClassLoader加载的;由Maven或者Gradle加载进来的第三方jar包也是由Application ClassLoader加载的。

    3.6 什么是类加载的双亲委派模型?

    JVM面试题 - 图3
    上图中的层次关系,也被称为加载器的双亲委派模型。双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载 。
    双亲委派模型总结为两点:

  • 向上逐层询问是否已加载;

  • 向下逐层尝试是否可加载。

双亲委派模型在抽象类ClassLoader中的protected方法loadClass()中被实现。

3.7 为什么设计双亲委派模型?

总结一下三点原因:

  • 确保安全,避免Java核心类库被修改。毕竟是BootStrap ClassLoader加载Java核心类库,在双亲委派模型下用户自定义的加载器无法加载Java核心类库;
  • 保证类的唯一性。用户自己编写了一个名称为java.lang.Object的类,并放在程序的ClassPath中,这个自定义的Object类将有ApplicationClassLoader加载到JVM中,那么系统中会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

    3.8 什么场景下需要破坏双亲委派模型?为什么要破坏?

    以Tomcat的多web应用程序为例,需要破坏双亲委派模型。
    原因是双亲委派模型不能满足Tomcat容器部署多web应用程序的场景。我们知道 Tomcat 容器可以同时部署多个 Web 应用程序,多个 Web 应用程序很容易存在依赖同一个 jar 包,但是版本不一样的情况。例如应用1和应用2都依赖了 spring ,应用1使用的 3.2. 版本,而应用2使用的是 4.3. 版本。此时在双亲委派模型下Tomcat无法解决上述问题。

    3.9 Tomcat如何解决一个容器内部署多个web程序的依赖加载问题?

    Tomcat 的类加载器如下图所示:
    image.png
    说明:

  • Bootstrap ClassLoader:对应双亲委派模型中的Bootstrap ClassLoader + Extension ClassLoader;

  • System ClassLoader:对应双亲委派模型中的Application Classloader;
  • Common ClassLoader:主要包含一些通用的类,这些类对 Tomcat 内部类和所有 Web 应用程序都可见;
  • WebappX ClassLoader:Tomcat 为每个部署的 Web 应用程序创建一个单独的类加载器,这样保证了不同应用之间是类加载是隔离的。

正是因为Tomcat会为部署的每一个app创建一个WebappX ClassLoader,由每一个appde WebappX ClassLoader去加载不同版本的依赖,解决上述问题。

3.10 如何打破双亲委派模型?

当我们想要自定义一个类加载器,并且想打破双亲委派模型时,可以继承ClassLoader类,覆写loadClass()方法。

3.11 如何自定义自己的类加载器?

可以通过继承ClassLoader类,通过覆写findClass方法或者loadClass方法,将自定义的逻辑在上面两个覆写的方法中实现。注意如果覆写loadClass方法,会打破类加载的双亲委派模型,因为在ClassLoader类的loadClass方法里先实现了双亲委派的逻辑,然后执行的findClass方法;如果不想打破JVM类加载的双亲委派模型,可以通过将自定义的类加载的逻辑覆写在findClass方法里。

3.12 ClassLoader类中的loadClass()、findClass()、defineClass()有什么区别?

  • loadClass():是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;
  • findClass():根据名称或者位置加载.class字节码;
  • defineClass():是个native方法,把字节码转换成java.lang.Class对象。

补充: