【java面试03-JVM】

1. Java 内存区域(运行时数据区)

JDK 1.8 之前:
在这里插入图片描述
JDK 1.8 :
在这里插入图片描述

我们在IDE中编写的Java源代码被javac编译器编译成.class的字节码文件,然后由Class Loader负责将这些class文件给加载到JVM中去执行。
在这里插入图片描述

在这里插入图片描述

2.类装载器

作用:加载.class文件 new Student(); 这个new出来的实例的引用(实例的名字)在栈中,具体的学生在堆中。
在这里插入图片描述

在这里插入图片描述

3.双亲委派机制

  1. 类加载器收到类加载的请求;
  2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器;
  3. 根加载器检查是否能够加载当前这个类,能加载就使用当前的加载器,否则,抛出异常,通知子加载器进行加载;
  4. 重复3
JVM中提供了三层的ClassLoader:

Bootstrap classLoader:(启动类加载器)主要负责加载核心的类库(java.lang.*等),rt.jar包,构造ExtClassLoader和APPClassLoader;
ExtClassLoader:(扩展类加载器)主要负责加载jre/lib/ext目录下的一些扩展的jar;
AppClassLoader(应用类加载器):主要负责加载应用程序的主函数类。
在这里插入图片描述
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了:

为什么要设计这种机制?

这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
原文链接

4.沙箱安全机制(了解)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.native关键字

凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层C语言的库。会进入本地方法栈,调用本地方法接口(JNI,Java native interface)。它在内存区域中专门开辟了一块标记区域:本地方法栈,用于登记native方法,在最终执行的时候通过JNI加载本地方法库。
JNI作用:扩展Java的使用,融合不同编程语言为Java所用,最初为了融合C和C++。

6.栈

7.堆

参考1:堆内存划分
参考2:垃圾回收机制
一个JVM只有一个堆,堆内存大小是可以调节的。
类加载器读取了类文件后,一般把 类、方法、常量、变量、引用类型的真实对象 放到堆中。
堆内存中分为三个区域:新生区(伊甸园区、幸存0区、幸存1区)、养老区、永久区。
新生区:类诞生、成长、或死亡的地方。所有对象都是从伊甸园区new出来的。
永久区:这个区域常驻内存,用来存放JDK自身携带的Class对象、interface元数据,存储的是Java运行时的环境。这个区域不存在垃圾回收,关闭虚拟机就会释放该区域的内存。
清理垃圾清理的是伊甸园区和养老区的内容。
当新生区和养老区都满了,也就是堆内存满了,就会报OOM错(outofmenory)。
默认分配总内存是电脑内存的1/4,初始化内存是电脑内存的1/64。
在这里插入图片描述
方法区中的小方块是常量池。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC(轻GC),清理年轻代内存空间。
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC(重GC)。
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
在这里插入图片描述
在这里插入图片描述
参考

GC中的算法

标记清除法,标记清除压缩法,复制算法(用于新生区),引用计数法

一、引用计数法

堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。(所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就是不可能再被使用的。)

优点:
  1. 可即时回收垃圾:在该方法中,每个对象始终知道自己是否有被引用,当被引用的数值为0时,对象马上可以把自己当作空闲空间链接到空闲链表。
  2. 最大暂停时间短。(最大暂停时间指 因执行GC而暂停执行程序所需的时间。)
  3. 没有必要沿着指针查找。
缺点:
  1. 计数器值的增减处理非常繁重
  2. 计算器需要占用很多位。
  3. 实现繁琐。
  4. 循环引用无法回收。(无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。)
二、标记-清除法

标记-清除算法采用从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如图所示。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。(该算法分为标记和清除两个阶段。标记就是把所有活动对象都做上标记的阶段;清除就是将没有做上标记的对象进行回收的阶段。)
在这里插入图片描述

优点:
  1. 实现简单
缺点:
  1. 碎片化:如上图所示,在回收的过程中会产生被细化的分块,到后面,即使堆中分块的总大小够用,但是却因为分块太小而不能执行分配。
  2. 分配速度:因为分块不是连续的,因此每次分块都要遍历空闲链表,找到足够大的分块,从而造成时间的浪费。
三、 标记-清除-压缩法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。(标记-压缩算法与标记-清理算法类似,只是后续步骤是让所有存活的对象移动到一端,然后直接清除掉端边界以外的内存。)

优缺点:该算法可以有效的利用堆,但是压缩需要花比较多的时间成本。

在这里插入图片描述

四、复制算法

复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。
在这里插入图片描述

优点:
  1. 优秀的吞吐量
  2. 可实现高速分配:复制算法不用使用空闲链表。这是因为分块是连续的内存空间,因此,调用这个分块的大小,只需要这个分块大小不小于所申请的大小,移动指针进行分配即可。
  3. 不会发生碎片化。
  4. 与缓存兼容。
缺点:
  1. 堆的使用效率低下。
  2. 不兼容保守式GC算法。
  3. 递归调用函数。

参考
参考
在这里插入图片描述

垃圾收集器回收垃圾的第一步先要确定哪些对象是可以被回收的。因此,JVM会扫描堆内存中的所有对象,并标记出可被回收的对象。

垃圾收集的标记算法有以下两种:

  1. 引用计数算法

