> 文章列表 > JVM相关面试题

JVM相关面试题

JVM相关面试题

目录

内存区域划分

 本地方法栈

程序计数器

虚拟机栈

堆区

元数据区

类加载

加载过程

双亲委派模型

垃圾回收机制 GC

 GC实际工作过程

1.找到垃圾/判定垃圾

1)引用计数(不是Java的做法,python/php)

2)可达性分析(Java的做法)

2.进行对象的释放

1.标记清除

2.复制算法

3.标记整理

分代回收


关于jvm,我们围绕jvm内存区域划分,jvm类加载机制,jvm垃圾回收机制这三个部分展开.

内存区域划分

jvm在启动的的时候,会申请到一整个很大的内存区域.jvm是一个应用程序,要从操作系统这里申请内存,申请完内存之后,jvm就会把整个空间,分成几个部分,每个部分各自有不同的功能部分.

 本地方法栈

native 就表示jvm内部的c++代码,这一块区域就是给调用native方法(jvm内部的方法)准备的栈空间.

程序计数器

记录当前线程执行到哪个指令了,是很小的一块,存一个地址,每个线程独有一份程序计数器.

虚拟机栈

此区域是给Java代码使用的栈,这里的栈不是只有一个,有很多个,每个线程有一个.

stack在之前的数据结构也学过,数据结构中的栈,是"先进后出"的.但是要注意的是,此处的栈,和数据结构的栈,并不是一个东西.此处所说的栈,是jvm中的一个特定空间,数据结构中的栈

对于jvm虚拟机栈,这里存储的是方法之间的调用关系.整个栈空间内部,可以认为是包含多个元素,每个元素表示一个方法,把这里的每个元素,称为是一个"栈帧",这一个栈帧里,会包含这个方法的入口地址,方法参数,返回地址,局部变量等等.

由于函数调用的关系,所以虚拟机栈,也具有"先进后出"的特点.

栈上的内存空间是跟着方法走的,调用一个方法,就会创建栈帧,方法执行结束了,这个栈帧也就销毁了,一帧一帧之间,是连续的.

栈空间是有上限的,jvm启动的时候,可以设置参数,其中有一个参数就可以设置栈空间的大小.

堆区

整个jvm空间最大的区域,new 出来的对象,都是在堆上的.

堆,是一个进程有一份,栈是一个线程有一份,一个进程有N个.

元数据区

以前叫做方法区,Java 8 开始改叫元数据区.类对象,常量池,静态成员都在这里.

一个进程只有一块,多个线程公用一块.


考点,某个变量在哪个区域上,原则就是:

1.局部变量在栈上

2.普通成员变量在堆上

3.静态成员变量在方法区/元数据区.


类加载

准确的来说,类加载就是把.class文件,从硬盘被加载到内存中的元数据区这样的过程.

加载过程

加载

找到.class文件,读取文件内容

验证

根据jvm虚拟机规范,检查.class文件的格式是否符合要求.

准备

给类对象分配内存空间(此时内存初始化成全0),此时就意味着静态成员也是设为0值了.

解析

针对字符串常量进行初始化,把符号引用转为直接引用.

字符串常量,要有一块内存空间,存这个字符的实际内容,还要有一个引用,来保存这个内存空间的起始地址.在类加载之前,字符串常量,此时是处在.class文件中的,此时这个"引用"记录的并非是字符串常量的真正的地址,而是它在文件中的"偏移量"或者是"占位符".在类加载之后,才真正把字符串常量放到内存中,此时再有内存地址,这个引用才真正的赋值成内存地址.

 初始化

真正 针对类对象里的内容进行初始化,加载父类,执行静态代码块等等.


一个类,会在什么时候被加载?

不是Java程序一运行,就把所有的类都记载了,而是真正用到的时候才加载.

1.构造类的实例

2.调用这个类的静态方法或使用静态属性

3.加载子类,就会先加载父类.

用到了才加载,加载过后,就不重复加载了.


双亲委派模型

破坏双亲委派模型

自己写的类加载器,可以去遵守也可以不遵守,是否遵守,主要是看需求.Tomcat,去加载webapp,就是单独的类加载器,不遵守双亲委派模型.


垃圾回收机制 GC

垃圾指的就是不在使用的内存,垃圾回收,就是把不用的内存帮我们自动释放了.

如果内存一直占着不用,又不释放,就会导致剩余空间越来越少,导致内存泄漏,进一步导致后续的内存申请操作失败.(尤其是服务器,特别要注意内存泄漏)

GC好处:非常省心,可以让程序员写代码简单点,不容易出错.

