JVM面试个人总结

x33g5p2x  于2022-03-18 转载在 Java  
字(6.4k)|赞(0)|评价(0)|浏览(328)

JVM 的内存模型以及分区情况和作用

线程共享:

  • 方法区:用于存储虚拟机加载的类信息,常量,静态变量等数据。
  • 堆内存:堆内存主要用于分配对象的存储空间,所有的对象和数组都要在堆上分配。堆内存又可细分为新生代、幸存区、老年代三部分。

线程私有:

  • 虚拟机栈:java执行方法的内存模型。主要存放8种基本类型的变量、对象的引用变量、操作数栈、动态链接、方法出口等信息。
  • 本地方法栈:本地方法栈的功能和特点类似于虚拟机栈,不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。
  • 程序计数器:jvm中最小的一块区域,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM 预定义的类加载器有哪几种?

启动类加载器(Bootstrap)
扩展类加载器(Extension)
应用程序类加载器(Application)

  • 启动类加载器:加载jdk最原始的类,jre\lib\rt.jar下的类
  • 扩展类加载器:加载jdk升级过程中扩展的类,jre\lib\ext目录下的类
  • 应用程序类加载器:加载当前应用的classpath的所有类

什么是双亲委派机制?

如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把请求委托给父加载器去完成,依次向上;只有当父加载器没有找到所需的类时,子加载器才会尝试去加载该类。

作用:保证了Java的出厂源码不会受到开发人员编写的污染。

类加载的过程?

【请你谈谈Java的类加载过程】

字节码文件中包含哪些内容?

  • 版本号信息
  • 静态常量池(符号常量)
  • 类相关的信息
  • 方法相关的信息
  • 字段相关的信息

大部分信息都是通过常量池中的符号常量来表述的。

怎么理解常量池?

在Java中,常量池包括两层含义:

  • 静态常量池,它是class文件中的一个部分,里面保存的是类相关的各种符号常量。
  • 运行时常量池, 其内容主要由静态常量池解析得到,也可以由程序添加。

什么是内存溢出?

内存溢出是指可用内存不足。

程序运行需要使用的内存超出系统当前所能提供的最大可用值,如果不进行处理就会影响到其他进程,所以现在操作系统的处理办法是:只要超出立即报错,比如抛出内存溢出错误(OOM) 。

什么是内存泄漏?

某些对象不会再被程序用到了,但是垃圾回收器又不能回收不了他们,这种情况叫做内存泄漏

举些例子:

  • 一些提供close方法的资源对象,我们在使用完之后没有调用close方法对其进行关闭就会导致内存泄漏,如数据库连接、IO连接、网络连接等
  • 如果在匿名内部类中创建静态实例也会造成的内存泄漏

内存泄漏和内存溢出有什么关系?

如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
内存泄漏一般是资源管理问题和程序BUG,内存溢出则是内存空间不足和内存泄漏的最终结果。

常用的JVM启动参数有哪些?

# JVM启动参数不换行
# 设置堆内存 
‐Xmx4g ‐Xms4g 
# 指定GC算法 
‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50 
# 指定GC并行线程数 
‐XX:ParallelGCThreads=4 
# 打印GC日志 
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps 
# 指定GC日志文件 
‐Xloggc:gc.log 
# 指定Meta区的最大值 
‐XX:MaxMetaspaceSize=2g 
# 设置单个线程栈的大小 
‐Xss1m 
# 指定堆内存溢出时自动进行Dump 
‐XX:+HeapDumpOnOutOfMemoryError 
‐XX:HeapDumpPath=/usr/local/

我知道的其实只有三个

  • ‐Xms:设置堆内存的初始大小
  • -Xmx:设置堆内存的最大值
  • -XX:MaxMetaspaceSize:设置元空间内存的最大值

什么是垃圾

垃圾是指在 运行程序中没有任何指针指向的对象, 这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

垃圾判断算法

