JVM

1、说说JVM 内存分哪几个区,每个区的作用是什么?

在这里插入图片描述

​ JVM在执行java程序时有若干数据区:

  • 程序计数器(类似PC)

  • 本地方法栈(存native方法的数据)

  • 方法区(类信息,常量,静态变量,可以不GC)

  • 虚拟机栈(栈帧形式,Java方法的数据,局部变量表)

  • 堆(存对象的地方,GC的主要位置)

2、Java堆内存都是线程共享的嘛?

​ 为了保证线程安全性,在线程初始化时,虚拟机会为每个线程在堆中分配一块TLAB空间,只给当前线程使用。

​ 对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是在eden还是在老年代

3、对象有几种创建方法?创建过程是怎么样的(内存分配)

  • 对象的创建有五种:

(1)最常见的就是new一个对象,拓展:调用类的静态方法、或者构建方法来实例化。

(2)通过反射创建(3)用一个类来克隆 (4)反序列化成对象:网络和文件中获取一个二进制流然后反序列化成对象(5)第三方库来创建

  • 对象的创建过程:

(1)先检查对象的类是否被加载、解析过,如果没有就先进行类加载。

(2)为对象分配空间,参造二,保证线程安全,先尝试在TLAB上进行分配, 然后再在堆上分配(判断是在老年代还是eden区)

两种方式分配内存,连续内存“指针碰撞”,不连续则维护空闲列表。

(3)对象初始化,进行对象信息赋值,因为内存分配时为0。

4、说一说对象的访问定位吧,他们各自的优点

​ 对象的访问定位方式有两种:句柄访问直接指针访问

  1. 句柄:引用指向句柄池两个指针,分别指向方法区中对象类型,堆中的实例化数据

  2. 直接指针:引用直接指向堆中的实例化数据 + 对象类型指针,实例化数据中包含了指向方法区中类型的指针

  1. 各自的优点:

(1)句柄访问,稳定,因为对象移动时,引用本身不需要更改,利于GC。

(2)直接指针访问,简单速度快,节省了定位的开销。

5、说一说Java 类加载过程?以及什么时候类加载?

  • 类加载过程:

​ 类的加载被分为了加载–>验证–>准备–>解析初始化几个阶段:

​ 加载主要是通过全限定类名生成Class文件Class文件按照JVM设定的格式存储在方法区。验证是验证Class文件是否符合JVM的规范。准备是为类变量等分配内存,设置零值。解析时将符号引用转换为直接引用(通过全限定类名去找,com那些包里面的)。然后最后进行初始化赋值。

  • 时机:

​ 时机:类加载的时机是由虚拟机自定义实现的,但初始化过程的触发一般源于以下情况:

  • 使用new实例化对象时
  • 读取或设置一个静态字段(final除外),调用静态方法
  • 反射调用类
  • 初始化子类时,会先初始化其父类
  • 虚拟机启动会执行主类

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

​ 类加载器是作用于类加载的加载动作。判断两个类是否相等,相等的前提条件:两个类由同一个类加载器加载。

​ 类加载器有启动类加载器(加载核心类库下的),扩展类加载器,系统类加载器也叫应用程序类加载器(默认使用的加载器)。

​ 自定义加载器的父类加载器是应用程序类加载器,但类加载器之间的父子关系不是使用继承来实现的,而是使用组合关系来复用父加载器的代码。

7、说说类加载器双亲委派模型机制?有什么作用?

​ 一个类加载器收到一个类加载的请求,它不会自己去尝试加载这个类,它会调用自己的loadClass方法把请求向父类加载器委派,每一层次都是如此,所以最终所有类加载请求都会委派到启动类加载器。当父类加载器搜索不到相应类时,才会反馈自己无法完成请求,子类加载器才会调用findclass方法去加载。

​ 作用的话我觉得:

  • 保证同一层级的类都由同一类加载器加载,避免有些类被重复加载。
  • 保护程序的安全,防止核心API被随缘篡改。

​ 当我们自己写java包下的类时,并打包到同样的包下,由于双亲委派模型的原因,这个类不会被加载,因为已经有一个一样的类被加载了,这时会返回异常。

8、简述Java内存分配

​ 对象创建那里提到过,Java的内存分配主要是:

(1)为了线程安全性,先在TLAB分配内存空间,因为是线程独有,不够再到堆上分配

(2)堆上分配优先在Eden区分配,放不下就进行GC,再放大对象在老年代分配内存,避免Eden区耗费太快而频繁GC。

长期存放在新生代的对象也会分配到老年代中(维护一个年龄计数器,每次GC就加1)

9、简述 Java 垃圾回收机制

​ 我对于垃圾回收的理解,垃圾是程序中没有指针指向的对象,JVM会自动的进行垃圾的回收来更好的进行内存分配。

​ 垃圾回收的意义在于降低了系统内存泄露和内存溢出的风险。

​ 垃圾回收主要是JVM 通过特定的垃圾回收器底层通过垃圾回收算法 进行垃圾回收。

10、在 Java 中,对象什么时候可以被垃圾回收?

​ 对象被GC有两种判定方法,引用计数器 + 可达性分析,但是JVM都是使用的是后者,因为引用计数不能回收循环引用问题。

​ 可达性分析,从GC Root系列节点开始,根据引用关系向下搜索。若从GC Roots到某个对象不可达,则该对象可回收。

  • GC Root对象

(1)虚拟机栈中引用的对象,本地方法栈引用的对象。

(2)方法区的静态属性引用对象、常量对象。

11、finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?

