JVM面试

JVM内存结构相关问题

[百度Java笔试题]
1、【单选题】下面有关java内存结构的描述,说法错误的是?
A.JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证
B.“synchronized” — 保证在块开始时都同步主内存的值到工作内存,而块结束时将变量同步回主内存
C.“volatile” — 保证修饰后在对变量读写前都会与主内存更新。
D.如果在一个线程构造了一个不可变对象之后(对象仅包含final字段),就可以保证了这个对象被其他线程正确的查看
正确答案为:D
【答案解析】
Java线程之间的通信由Java内存模型(简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化

volatile变量的写-读可以实现线程之间的通信。
从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义;volatile读与监视器的获取有相同的内存语义。

c表述有问题,volatile修饰的变量在线程内修改时操作的实际上还是该变量在线程内存的映射,只是修饰后在对变量读写前都会与主内存更新。

线程写volatile变量的过程
①改变线程工作内存中volatile变量副本的值
②将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
①从主内存中读取volatile变量的最新值到线程的工作内存中
②从工作内存中读取volatile变量的副本的值

【阿里巴巴Java笔试题】
2、【单选题】下面有关JVM内存,说法错误的是?
A. 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,是线程隔离的
B. Java方法执行内存模型,用于存储局部变量,操作数栈,动态链接,方法出口等信息,是线程隔离的
C. 方法区用于存储JVM加载的类信息、常量、静态变量、即使编译器编译后的代码等数据,是线程隔离的
D. 原则上讲,所有的对象都在堆区上分配内存,是线程之间共享的
正确答案为C
【答案解析】
运行时数据区包括:虚拟机栈区,堆区,方法区,本地方法栈,程序计数器
虚拟机栈区 :也就是我们常说的栈区,线程私有,存放基本类型,对象的引用和 returnAddress ,在编译期间完成分配。
堆区 , JAVA 堆,也称 GC 堆,所有线程共享,存放对象的实例和数组, JAVA 堆是垃圾收集器管理的主要区域。
方法区 :所有线程共享,存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的对象的回收和对类型的卸载。
程序计数器 :线程私有,每个线程都有自己独立的程序计数器,用来指示下一条指令的地址

选择题总是有技巧的:C中静态变量肯定是共享的啊,怎么可能是线程隔离的呢?肯定选C

类加载机制相关问题

1、 能否自己定义java.lang.String
这道题考核的是类加载器,理解后就可以明确回答出问题

【答案】
可以自己定义,但是永远不会被加载。
不会被加载的原因是双亲委派模型存在,java.lang.*是java的核心类,根据双亲委派模型,会有启动类加载器(bootstrap classloader)完成加载任务返回,因此自己定义的类永远不会被加载。

【备注】“JAVA类加载机制”知识复习可以参照这个地址:类加载机制

2、 ClassCastException发生在哪步,ClassNotFoundException发生在哪步
这道题考核的是类加载机制,类加载机制就是加载.class文件到内存模型中,类加载机制详解:

类加载器就是上面步骤中加载(也叫装载)过程的具体实现

【答案解析】
此题考核的是类的加载的过程。ClassNotFoundException发生在加载(装载)阶段,ClassCastException发生在连接阶段(连接阶段的验证过程中)

【备注】“类加载机制”知识复习可以参照2.2中的备注,类加载机制包括:加载(装载) 连接【验证、准备、解析(可选)】 初始化 使用 卸载

垃圾回收相关问题

【JAVA练习题】
1、【单选题】关于JAVA的垃圾回收机制,下面哪些结论是正确?
A. 程序可以任意指定释放内存的时间
B. JAVA程序不能依赖于垃圾回收的时间或者顺序
C. 程序可明确地标识某个局部变量的引用不再被使用
D.程序可以显式地立即释放对象占有的内存
正确答案为:B
【答案解析】
java提供了一个系统级的线程,即垃圾回收器线程。用来对每一个分配出去的内存空间进行跟踪。当JVM空闲时,自动回收每块可能被回收的内存,GC是完全自动的,不能被强制执行。程序员最多只能用System.gc()来建议执行垃圾回收器回收内存,但是具体的回收时间,是不可知的。
当对象的引用变量被赋值为null,可能被当成垃圾。

【阿里巴巴笔试题】
2、【单选题】下面哪种情况会导致持久区jvm堆内存溢出?
A.循环上万次的字符串处理
B.在一段代码内申请上百M甚至上G的内存
C.使用CGLib技术直接操作字节码运行,生成大量的动态类
D.不断创建对象
正确答案为:C
【答案解析】

简单的来说 java的堆内存分为两块:permantspace(持久带) 和 heap space。
持久带中主要存放用于存放静态类型数据,如 Java Class, Method 等, 与垃圾收集器要收集的Java对象关系不大。

方法区是逻辑上的,物理上也存在堆中,所以上述说法也对
FULL GC的条件?要尽量避免

  • System.gc()方法的调用 虽然只是建议而非一定,但很多情况下它会触发 Full GC,
  • 老年代代空间不足
  • 永生区空间不足
  • 对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
  • 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
  • 堆中分配很大的对象

【人人网笔试题】
3、【单选题】以下哪项陈述是正确的?
A.垃圾回收线程的优先级很高,以保证不再 使用的内存将被及时回收
B.垃圾收集允许程序开发者明确指定释放 哪一个对象
C.垃圾回收机制保证了JAVA程序不会出现 内存溢出
D.进入”Dead”状态的线程将被垃圾回收器回收
E.以上都不对
正确答案为:E
【答案解析】
当程序运行时,至少会有两个线程开启启动,一个是我们的主线程,一个时垃圾回收线程,垃圾回收线程的priority(优先级)较低
垃圾回收器会对我们使用的对象进行监视,当一个对象长时间不使用时,垃圾回收器会在空闲的时候(不定时)对对象进行回收,释放内存空间,程序员是不可以显示的调用垃圾回收器回收内存的,但是可以使用System.gc()方法建议垃圾回收器进行回收,但是垃圾回收器不一定会执行。
Java的垃圾回收机制可以有效的防止内存溢出问题,但是它并不能完全保证不会出现内存溢出。例如当程序出现严重的问题时,也可能出现内存溢出问题。
A:垃圾回收在jvm中优先级相当相当低。
B:垃圾收集器(GC)程序开发者只能推荐JVM进行回收,但何时回收,回收哪些,程序员不能控制。
C:垃圾回收机制只是回收不再使用的JVM内存,如果程序有严重BUG,照样内存溢出。
D:进入DEAD的线程,它还可以恢复,GC不会回收

1、 垃圾回收算法与垃圾回收器,如何设置,吞吐优先与影响优先,实际项目中G1与CMS对比?
这道题考核的是垃圾回收算法和垃圾回收器,需要掌握相关知识就很好回答了。

垃圾回收算法有如下,目的就是为了新写入对象的时候有连续的内存空间:

  • 标记-清理算法
    标记-清理算法:从根节点开始标记所有可达对象,其余没标记的即为垃圾对象,执行清除。但回收后的空间是不连续的。
  • 复制算法:
    将内存分成两块,每次只使用其中一块,垃圾回收时,将标记的对象拷贝到另外一块中,然后完全清除原来使用的那块内存。复制后的空间是连续的。复制算法适用于新生代,因为垃圾对象多于存活对象,复制算法更高效。在新生代串行垃圾回收算法中,将eden中标记存活的对象拷贝未使用的s1中,s0中的年轻对象也进入s1,如果s1空间已满,则进入老年代;这样交替使用s0和s1。这种改进的复制算法,既保证了空间的连续性,有避免了大量的内存空间浪费。
    jvm4
  • 标记-压缩算法(Mark-compact)
    标记-压缩算法 :适合用于老年代的算法(存活对象多于垃圾对象)。
    标记后不复制,而是将存活对象压缩到内存的一端,然后清理边界外的所有对象。
    jvm5
  • 分代收集算法(Generational Collection)
    分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
jvm6
而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。

另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。

垃圾回收器就是垃圾回收算法的具体实现,有以下:
gc1

【答案】垃圾回收算法:
垃圾回收算法是分代处理的,新生代(标记-清理算法、复制算法),老年代(标记-压缩算法)。

吞吐优先与响应优先:
web项目和大数据运算类项目的区别。

  • 响应时间优先的并发收集器
    如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
    例如:XX:+UseConcMarkSweepGC -XX:+UseParNewGC(Parnew+CMS)或者使用G1(-XX:+UseG1GC)
  • 吞吐量优先的并行收集器
    如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和纯后台处理等。例如:-XX:+UseParallelGC -XX:+UseParallelOldGC

实际项目中采用哪种算法是需要跟踪和监控的,目前公司实际很多web项目还是使用Parnew+CMS垃圾回收器(稳定性优先),也有项目直接使用G1垃圾回收器

【备注】“垃圾回收算法和垃圾回收器”复习可以详见这个地址:垃圾回收算法和垃圾回收器

2、为什么会有两个Survior?
【答案解析】2.4完成后就很明确这个了,算法中确定的,以空间换时间

新生代使用的是复制算法,这个算法是以空间换效率,新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),对象在新生代中首先分配在Eden中,然后当进行回收时,将该两块空间(Eden和Survivior0)中还存活的对象复制到另一块Survivor1空间中,此时S0是完全空闲的,下次回收从Eden和Survivior1中复制到S0中,因此需要两个Survivo