GC坏处:需要额外的系统资源,也有额外的性能开销.另外,GC还存在一个比较关键的问题,STW(stop the world)问题,如果有时候,内存中的垃圾已经很多了,此时触发一次GC操作,开销会很大,可能会吃掉很多系统资源,会造成几十毫秒甚至上百毫秒的卡顿;另一方面,GC回收垃圾的时候可能会涉及到一些锁操作,会导致业务代码无法正常执行.

新版Java(Java 13 开始)引入了zgc垃圾回收器,已经设计的非常精致了,可以是STW控制在1ms以下.

GC 主要是针对堆区域进行释放的.

GC是以"对象"为基本单位进行回收的.(不是以字节为单位)

GC回收的是整个对象都不使用的情况,而一部分使用,一部分不使用的对象,暂且是先不回收的.

 GC实际工作过程

1.找到垃圾/判定垃圾

关键思路,抓住这个对象,查看它到底有没有引用指向它.Java中使用对象,就只能通过引用来使用,如果一个对象有引用指向它,就可能被使用到;如果一个对象没有引用指向了,就不会再使用了.

那么具体如何知道对象是否有引用指向呢?有两种典型实现.

1)引用计数(不是Java的做法,python/php)

给每个对象分配了一个计数器(整数),每次创建一个引用指向该对象,计数器就+1,每次引用被销毁了,计数器就-1;

这个方法简单有效,但是Java没有使用,这个方法存在两个问题:

  • 内存空间浪费多(空间利用率低),每个对象都要分配一个计数器,如果按四个字节算,代码中的对象少无所谓,但是对象特别多了,占用的额外空间就会很多,尤其是每个对象都比较小的情况下.
  • 存在循环引用问题 

 

2)可达性分析(Java的做法)

Java中的对象,都是通过引用来指向并访问的,经常是一个引用指向一个对象,这个对象里的成员又指向别的对象.

 整个Java中的对象,就是通过上述类似的关系,通过链式/树形 结构,整体的串起来.

可达性分析:就是把所有这些对象被组织的结构视为树,就从树根节点出发,遍历树,所有能被访问到的对象,标记为"可达",不能被访问到的,就是不可达.

jvm有一个所有对象的名单,通过上述的遍历,把可达的标记出来了,剩下的不可达的就可以作为垃圾进行回收了.

 

2.进行对象的释放

如何清理垃圾:主要是三种基本做法

1.标记清除

简单粗暴,但是存在内存碎片问题,被释放的空闲空间,是零散的,不是连续的.

 申请内存要求的是连续的空间,总的空闲空间可能很大,但是每一个具体的空间都很小,可能就会导致申请大一点的内存就申请失败了.

2.复制算法

解决了内存碎片化的问题.直接把整个内存分成两半,用一半,丢一半.

1,3,5标记成垃圾.

把不是垃圾的对象复制到另外一半,然后原先的一半内存空间清空.每次触发复制算法,都是向另外一侧进行复制,把内存中的数据拷贝过去.

缺点:

1.空间利用率低

2.如果是垃圾比较少,有效对象多的情况下,复制成本就比较大了. 


3.标记整理

解决复制算法的缺点,类似于顺序表元素搬运的操作.

1,3,5是垃圾. 

保证了空间利用率,同时也解决了内存碎片化的问题,但是这种做法,效率也不高,如果要搬运的空间比较大,此时开销也是比较大的. 

 


分代回收

基于上述基本策略,创新了一个复合策略"分代回收".

把垃圾回收,分成不同的场景,各个场景的算法不同,各展所长.

如何分代?

基于一个经验规律:如果一个东西,存在的时间比较长了,那么大概率还会继续长时间的持续存下去.

这个规律,对于Java也是有效的(有一系列的实验和论证过程).

java的对象要么是生命周期特别短,也么是特别长,根据生命周期的长短,分别使用不同的算法.

给对象引入一个概念,年龄.(单位不是年,而是熬过GC的轮次)年龄越大,这个对象存在的时间也就越长.

 

刚new出来的对象,年龄是0的对象,放到伊甸区.熬过一轮GC,对象就被放到幸村区了,虽然看起来伊甸区很大,幸村区很小,但是一般是够用的.(根据上述经验规律,大部分的Java中的对象都是生命周期非常短的).伊甸区到幸村区是复制算法.

到幸存区之后,也要周期性的接受GC的考验,如果变成垃圾,就要被释放,不是垃圾,就拷贝到另一个幸存区.(两个幸存区同一时刻只能用一个),再两者之间来回拷贝,采用复制算法,用于幸存区的体积不大,所以空间浪费也可以接受.

如果这个对象已经再两个幸存区之间来回拷贝过很多次了,这个时候就要进入老年代了.

老年代都是年纪大的对象,生命周期普遍是比较长的,针对老年代,也要周期性的进行GC的扫描,但是扫描的频率更低,如果老年代的对象是垃圾了,就通过标记整理的方式进行释放.

免费职业培训