​ finalize() 方法在对象被GC之前调用,每个对象的finalize方法只能执行一次,该方法执行代价大,一般不使用。

目的:一般用于对象回收前资源释放,和对象的自救(重新建立引用连接)。

12、如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

不会立即释放对象占用的内存。 只是断开了栈帧中的引用关系,只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,执行finalize方法,如果没有恢复引用这时候才会清除对象内存。

13、GC触发的类型,分别在什么时候发生?

​ GC就分为两种,一个是Minor GC,一种是Full GC。

(1)Minor GC:针对新生代的回收,当Eden区域空间不足时触发

(2)Full GC:针对老年代和方法区的回收,当老年代和方法区空间不足时;新生代中创建大对象直接扔到老年代,老年代存不下时;主动调用System.gc时;

CMS并行回收,预留空间不足,放入到了老年代中,老年代空间也不足;

14、GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分代收集理论如何实现?

(1)标记-清除:标记回收对象后逐个回收,标记和清除效率都低,且产生很大碎片

(2)标记整理:标记后,将对象往前移动,效率低,但回收到连续空间,不会碎片

(3)复制:每次分配内存只使用Eden和一块Survivor,发生垃圾收集时,将存活的对象复制到另一块Survivor中,清除Eden和使用过的Survivor。

当Survivor中的空间不足以容纳一次Minor GC的存活对象时,需要依赖老年代区域进行分配担保(即多余的存活对象进入老年代区)。

高效,但是占用空间大,需要两倍空间

(4)分代收集:分老年代和新生代,并进行实际物理分块。结合复制8:1:1,老年代担保。

  • 年轻代:对象比较短,生命周期短,使用复制算法比较好(Eden Survivor0 复制到 Survivor1)
  • 老年代:使用标记-清除-整理结合,平时使用标记-清除算法,当内存碎片化过于严重,以致影响对象分配时,使用一次标记-整理算法消除碎片化空间。

15、GC 收集器有哪些?CMS 收集器与 G1 收集器的特点

在这里插入图片描述

​ 比较经典的一些垃圾收集器

  • Serial收集器:复制算法,回收时暂停所有线程;parNew收集器:多线程版本的Serial,cpu数量有影响;Parallel Scavenge收集器:多线程,复制算法,更注重于达到一个可控制的吞吐量,吞吐量优先。
  • Serial Old收集器:标记-整理算法,单线程,(作用:java5及之前搭配Parallel Scavenge;作为CMS后备) ;Parallel Old收集器:标记-整理算法,Parallel Scavenge的老年代版本,因为搭配Serial Old拖累性能,java6产生,搭配Parallel Scavenge用于吞吐量和cpu资源敏感(防止cpu使用过高)的地方。

​ 并发垃圾收集器:垃圾回收与用户线程并发执行

  • CMS:老年代的收集器,标记-清除算法,分四步,两步执行时间较长的步骤(并发标记,并发清除)运行工作线程一起执行,但只能搭配Serial收集器或者parNew收集器。特点:并发操作使得程序变慢;并发阶段产生新的浮动垃圾;因为是标记-清除算法,使得碎片化,出现大对象无法分配从而Full GC,进行碎片整理。

初始标记:标记GC Roots直接关联到的对象,这个过程要使得所有工作线程停顿

并发标记:进行GC Roots 整个对象可达性标记,耗时所以和工作线程并发运行

重新标记:工作线程的运行会导致标记发生变动,这一步用于修正标记。

并发清除:开始垃圾回收,可以与工作线程一起执行(所以是标记-清除)

  • G1:不区分年轻代和老年代,以Region为最小回收单位,通过跟踪每个Region的回收价值大小(即回收所获得空间和回收所需时间的经验值),并维护一个优先级列表。优先回收价值高的Region,流程前三步与CMS相同,最后一步筛选回收。

筛选回收:需暂停用户线程,根据优先级来逐个进行区域的回收。(局部复制,整体又是压缩)

使用标记-整理算法,回收时不会产生可用空间零碎分布的现象。

16、JVM 的永久代中会发生垃圾回收么?

​ 永久代:方法区的一种实现。

类的实例化对象都被回收,或者该类的classloader对象被回收,永久代也会被进行垃圾回收。

17、System.gc() 和 Runtime.gc() 会做什么事情?

方法 System.gc() 是调用Runtime.gc() 方法的一种传统而便捷的方式,因为它是一个静态的方法,不用先获取Runtime实例。实质上是调用Runtime.getRuntime().gc()。

18、能说说Java 中会存在内存溢出和内存泄漏的原因?

  • 内存溢出(OOM)

    ​ 没有空闲内存,并且垃圾收集器GC后也无法提供更多的内存了,会尝试回收软引用对象

    原因:(1)堆内存设置的不够,也可能有内存泄露问题

    ​ (2)创建大量的大对象,并且没有被回收。

  • 内存泄露

    生命周期的对象持有短生命周期的引用,换句话说只有对象不会再被程序用到,并且GC不会回收到它们,才叫做内存泄露。

    对象的生命周期很长(比如局部变量,设置成Static变量)就有可能出现内存泄露。

举例(面试):

  1. 单例模式:

    单例对象的生命周期同应用程序一样长,如果它有外部引用的话,则会导致内存泄露。

  2. 一些提供了close()的资源未使用close():

    例如:数据库连接,使用dataSource.getConnection(),网络连接Socket 和 IO流未手动close(),否则都无法回收。

  3. 集合容器:

    集合容器使用完后,未置空引用,则会出现这种情况。