3、Minor Gc与Full GC的区别?如何防止Full GC? Java中可以手工回收对象么
【答案解析】
Minor Gc ,Major GC, Full GC分别指什么?
Minor Gc是新生代的GC
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。一般老年代也理解为Full GC(清理整个堆空间—包括年轻代和老年代,非绝对的)。

目前阶段我们不用太理会Major GC和Full GC,也避免用这种方式区分,我们实际要关注的是jstat命令监控的应用延迟或者吞吐量,然后将 GC 事件和结果联系起来,这样去优化我们的代码。

如何防止Full GC的措施,一般面试被问到的时候至少要回答几条,以下会导致Full GC,是我们要避免的:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

4、OutOfMemory与Jvm crash的区别,碰到后如何解决
【答案解析】
crash和outofmomery的区别先答出来,crash了Java程序就不存在了,outOfMomery了Java程序还存在

解决思路的几步,以Tomcat为例:
a)、在catalina.sh或者catalina.bat增加参数(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof),这样出现outOfMomemory时会导出“出问题时刻的内存headDump文件”
b)、用工具进行分析headDump文件中的堆栈信息,可以看到是拿到对象占用很多或者提示出问题的代码块,这样可以快速定位到出问题的区域
c)、然后修改对应的代码,解决问题

当然也有一种情况是没设置最大和最小内存,采用的是默认内存,这个时候可以先用7中的参数设置一下内存,如果设置后还出问题用上面的方法解决