在堆里存放着几乎所有的Java对象实例,在GC(垃圾回收器)执行垃圾回收之前,首先需要区分出内存中哪些是存活对象(有用对象),哪些是死亡对象(垃圾对象)。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段

那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时就可以宣告为已经死亡

判断对象存活一般有两种方式:引用计数算法 和 可达性分析算法

标记阶段:引用计数算法

引用计数算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况

优点实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点

  • 需要单独的字段存储计数器,这样的做法增加了存储空间的开销
  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
  • 引用计数器有个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法
    循环引用现象

标记阶段:可达性分析算法

可达性分析特点:

  • 相较于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数法中循环引用的问题,防止内存泄漏的发生
  • 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集

基本思路:

  • 可达性分析是以跟对象集合(GC Roots)为起始点,按从上至下的方式搜索,能与GC Roots直接或间接连接的对象就是存活的对象,也叫做可达对象
  • 搜索所过的路径称为引用链(Reference Chain),如果一个对象没有和任何引用链相连,则是不可达的,就认为该对象为垃圾对象


问题来了,哪些对象可被称为GC Roots对象呢?或者说Java中,GC Roots包含哪几类对象呢?

  • 虚拟机栈中引用的对象,比如:Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量等
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象,比如:字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用。基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器。

注意:

  • 使用可达性分析算法时,分析工作必须在一个能保障一致性的快照中进行。 这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称几乎不会停顿的CMS垃圾回收器中,枚举根节点时也是必须要停顿的

垃圾清除算法

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收、释放掉垃圾对象所占用的内存,以便有足够的可用空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾回收算法是:

  • 标记 - 清楚算法(Mark - Sweep)
  • 复制算法(Copying)
  • 标记 - 压缩算法(Mark - Compact)

清除阶段:标记-清除算法

标记-清除(Mark - Sweep)算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:

  • 标记:从GC Roots开始遍历,标记所有被引用的对象。一般是在对象头中记录是否是可达对象
  • 清除:对堆内存从头到尾遍历,如果发现某个对象的对象头中没有标记为可达对象,则将其回收

优点:

  • 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效

缺点:

  • 标记和清除过程的效率都不算高
  • 这种方法需要使用一个空闲列表空闲列表记录哪些内存是没有被占用状态,空闲的来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量
  • 标记清除后会产生大量不连续的内存碎片

清除阶段:标记-整理算法

标记-整理分为“标记”和“整理”两个阶段:

  • 标记:和标记清除算法一样,从GC Roots开始标记所有被引用的对象
  • 整理将所有的存活对象压缩到内存的一端,按顺序排放之后清理外边界的空间(清理垃圾)

标记-整理算法的最终效果等同于标记-清除算法执行后,再进行一次内存碎片整理,因此也可以把它称为标记-清除-压缩算法

可以看到,标记的存活对象将会被整理,按照内存地址依次排序。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销

优点:

  • 消除了标记-清除算法中,内存区域分散的缺点(内存碎片)

缺点:

  • 移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。即STW

清除阶段:复制算法

核心思想:
将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中之后清除正在使用的内存块中的对象,交换两个内存的角色,最后完成垃圾回收


对于这种算法来说,如果存活的对象过多的话则要执行较多的复制操作,效率会变低,因此它适合存活率较低的情况。事实上在年轻代中就是使用的复制算法

优点:

  • 没有标记和清除的过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题

缺点:

  • 需要两倍的内存空间,比较浪费
  • 如果存活对象较多,那么复制操作就比较多,效率相对会降低

对比三种清除算法

Mark-SweepNark-CompactCopying
速度中等最慢最快
空间开销少(但有内存碎片)少(没有内存碎片)需要额外的一半内存开销
移动对象

从效率来说,复制算法是当之无愧的老大,但是却浪费了太多内存

而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除算法多了一个整理的阶段

分代收集

通俗的理解java对象的这一辈子
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