引用计数算法通过在每个对象中添加一个计数器,当有一个地方引用它的时候计数器的值就会增加1;当引用失效的时候计数器的值则会减1。当计数器的值为0时,则可认为这个对象已经不再使用。因此对于引用计数算法,垃圾收集器只需要回收计数器为0的对象即可。

引用计数算法的优点是效率很高,不需要遍历所有对象。但它是存在一个致命的缺点,即无法解决对象之间循环引用的问题。比如对象A引用了对象B,对象B也引用了对象A,除此之外,A、B两个对象再也没有被其他地方引用。此时对象A和对象B的计数器均不为0,所以A、B两个对象都无法被回收。所以,目前商用的Java虚拟机都没有选用引用计数算法来进行标记。

  1. 可达性分析算法

可达性分析算法也被称为根搜索算法。这一算法的基本思路是用一系列的“GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为”引用链“(Reference Chain)。如果一个对象到”GC Roots"没有任何的引用链相连,则证明此对象可能不再被使用。

如下图所示,灰色部分的对象没有关联到引用链上,此时这些对象就会被判定为可回收对象。
在这里插入图片描述

垃圾收集算法

  1. 标记-清除算法(Mark-Sweep)
  2. 标记-复制算法(Copying)
  3. 标记-整理算法(Mark-Compact)
JAVA有了GC同样会出现内存泄漏
  1. 静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
  2. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  3. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

GC:分代收集算法

对于年轻代,存活率低,适合采用复制算法。
对于老年代,标记清除(内存碎片不多的时候)+标记压缩混合实现。

常见的垃圾收集器【下面一张图是HotSpot虚拟机包含的所有收集器】

在这里插入图片描述

参考这篇文章

一、串行垃圾回收器

在JDK1.3之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU或一个手机线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它效率高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。

串行的垃圾收集器有两种,Serial和Serial Old,一般两者搭配使用。

新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。Client应用或者命令行程序可以通过

-XX:+UseSerialGC开启串行垃圾回收器。

二、并行垃圾回收器

并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW)。适合Server模式以及多CPU环境。一般会和JDK1.5之后出现的CMS搭配使用。并行的垃圾回收器有以下几种:

ParNew:Serial收集器的多线程版本,默认开启的收集线程数和CPU数量一样,运行数量可以通过修改ParallelGCThreads设定。用于新生代手机,复制算法。用-XX:+UseParNewGC,和Serial Old收集器组合进行内存回收。

Parallel Scavenge: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用CPU时间,尽快完成程序的运算任务可以升值最大停顿时间MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。如果设置了-XX:+UseAdaptiveSizePolicy参数,则随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量。用于新生代收集,复制算法。通过-XX:+UseParallelGC参数,Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。

Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6开始提供的。在此之前Parallel Scavenge的地位也很尴尬,而有了Parllel Old之后,通过-XX:+UseParallelOldGC参数使用Parallel Scavenge + Parallel Old器组合进行内存回收。

三、CMS收集器

这个讲的很清楚
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,会"Stop The World"。
  2. 并发标记:GC Roots Tracing,可以和用户线程并发执行。
  3. 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
  4. 并发清除:清除对象,可以和用户线程并发执行。
    由于垃圾回收线程可以和用户线程同时运行,也就是说它是并发的,那么它会对CPU的资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,并发回收是垃圾收集线程就不会少于25%,而且随着CPU减少而增加,这样会影响用户线程的执行。而且由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
    所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。

四、G1垃圾收集器

1、G1垃圾收集器
把G1单独拿出来的原因是其比较复杂,在JDK 1.7确立是项目目标,在JDK 7u2版本之后发布,并在JDK 9中成为了默认的垃圾回收器。通过“-XX:+UseG1GC”启动参数即可指定使用G1 GC。

G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。这样就意味者它空间整合做的比较好,因为不会产生空间碎片。G1还是并发与并行的,它能够充分利用多CPU、多核的硬件环境来缩短“stop the world”的时间。G1还是分代收集的,但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的ygc,全堆扫描的full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC。G1还可预测停顿,通过调整参数,制定垃圾收集的最大停顿时间。

G1收集器的运作大致可以分为以下步骤:初始标记、并发标记、最终标记、筛选回收。

  1. 初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要STW,但耗时很短。
  2. 并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行。
  3. 最终标记阶段则是为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记需要把Remembered Set Logs的数据合并到Remembered Sets中,这阶段需要暂停线程,但是可并行执行。
  4. 筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。G1收集器运行示意图如下图所示。
    在这里插入图片描述
    2、G1分区的概念

看这个

对象死亡(被回收)前的最后一次挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
  第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
  第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。
  第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

方法区如何判断是否需要回收

方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

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

8. jprofiler分析OOM错误

参考

9. GC(garbage Collection)

两类GC:重GC、轻GC

10. JMM(Java Memory Model,java内存模型)

参考
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。

JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
在这里插入图片描述

内存划分

参考
JMM规定了内存主要划分为主内存和工作内存两种。
此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
在这里插入图片描述
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

11. 类加载过程

五个步骤 加载 验证 准备 解析 初始化

在这里插入图片描述

  1. 加载:将外部class文件转化为二进制流 加载到java虚拟机 存储到方法区内
  2. 验证:确保加载进来的class文件包含的信息符合java虚拟机的要求
  3. 准备:为类变量分配内存 设置类变量的初始值
  4. 解析:将常量池内符号引用转为直接引用
  5. 初始化
    参考
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值