JVM实际问题处理

1、tomcat中如何设计java相关参数
【答案解析】
在catalina.bat(windows)或者catalina.sh(linux)增加对应的配置,一般是JAVA_OPS。

JAVA_OPTS=
"
$JAVA_OPTS -server 
-Xms2g #堆最小内存
-Xmx2g #堆最大内存
-Xmn1g #新生代大小
-Xss1024K #线程栈大小
-XX:PermSize=256m #永久代大小
-XX:MaxPermSize=512m #永久代最大值
-XX:MetaspaceSize=256m #元空间大小
-XX:MaxMetaspaceSize=512m #元空间最大值
-XX:+UseConcMarkSweepGC #使用CMS
-XX:+UseParNewGC #使用ParNew
-XX:+UseCMSCompactAtFullCollection #开启CMS完整收集时压缩,jdk8已失效
-XX:SurvivorRatio=4 #Eden区与每一个Survivor区比值
-XX:MaxTenuringThreshold=10 #年轻代晋升老年代的最大年龄阈值
-XX:CMSInitiatingOccupancyFraction=80 #触发垃圾收集的老年代空间占用率阈值
"
参数名称 含义 默认值 说明
-Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn 年轻代大小(1.4or later) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。整个堆大小=年轻代大小 + 老年代大小 + 持久代(永久代)大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4
-Xss 每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了
-XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio Eden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:+DisableExplicitGC 关闭System.gc() 这个参数需要严格的测试
-XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节 新生代采用Parallel ScavengeGC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:ParallelGCThreads 并行收集器的线程数 此值最好配置与处理器数目相等 同样适用于CMS
-XX:MaxGCPauseMillis 每次年轻代垃圾回收的最长时间(最大暂停时间) 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.

2、如何监控JVM,相关命令:jconsole,jmap(查看内存),jstat(性能分析),jstack(查看线程)
【答案解析】
jconsole实时看java进程的堆栈信息,要求服务器有图像组件,我们会稍后演示一下
jmap是打印java堆栈信息到一个文本文件,然后再分析这个文本文件,一般用于线上环境
jstack:查看相关的线程信息
jstat是可以查看堆内存各部分的使用量,以及加载类的数量, 以及垃圾回收次数等,如下:
155307095663636
jstat -gc 进程pid就可以看到具体信息,其中:
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
PC:Perm space区(方法区或者永久带)大小
PU:方法区使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

jdk1.8开始执行截图如下:
165530722786636
其中,方法区pc和pU没有了,改为MC和MU,即元空间大小和使用大小,相关属性如下(其中ccsc和ccsu了解即可):
MC:metaspace(元空间)的容量 (字节)
MU:metaspace(元空间)目前已使用空间 (字节)
CCSC:当前压缩类空间的容量 (字节)
CCSU:当前压缩类空间目前已使用空间 (字节)
【备注】
CCSC是为了在 64bit 机器上使用 32bit 的原始对象指针(oop,ordinary object pointer,这里直接就当成指针概念理解就可以了,不用关心啥是 ordinary)来节约成本(减少内存/带宽使用),提高性能(提高 Cache 命中率)

【备注】以上命令必须能明确知道,比如面试时问你如何实时监控JVM中是否有Full GC?(api项目中要严格避免的)

3、 接手一个项目,发现程序执行到一半挂在那了,如何解决?
【答案解析】
使用命令 jstack 或者jconsole pid可以列出当前pid对应jvm的所有线程栈描述,描述主要包括了每个线程的状态以及堆栈内各栈帧的方法全限定名,代码位置。

某个后台任务执行到一半停顿在那了,原因要么是有占用很多资源的作业在跑或者就是死锁了,多线程中锁使用不合理也会导致死锁,这个就是就是用jstack导出当前的线程信息,然后查看线程就可以定位出出问题的代码位置,然后修改代码就可以了

4、写了个类,发现加载的版本与自己写的不一致,如何解决?想换掉某个jar包中的某个类,如何解决?
要从类的加载器顺序思考,以web项目为例,顺序优先级为:
WEB-INF/lib/.jar < WEB-INF/classs/.java < -应用程序类加载器(Application classloader)(系统classpath中定义的jar) < 扩展类加载器(Extension classloader) (jre\lib\ext*.jar)< 启动类加载器((Bootstrap classloader)

posted @ 2022-06-22 09:48  Faetbwac  阅读(302)  评论(0编辑  收藏  举报