分代收集过程

  • 新生代:几乎所有新生成的对象首先都是放在年轻代的(对象过大会被分配到老年代)。新生代内存按照 8:1:1 的比例分为一个 Eden(伊甸园区) 和两个 Survivor(幸存区),幸存区一个称为“From”区、一个称为“To”区,名字是动态的(谁空谁是“to”)。当新对象生成,Eden存满了空间不足,则会发起一次 Minor GC。回收时先将 Eden 区存活对象复制到一个 From区,然后清空 Eden 区。再次触发Minor GC时(Eden园满了才会触发),则将 Eden 区和 From区的存活对象复制到另一个 To区,然后清空 Eden 和这个 From区,此时 From区是空的,然后将 From区和 To区交换(谁空谁是To区), 如此往复。当 To区不足以存放 Eden 和 From的存活对象时,就将存活对象直接存放到老年代。当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1默认情况下,如果对象年龄达到 15 岁,就会移动到老年代中。若是老年代也满了就会触发一次 Full GC,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制 Eden 和 Survivor 的比例。(垃圾清除算法用的是复制算法
  • 老年代:在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和 JVM 的相关参数。(垃圾清除算法用的是 标记清除或者标记整理

强软弱虚引用

强引用: 表示一种比较强的引用关系,只要还有强引用对象指向一个对象,那么表示这个对象还活着(GC Roots可达),垃圾收集器宁可抛出OOM异常,也不会回收这个对象。

软引用: 软引用用于关联一些可有可无的对象例如缓存当系统内存充足时,这些对象不会被回收当系统内存不足,将要发生内存溢出之前,就会回收这些对象(即使这些对象GC Roots可达),如果回收完这些对象后内存还是不足,就会抛出OOM异常

弱引用: 被弱引用关联的对象只能生存到一下次垃圾回收之前。当垃圾收集器工作时,无论内存空间是否充足,都会回收掉被弱引用关联的对象。ThreadLocal中就使用了WeakReference来避免内存泄漏。

虚引用: - 形态虚设,如果一个对象仅持有虚引用,那么它**就和没有任何引用一样,在任何时候都可能被垃圾回

说一下你知道的垃圾回收器?

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:CMS、Serial Old、Parallel Old
  • 整堆收集器: G1

Java8默认使用的垃圾收集器是什么?

jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)

Parallel Scavenge 收集器:
属于新生代收集器,是并行的多线程收集器,采用复制算法**。
该收集器还有个特点就是GC自适应调节策略,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Serial Old 收集器:

属于老年代收集器,是单线程收集器,采用标记-整理算法

Java9之后,官方JDK默认使用的垃圾收集器是G1。

CMS收集器

一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记-清除算法实现。并发收集、低停顿。

  • 初始标记:STW,单线程,因为是从GCRoot寻找直达的对象,速度快算
  • 并发标记:与应用线程一块儿运行,是CMS最主要的工做阶段,经过直达对象,扫描所有的对象、进行标记
  • 重新标记:STW,因为并发标记阶段用户线程也在运行,会产生对象引用的修改。因此该阶段重新扫描之前并发标记阶段所有残留更新的对象。多线程,速度快,须要全局停顿并发
  • 并发清除:与应用程序一块儿运行,采用标记清除算法。为什么是标记清除而不是标记整理呢?因为CMS主要关注低延迟,如果使用标记整理算法,那么在设计对象的移动时就不得不发生STW,这样的话停顿时间又会变长,延迟变大。

什么是GC停顿,STW?

因为GC过程中,有一部分操作需要等所有应用线程都到达安全点、暂停之后才能执行,这时候就叫做Stop-the-world。除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。

哪些可以当做安全点:
安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“ 是否具有让程序长时间执行的特征” 为标准。比如: 选择一些执行时间较长的指令作为safe Point ,如:

  • 循环的末尾
  • 方法返回前
  • 调用方法之后
  • 抛异常的位置

相关文章