JVM⾯试题(史上最强、持续更新、吐⾎推荐)
⽂章很长,建议收藏起来慢慢读! | | 为您奉上珍贵的学习资源 :
免费赠送经典图书: ⾯试必备 + ⼤⼚必备 +涨薪必备加尼恩免费领
免费赠送经典图书: ⾯试必备 + ⼤⼚必备 +涨薪必备加尼恩免费领
免费赠送经典图书: ⾯试必备 + ⼤⼚必备 +涨薪必备加尼恩免费领
免费赠送经典图书: ⾯试必备 + ⼤⼚必备 +涨薪必备加尼恩免费领
免费赠送资源宝库: Java 必备百度⽹盘资源⼤合集价值>10000元
推荐:⼊⼤⼚、做架构、⼤⼒提升Java 内功的精彩博⽂
⼊⼤⼚、做架构、⼤⼒提升Java 内功必备的精彩博⽂2021 秋招涨薪1W + 必备的精彩博⽂1:2:
3: (⾯试必备)4: (史上最全)
5:6:
7:8:
9:10:
11:12:
13:14:
15:16:
SpringCloud 微服务精彩博⽂
史上最全 Java ⾯试题 30 专题总⽬录
精⼼梳理、吐⾎推荐、史上最强、建议收藏阿⾥、京东、美团、头条.... 随意挑、横着⾛1. 2.
3.4、
5、6、
7.8、
9.10、
11.12、
13.15.
14、16、
17、18、
19、20、
21、26、
22.23、
24、25、
27、28、
29、30、
史上最全 Java ⾯试题:JVM 篇
社流的⾯试真题
⾯试真题1:元空间会产⽣内存溢出么?在什么情况下会产⽣内存溢出?
具体问题:元空间会产⽣内存溢出么?在什么情况下会产⽣内存溢出?。
java8 及以后的版本使⽤Metaspace来代替永久代,Metaspace是⽅法区在HotSpot中的实现,它与持久代最⼤区别在于,Metaspace并不在虚拟机内存中⽽是使⽤本地内存也就是在JDK8中,classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory.
永久代(java 8 后被元空间Metaspace取代了)存放了以下信息:
虚拟机加载的类信息
常量池
静态变量
即时编译后的代码
出现问题原因
错误的主要原因, 是加载到内存中的 class 数量太多或者体积太⼤。
解决办法
增加 Metaspace 的⼤⼩
-XX:MaxMetaspaceSize=512m
代码演⽰
模拟Metaspace空间溢出,我们不断⽣成类往元空间灌,类占据的空间是会超过Metaspace指定的空间⼤⼩的
查看元空间⼤⼩
java -XX:+PrintFlagsInitial
设置配置 这⾥设置10m⽅便演⽰效果
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
编写代码
import lib.proxy.Enhancer;
import lib.proxy.MethodInterceptor;
import lib.proxy.MethodProxy;
import flect.Method;
public class MetaspaceDemo {
static class OOM{}
public static void main(String[] args) {
int i = 0;//模拟计数多少次以后发⽣异常
try {
while (true){
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
}
spring framework面试题} catch (Throwable e) {
System.out.println("=================多少次后发⽣异常:"+i);
e.printStackTrace();
}
}
}
运⾏结果:
更多⾯试真题,请来疯狂创客圈社的流,具体请参考
正式内容1:Java内存区域
解释 Java 堆空间及 GC?
当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的⼀部分⽤于创建堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC 是 JVM 内部的⼀个进程,回收⽆效对象的内存⽤于将来的分配。
说⼀下 JVM 的主要组成部分及其作⽤?
JVM包含两个⼦系统和两个组件,两个⼦系统为Class loader(类装载)、Execution engine(执⾏引擎);两个组件为Runtime data area(运⾏时数据区)、Native Interface(本地接⼝)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class⽂件到Runtime data area中的method area。
Execution engine(执⾏引擎):执⾏classes中的指令。
Native Interface(本地接⼝):与native libraries交互,是其它编程语⾔交互的接⼝。
Runtime data area(运⾏时数据区域):这就是我们常说的JVM的内存。
作⽤ :⾸先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运⾏时数据区(Runtime data area)的⽅法区内,⽽字节码⽂件只是 JVM 的⼀套指令集规范,并不能直接交给底层操作系统去执⾏,因此需要特定的命令解析器执⾏引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU
去执⾏,⽽这个过程中需要调⽤其他语⾔的本地库接⼝(Native Interface)来实现整个程序的功能。
下⾯是Java程序运⾏机制详细说明
Java程序运⾏机制步骤
⾸先利⽤IDE集成开发⼯具编写Java源代码,源⽂件的后缀为.java;
再利⽤编译器(javac命令)将源代码编译成字节码⽂件,字节码⽂件的后缀名为.class;
运⾏字节码的⼯作是由解释器(java命令)来完成的。
从上图可以看,java⽂件通过编译器变成了.class⽂件,接下来类加载器⼜将这些.class⽂件加载到JVM中。
其实可以⼀句话来解释:类的加载指的是将类的.class⽂件中的⼆进制数据读⼊到内存中,将其放在运⾏时数据区的⽅法区内,然后在堆区创建⼀个 java.lang.Class对象,⽤来封装类在⽅法区内的数据结构。
说⼀下 JVM 运⾏时数据区?或者:说⼀下JVM内存模型?
思路: 给⾯试官画⼀下JVM内存模型图,并描述每个模块的定义,作⽤,以及可能会存在的问题,如栈溢出等。
Java 虚拟机在执⾏ Java 程序的过程中会把它所管理的内存区域划分为若⼲个不同的数据区域。这些区域都有各⾃的⽤途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动⽽存在,有些区域则是依赖线程的启动和结束⽽建⽴和销毁。Java 虚拟机所管理的内存被划分为如下⼏个区域:
程序计数器(Program Counter Register):当前线程所执⾏的字节码的⾏号指⽰器,字节码解析器的⼯作是通过改变这个计数器的值,来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
Java 虚拟机栈(Java Virtual Machine Stacks):⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息;
本地⽅法栈(Native Method Stack):与虚拟机栈的作⽤是⼀样的,只不过虚拟机栈是服务 Java ⽅法的,⽽本地⽅法栈是为虚拟机调⽤ Native ⽅法服务的;
Java 堆(Java Heap):Java 虚拟机中内存最⼤的⼀块,是被所有线程共享的,⼏乎所有的对象实例都在这⾥分配内存;
⽅法区(Methed Area):⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了⼀个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了⼀个指针并且申请了⼀个新的内存,使这个增加的指针指向这个新的内存,
使⽤深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同⼀个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发⽣改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟⼀块新的内存地址⽤于存放复制的对象。
说⼀下堆栈的区别?
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。⽐如,标记-消除,复制,标记-压缩,分代(即新⽣代使⽤复制算法,⽼年代使⽤标记——压缩)
栈使⽤的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分别
堆因为是不连续的,所以分配的内存是在运⾏期确认的,因此⼤⼩不固定。⼀般堆⼤⼩远远⼤于栈。
栈是连续的,所以分配的内存⼤⼩要在编译期就确认,⼤⼩是固定的。
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序⽅法的执⾏。
PS:
静态变量放在⽅法区
静态的对象还是放在堆。
程序的可见度
堆对于整个应⽤程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的⽣命周期和线程相同。
Java 中堆和栈有什么区别?
JVM 中堆和栈属于不同的内存区域,使⽤⽬的也不同。栈常⽤于保存⽅法帧和局部变量,⽽对象总是在堆上分配。栈通常都⽐堆⼩,也不会在多个线程之间共享,⽽堆被整个 JVM
的所有线程共享。
队列和栈是什么?有什么区别?
队列和栈都是被⽤来预存储数据的。
操作的名称不同。队列的插⼊称为⼊队,队列的删除称为出队。栈的插⼊称为进栈,栈的删除称为出栈。
可操作的⽅式不同。队列是在队尾⼊队,队头出队,即两边都可操作。⽽栈的进栈和出栈都是在栈顶进⾏的,⽆法对栈底直接进⾏操作。
操作的⽅法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进⾏的。新来的成员总是加⼊队尾(不能从中间插⼊),每次离开的成员总是队列头上(不允许中途离队)。⽽栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插⼊(进栈)的元素,⽽最先插⼊的被放在栈的底部,要到最后才能删除。
虚拟机栈(线程私有)
是描述java⽅法执⾏的内存模型,每个⽅法在执⾏的同时都会创建⼀个栈帧(Stack Frame)⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息。 每⼀个⽅法从调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在虚拟机栈中⼊栈到出栈的过程。
栈帧( Frame)是⽤来存储数据和部分过程结果的数据结构,同时也被⽤来处理动态链接(Dynamic Linking)、 ⽅法返回值和异常分派( Dispatch Exception)。 栈帧随着⽅法调⽤⽽创建,随着⽅法结束⽽销毁——⽆论⽅法是正常完成还是异常完成(抛出了在⽅法内未被捕获的异常)都算作⽅法结束。
程序计数器(线程私有)
⼀块较⼩的内存空间, 是当前线程所执⾏的字节码的⾏号指⽰器,每条线程都要有⼀个独⽴的程序计数器,这类内存也称为“线程私有” 的内存。
正在执⾏ java ⽅法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是 Native ⽅法,则为空。这个内存区域是唯⼀⼀个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
什么是直接内存?
直接内存并不是 JVM 运⾏时数据区的⼀部分, 但也会被频繁的使⽤: 在 JDK 1.4 引⼊的 NIO 提供了基于 Channel 与 Buffer 的 IO ⽅式, 它可以使⽤ Native 函数库直接分配堆外内存, 然后使⽤DirectByteBuffer 对象作为这块内存的引⽤进⾏操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在⼀些场景中可以显著提⾼性能。
HotSpot虚拟机对象探秘
对象的创建
说到对象的创建,⾸先让我们看看 Java 中提供的⼏种对象创建⽅式:
Header解释
使⽤new关键字调⽤了构造函数
使⽤Class的newInstance⽅法调⽤了构造函数
使⽤Constructor类的newInstance⽅法调⽤了构造函数
下⾯是对象创建的主要流程:
虚拟机遇到⼀条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执⾏相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使⽤“指针碰撞“⽅式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“⽅式。划分内存时还需要考虑⼀个问题-并发,也有两种⽅式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做⼀些必要的对象设置(元信息、哈希码…),最后执⾏<init>⽅法。
为对象分配内存
类加载完成后,接着会在Java堆中划分⼀块内存分配给对象。内存分配根据Java堆是否规整,有两种⽅式:
指针碰撞:如果Java 堆的内存是规整,即所有⽤过的内存放在⼀边,⽽空闲的的放在另⼀边。分配内存时将位于中间的指针指⽰器向空闲的内存移动⼀段与对象⼤⼩相等的距离,这样便完成分配内存⼯作。
空闲列表:如果Java 堆的内存不是规整的,则需要由虚拟机维护⼀个列表来记录那些内存是可⽤的,这样在分配的时候可以从列表中查询到⾜够⼤的内存分配给对象,并在分配后更新列表记录。
选择哪种分配⽅式是由 Java 堆是否规整来决定的,⽽ Java 堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
处理并发安全问题
对象的创建在虚拟机中是⼀个⾮常频繁的⾏为,哪怕只是修改⼀个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B ⼜同时使⽤了原来的指针来分配内存的情况。解决这个问题有两种⽅案:
对分配内存空间的动作进⾏同步处理(采⽤ CAS + 失败重试来保障更新操作的原⼦性);
把内存分配的动作按照线程划分在不同的空间之中进⾏,即每个线程在 Java 堆中预先分配⼀⼩块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB )。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB ⽤完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB 参数来设定虚拟机是否使⽤TLAB
。
对象的访问定位
Java 程序需要通过 JVM 栈上的引⽤访问堆中的具体对象。对象的访问⽅式取决于 JVM 虚拟机的实现。⽬前主流的访问⽅式有 句柄 和 直接指针 两种⽅式。
指针: 指向对象,代表⼀个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,⽽是指向对象的指针(句柄不发⽣变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
句柄访问
Java
堆中划分出⼀块内存来作为句柄池,引⽤中存储对象的句柄地址,⽽句柄中包含了对象实例数据与对象
类型数据各⾃的具体地址信息,具体构造如下图所⽰:
优势:引⽤中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是⾮常普遍的⾏为)时只会改变句柄中的实例数据指针,⽽引⽤本⾝不需要修改。直接指针
如果使⽤直接指针访问,引⽤ 中存储的直接就是对象地址,那么Java
堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了⼀次指针定位的时间开销。由于对象的访问在Java 中⾮常频繁,因此这类开销积少成多后也是⾮常可观的执⾏成本。HotSpot 中采⽤的就是这种⽅式。
64 位 JVM 中,int 的长度是多数?
Java 中,int 类型变量的长度是⼀个固定值,与平台⽆关,都是 32 位。意思就是说,在 32 位 和 64 位 的 Java 虚拟机中,int 类型的长度是相同的。
32 位和 64 位的 JVM ,int 类型变量的长度是多数?
32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4个字节。
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64位?
你可以检查某些系统属性如 sun.del 或 os.arch 来获取该信息。
使⽤clone ⽅法没有调⽤构造函数使⽤反序列化
没有调⽤构造函数
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论