共计 7292 个字符,预计需要花费 19 分钟才能阅读完成。
公众号(五分钟学大数据)已推出大数据面试系列文章—五分钟小面试 ,此系列文章将会 深入研究各大厂笔面试真题 ,并依据笔面试题 扩大相干的知识点,助力大家都可能胜利入职大厂!
大数据笔面试系列文章分为两种类型:混合型(即一篇文章中会有多个框架的知识点—死记硬背);专项型(一篇文章针对某个框架进行深刻解析—专项演练)。
此篇文章为系列文章的第二篇(JVM 专项)
第一题:JVM 内存相干(百度)
问:JVM 内存模型理解吗,简略说下
答:
因为这块内容太多了,许多小伙伴可能记不住这么多,所以 上面的答案分为简答和精答。
JVM 运行时内存共分为 程序计数器,Java 虚拟机栈,本地办法栈,堆,办法区 五个局部:
注:JVM 调优次要就是优化 Heap 堆 和 Method Area 办法区
- 程序计数器(线程公有):
简答:每个线程都有一个程序计算器,就是一个指针,指向办法区中的办法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个十分小的内存空间,简直能够疏忽不记。
精答:占据一块较小的内存空间,能够看做以后线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支,循环,跳转,异样解决,线程复原等根底性能都须要依赖这个计数器来实现。
因为 jvm 的多线程是通过线程轮流切换并调配处理器执行工夫的形式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因而将来线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。
如果线程正在执行的是一个 Java 办法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;
如果正在执行的是 Native 办法,这个计数器则为空(undefined)。
此内存区域是惟一一个在 Java 虚拟机标准中没有规定任何 OutOfMemoryError 状况的区域。
- Java 虚拟机栈(线程公有):
简答:主管 Java 程序的运行,在线程创立时创立,它的生命期是追随线程的生命期,线程完结栈内存也就开释,对于栈来说不存在垃圾回收问题,只有线程一完结该栈就 Over,生命周期和线程统一,是线程公有的。根本类型的变量和对象的援用变量都是在函数的栈内存中调配。
精答:线程公有,生命周期和线程雷同,虚拟机栈形容的是 Java 办法执行的内存模型,每个办法在执行的同时都会创立一个栈帧用于存储局部变量表,操作数栈,动静链接,办法进口等信息。每一个办法从调用直至实现的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表寄存了编译期可知的各种根本类型数据(boolean、byte、char、short、int、float、long、double)、对象援用、returnAddress 类型(指向了一条字节码指令的地址)。
其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量表空间(slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期实现调配,当进入一个办法时,这个办法所须要在栈帧中调配多大的局部变量空间是齐全确定的,在办法运行期间不会扭转局部变量表的大小。
在 Java 虚拟机标准中,对此区域规定了两种异样情况:如果线程申请的栈深度大于虚拟机所容许的深度,将会抛出 Stack OverflowError 异样;如果虚拟机栈能够动静扩大时无奈申请到足够的内存,就会抛出 OutOfMemoryError 异样。
- 本地办法栈(线程公有):
简答:本地办法栈为虚拟机中应用到的 native 办法服务,native 办法作用是交融不同的编程语言为 Java 所用,它的初衷是交融 C /C++ 程序,Java 诞生的时候 C /C++ 横行的时候,要想立足,必须有调用 C /C++ 程序,于是就在内存中专门开拓了一块区域解决标记为 native 的代码。
精答:本地办法栈与虚拟机栈所施展的作用十分类似,他们之间的区别不过是虚拟机栈为虚拟机执行 Java 办法(字节码)服务,而本地办法栈则为虚拟机中应用到的 native 办法服务。在虚拟机标准中对本地办法栈中办法应用的语言、应用形式与数据结构并没有强制规定,因而具体的虚拟机能够自在实现它。甚至有的虚拟机间接把本地办法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出 Stack OverflowError 异样和 OutOfMemoryError 异样。
- Java 堆(线程共享):
简答:堆这块区域是 JVM 中最大的,利用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 次要的回收区,一个 JVM 实例只存在一个堆类存。堆内存的大小是能够调节的。
精答:对于大多数利用来说,堆空间是 jvm 内存中最大的一块。Java 堆是被所有线程共享,虚拟机启动时创立,此内存区域惟一的目标就是寄存对象实例,简直所有的对象实例都在这里分配内存。这一点在 Java 虚拟机标准中的形容是:所有的对象实例以及数组都要在堆上调配,然而随着 JIT 编译器的倒退和逃逸剖析技术逐步成熟,栈上调配,标量替换优化技术将会导致一些奥妙的变动产生,所有的对象都调配在堆上也就变得不那么相对了。
Java 堆是垃圾收集器治理的次要区域,因而很多时候也被称为“GC 堆”。从内存回收角度看,因为当初收集器根本都采纳分代收集算法,所以 Java 堆还能够细分为:新生代和老年代;再粗疏一点的有 Eden 空间,From Survivor 空间,To Survivor 空间等。从内存调配的角度来看,线程共享的 Java 堆中可能划分出多个线程公有的调配缓冲区。不过无论如何划分,都与寄存内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目标是为了更好的回收内存,或者更快的分配内存。(如果在堆中没有内存实现实例调配,并且堆也无奈再扩大时,将会抛出 OutOfMemoryError 异样。)
- 办法区(线程共享):
简答:和堆一样所有线程共享,次要用于存储已被 jvm 加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。
精答:办法区是被所有线程共享,所有字段和办法字节码,以及一些非凡办法如构造函数,接口代码也在此定义。简略说,所有定义的办法的信息都保留在该区域,此区域属于共享区间。
动态变量,常量,类信息(构造方法 / 接口定义),运行时常量池存在办法区中;然而实例变量存在堆内存中,和办法区无关。
在 JDK1.7 公布的 HotSpot 中,曾经把字符串常量池移除办法区了。
- 常量池(线程共享)::
简答:运行时常量池是办法区的一部分。用于寄存编译期生成的各种字面量和符号援用,它的重要个性是动态性,即 Java 语言并不要求常量肯定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。
精答:运行时常量池是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有一项信息是常量池,用于寄存编译期生成的各种字面量和符号援用,这部分内容将在类加载后进入办法区的运行时常量池中寄存。
Java 虚拟机对 class 文件每一部分的格局都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被 jvm 认可。但对于运行时常量池,Java 虚拟机标准没做任何细节要求。
运行时常量池有个重要个性是动态性,Java 语言不要求常量肯定只在编译期能力产生,也就是并非预置入 class 文件中常量池的内容能力进入办法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种个性应用最多的是 String 类的 intern()办法。
既然运行时常量池是办法区的一部分,天然受到办法区内存的限度。当常量池无奈再申请到内存时会抛出 outOfMemeryError 异样。
jdk 1.8 同 jdk 1.7 比,最大的差异就是:元数据区取代了永恒代 。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代之间最大的区别在于: 元数据空间并不在虚拟机中,而是应用本地内存。
第二题:类加载相干(新浪微博)
问:jvm 加载类的过程次要有哪些,具体怎么加载?
答:
简答 :类加载过程即是指 JVM 虚拟机把.class 文件中类信息加载进内存,并进行解析生成对应的 class 对象的过程。分为五个步骤:加载 -> 验证 -> 筹备 -> 解析 -> 初始化。 加载 :将内部的 .class 文件加载到 Java 虚拟机中; 验证 :确保加载进来的 calss 文件蕴含的额信息合乎 Java 虚拟机的要求; 筹备 :为类变量分配内存,设置类变量的初始值; 解析 :将常量池内的符号援用 转为 间接援用; 初始化:初始化类变量和动态代码块。
精答 : 后方预警,内容较长,做好筹备!
一个 Java 文件从编码实现到最终执行,个别次要包含两个过程:编译、运行
- 编译:即把咱们写好的 java 文件,通过 javac 命令编译成字节码,也就是咱们常说的.class 文件。
- 运行:则是把编译生成的.class 文件交给 Java 虚拟机 (JVM) 执行。
而咱们所说的类加载过程即是指 JVM 虚拟机把.class 文件中类信息加载进内存,并进行解析生成对应的 class 对象的过程。
- 类加载过程
举个简略的例子来说,JVM 在执行某段代码时,遇到了 class A,然而此时内存中并没有 class A 的相干信息,于是 JVM 就会到相应的 class 文件中去寻找 class A 的类信息,并加载进内存中,这就是咱们所说的类加载过程。
由此可见,JVM 不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个须要运行的类时才会加载,且只加载一次。
- 类加载
类加载的过程次要分为三个局部:加载、链接、初始化。
而链接又能够细分为三个小局部:验证、筹备、解析。
- 加载
简略来说,加载指的是把 class 字节码文件从各个起源通过类加载器装载入内存中。
这里有两个重点:
字节码起源:个别的加载起源包含从本地门路下编译生成的.class 文件,从 jar 包中的.class 文件,从近程网络,以及动静代理实时编译
类加载器:个别包含启动类加载器,扩大类加载器,利用类加载器,以及用户的自定义类加载器。
注:为什么会有自定义类加载器?
一方面是因为 java 代码很容易被反编译,如果须要对本人的代码加密的话,能够对编译后的代码进行加密,而后再通过实现本人的自定义类加载器进行解密,最初再加载。
另一方面也有可能从非标准的起源加载代码,比方从网络起源,那就须要本人实现一个类加载器,从指定源进行加载。
- 验证
次要是为了保障加载进来的字节流合乎虚拟机标准,不会造成平安谬误。
包含对于文件格式的验证,比方常量中是否有不被反对的常量?文件中是否有不标准的或者附加的其余信息?
对于元数据的验证,比方该类是否继承了被 final 润饰的类?类中的字段,办法是否与父类抵触?是否呈现了不合理的重载?
对于字节码的验证,保障程序语义的合理性,比方要保障类型转换的合理性。
对于符号援用的验证,比方校验符号援用中通过全限定名是否可能找到对应的类?校验符号援用中的拜访性(private,public 等)是否可被以后类拜访?
- 筹备
次要是为类变量(留神,不是实例变量)分配内存,并且赋予初值。
特地须要留神,初值,不是代码中具体写的初始化的值,而是 Java 虚拟机依据不同变量类型的默认初始值。
比方 8 种根本类型的初值,默认为 0;援用类型的初值则为 null;常量的初值即为代码中设置的值,final
static tmp = 456,那么该阶段 tmp 的初值就是 456。
- 解析
将常量池内的符号援用替换为间接援用的过程。
两个重点:
符号援用:即一个字符串,然而这个字符串给出了一些可能唯一性辨认一个办法,一个变量,一个类的相干信息。
间接援用:能够了解为一个内存地址,或者一个偏移量。比方类办法,类变量的间接援用是指向办法区的指针;而实例办法,实例变量的间接援用则是从实例的头指针开始算起到这个实例变量地位的偏移量。
举个例子来说,当初调用办法 hello(),这个办法的地址是 1234567,那么 hello 就是符号援用,1234567 就是间接援用。
在解析阶段,虚构机会把所有的类名,办法名,字段名这些符号援用替换为具体的内存地址或偏移量,也就是间接援用。
- 初始化
这个阶段次要是对类变量初始化,是执行类结构器的过程。
换句话说,只对 static 润饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时蕴含多个动态变量和动态代码块,则依照自上而下的程序顺次执行。
- 总结
类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,能力取得可能被虚拟机加载的字节码文件;在其后还有具体的类应用过程,当应用实现之后,还会在办法区垃圾回收的过程中进行卸载。如果想要理解 Java 类整个生命周期的话,能够自行上网查阅相干材料,这里不再多做赘述。
第三题:JVM 内存相干(云从科技)
问:Java 中会存在内存透露吗,请简略形容
答:
实践上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被宽泛应用于服务器端编程的一个重要起因);然而 在理论开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收也会产生内存泄露。
一个例子就是 Hibernate 的 Session(一级缓存)中的对象属于长久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。
上面的例子也展现了 Java 中产生内存泄露的状况:
package com.yuan_more;
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack(){elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem){ensureCapacity();
}
public T pop(){if(size == 0){throw new EmptyStackException();
}
return elements[-- size];
}
private void ensureCapacity() {if(elements.length == size){elements = Arrays.copyOf(elements,2 * size +1);
}
}
}
下面的代码实现了一个栈(先进后出(FILO))构造,乍看之下仿佛没有什么显著的问题,它甚至能够通过你编写的各种单元测试。
然而其中的 pop 办法却存在内存泄露的问题,当咱们用 pop 办法弹出栈中的对象时,该对象不会被当作垃圾回收,即便应用栈的程序不再援用这些对象,因为栈外部保护着对这些对象的过期援用(obsolete reference)。
在反对垃圾回收的语言中,内存泄露是很荫蔽的,这种内存泄露其实就是有意识的对象放弃。
如果一个对象援用被有意识的保留起来了,那么垃圾回收器不会解决这个对象,也不会解决该对象援用的其余对象,即便这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极其状况下会引发 Disk Paging(物理内存与硬盘的虚拟内存替换数据),甚至造成 OutOfMemoryError。
第四题:垃圾回收相干(滴滴出行)
问:晓得 GC 吗?为什么要有 GC?
答:
GC 是垃圾收集的意思,内存解决是编程人员容易呈现问题的中央,遗记或者谬误的内存回收会导致程序或零碎的不稳固甚至解体。
Java 提供的 GC 性能能够主动监测对象是否超过作用域从而达到主动回收内存的目标,Java 语言没有提供开释已分配内存的显示操作方法。Java 程序员不必放心内存治理,因为垃圾收集器会主动进行治理。
要申请垃圾收集,能够调用上面的办法之一:System.gc() 或 Runtime.getRuntime().gc(),留神,只是申请,JVM 何时进行垃圾回收具备不可预知性。
垃圾回收能够无效的避免内存泄露,无效的应用能够应用的内存。垃圾回收器通常是作为一个独自的低优先级的线程运行,不可预知的状况下对内存堆中曾经死亡的或者长时间没有应用的对象进行革除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
在 Java 诞生初期,垃圾回收是 Java 最大的亮点之一,因为服务器端的编程须要无效的避免内存泄露问题,然而时过境迁,现在 Java 的垃圾回收机制曾经成为被诟病的货色。挪动智能终端用户通常感觉 iOS 的零碎比 Android 零碎有更好的用户体验,其中一个深层次的起因就在于 Android 零碎中垃圾回收的不可预知性。
第五题:JVM 内存相干(阿里)
问:Hotspot 虚拟机中的堆为什么要有新生代和老年代?
答:
因为有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采纳不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点,提高效率。
所谓的新生代和老年代是针对于分代收集算法来定义的,新生代又分为 Eden 和 Survivor 两个区。加上老年代就这三个区。
数据会首先调配到 Eden 区当中,当然也有非凡状况,如果是大对象那么会间接放入到老年代(大对象是指须要大量间断内存空间的 java 对象)。当 Eden 没有足够空间的时候就会触发 jvm 发动一次 Minor GC。新生代垃圾回收采纳的是复制算法。
如果对象通过一次 Minor GC 还存活,并且又能被 Survivor 空间承受,那么将被挪动到 Survivor 空间当中。并将其年龄设为 1,对象在 Survivor 每熬过一次 Minor GC,年龄就加 1,当年龄达到肯定的水平(默认为 15)时,就会被降职到老年代中了,当然降职老年代的年龄是能够设置的。如果老年代满了就执行:Full GC,因为不常常执行,因而 老年代垃圾回收采纳了标记 - 整顿(Mark-Compact)算法。