JAVA面试——JVM知识

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhengzhaoyang122/article/details/82183523

1、什么情况下会发生栈内存溢出。
【1】、线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。递归的调用一个简单的方法,不断累积就会抛出StackOverflowError异常。
【2】、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。无限循环的创建线程,并对每个线程增加内存。则会抛出OutOfMemoryError异常。
 注意:在多线程的情况下,给每个线程的栈分配的内存越大,越容易产生内存溢出异常。操作系统为每个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部分共享内存的最大值,忽略程序计数器的内存消耗(很小),以及进程本身消耗的内存,剩下的内存便给了虚拟机栈和本地方法栈。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少。因此,如果是建立过多的线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和每个线程的栈容量来换取更多的线程。结合下图理解学习:
 

2、JVM的内存结构,Eden和Survivor比例。
  
  JVM内存结构主要由三部分组成:堆空间、方法区和栈。堆内存是JVM虚拟机中最大的一部分,它由年轻代和老年代组成。而年轻代有被分为三部分,Eden空间、From Survivor空间和To Survivor空间。默认情况下年轻代按照8:1:1的比例分配。方法区存储类信息、常量、静态变量等数据,是线程共享的区域。为了与Java堆区分,方法区有一个别名Non-Heap(非堆)。栈分为Java虚拟机栈和本地方法栈主要用于方法的执行。

3、JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
 ● 所谓新生代和老年代是针对分代(存活年龄)收集算法来定义的,这样就针对性的进行GC。例如:MinorGC回收清理新生代,频率相对较高。MajorGC主要清理老年代,频率相对较少。FullGC是指清理这个堆空间包括年轻代和持久代等。这样非常高效的提高了GC的效率,提交系统性能。
 ● 新生代分为Eden和Survivor两个分区,当新对象时首先会分配到Eden区中(特殊情况:大对象会直接放入老年代,大对象指需要连续的大量内存空间的对象),当Eden没有足够空间的时候就会触发MinorGC。如果GC后,对象还活着,并且能被Survivor空间接受,那么就会移至Survivor中,并将其年龄设为1,对象在Survivor中每熬过一次GC,年龄就+1,当年龄增加到一定程度(默认为15,可设置)时,就会被晋升到老年代。

4、JVM中一次完整的GC流程是怎样的,说说你知道的几种主要的JVM参数。
   Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。

-Xms 初始堆大小
-Xmx 最大堆大小
-Xmn 最小堆大小
-XX:newSize 新生代初始值
-XX:newRatio

3表示:年轻代:老年代=1:3

-XX:permSize 持久代初始值
-XX:survivorRatio

Eden与Survivor的比例。

8表示:Eden:Survivor = 8 : 2(Form:To=1:1)

5、你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。
  目前比较常用的垃圾收集器和它们直接搭配使用的情况如下,上面是新生代收集器,下面则是老年代收集器,根据不同业务场景进行选取。
 
 ● Serial收集器:是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)也就是Stop The World。是JVM Client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
 ● ParNew收集器:可看作Serial收集器的多线程版本,除使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 使用方式可以使用-XX:+UseConcMarkSweepGC,或者是使用-XX:+UseParNewGC来强制开启,可以通过-XX:ParallelGCThreads 来调整或者限制垃圾收集的线程数量。
 ● Parallel Scavenge收集器:也是新生代收集器,也是使用复制算法的收集器,又是并行多线程收集器。Parallel Scavenge收集器的特点是它关注的点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集的时间,从而降低用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
   Parallel Scavenge提供了两个参数用来精确控制,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
   MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
   GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
   Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
 ● Serial Old:是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。
 ● Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
   直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
 ● CMS(Concurrent Mark Sweep)收集器:是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
   从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:① 初始标记(CMS initial mark)② 并发标记(CMS concurrent mark)③ 重新标记(CMS remark)④ 并发清除(CMS concurrent sweep)其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
   CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
   CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
 ● G1收集器:是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。
   ☞ 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
   ☞ 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
   ☞ 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
   ☞ 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
   在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

6、垃圾回收算法的实现原理。
  1)、标记-清除:是现代垃圾回收算法的基本思想。标记-清除将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始可达的对象。因此,未标记的对象就是未被引用的对象。然后,在清除阶段,清除所有未被标记的对象。


   2)、标记-压缩:标记-压缩算法适合用于存活对象较多的场合,如老年代。在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,并不是简单的清理未标记的对象,而是将所有的存活对象压缩到内存的另一端,之后,清理边界外所有的对象。

  3)、复制算法:①、与标记算法相比,复制算法是一种相对高效的回收方法。
           ②、不适用存活对象较多的场合,比如老年代。      
           ③、将原有的内存空间分为两块,每次只是用一块,在垃圾回收时,将正在适用的内存存活对象复制一份到未使用的内存中,之后,清除正在适用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

7、当出现了内存溢出,你怎么排错。|
   首先分析是什么类型的内存溢出,对应的调整参数或者优化代码。
   https://wangkang007.gitbooks.io/jvm/content/4jvmdiao_you.html

8、JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。
  ♣ 重排序:在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。(譬如:a=1;b=2;重排后b=2;a=1;)。
     ◀ 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
     ◀ 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
     ◀ 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    
  ♣ 内存屏障(Memory Barrier):又称内存栅栏,为了保障执行顺序和可见性的一条cpu指令。
     ◀ 保障执行顺序:编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
     ◀ Memory Barrier保证可见性是指:强制刷出各种CPU cache,如一个 Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。例如:Volatile就是基于Memory Barrier实现的。
  ♣ happen-before:操作之间的内存可见性。在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。
     ◀ 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
     ◀ 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
     ◀ volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
     ◀ 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
  ♣ 主内存:共享变量存储的区域即是主内存。
  ♣ 工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本。

9、简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
 ☞ 类的加载过程:JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤:
    ● 装载:查找并加载类的二进制数据;
    ● 链接:验证:确保被加载类的正确性;
                  准备:为类的静态变量分配内存,并将其初始化为默认值;
                  解析:把类中的符号引用转换为直接引用;
    ● 初始化:为类的静态变量赋予正确的初始值;
 ☞ JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
    

 ☞ 打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。默认的loadClass方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载。为了破坏双亲委派机制必须重写loadClass方法。

10、讲讲JAVA的反射机制。
   JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

11、你们线上应用的JVM参数有哪些。

-server Server模式启动
-Xms6000M 初始堆内存6000m
-Xmx6000M 最大堆内存6000m
-Xmn500M 最小堆内存500m
-XX:PermSize=500M 永久代500m
-XX:MaxPermSize=500M 最大永久代500m
-XX:SurvivorRatio=65536 设置年轻代中Eden区与Survivor区的比值
-XX:MaxTenuringThreshold=0 设置垃圾最大年龄(在年轻代的存活次数)。如果设置为0的话,则年轻代对象不经过Survivor区直接进入年老代
-Xnoclassgc 关闭class的垃圾回收功能,即虚拟机加载的类,即便是不使用,没有实例也不会回收
-XX:+DisableExplicitGC 不响应 System.gc() 代码
-XX:+UseParNewGC 设置年轻代为并发收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此参数
-XX:+UseConcMarkSweepGC 即CMS收集,设置老年代为并发收集
-XX:+UseCMSCompactAtFullCollection 打开内存空间的压缩和整理,在Full GC后执行。可能会影响性能,但可以消除内存碎片
-XX:CMSFullGCsBeforeCompaction=0 由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间并行收集以后会产生内存碎片,内存使用效率降低。此参数设置运行0次Full GC后对内存空间进行压缩和整理,即每次Full GC后立刻开始压缩和整理内存
-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
-XX:+CMSParallelRemarkEnabled 开启并行收集
-XX:CMSInitiatingOccupancyFraction=90 允许90%的Survivor区被占用(JVM默认为50%)。提高对于Survivor区的使用率
-XX:SoftRefLRUPolicyMSPerMB=0 软引用对象在最后一次被访问后能存活0毫秒(JVM默认为1000毫秒)
-XX:+PrintClassHistogram 按下 Ctrl+Break 后打印堆内存中类实例的柱状信息,同JDK的 jmap -histo 命令
-XX:+PrintGCDetails 每次GC时打印详细信息
-XX:+PrintGCTimeStamps 打印每次GC的时间戳
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:log/gc.log 日志文件的输出路径

12、g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。
   CMS是以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。比较占用cpu资源,切易造成碎片。
   G1是面向服务端的垃圾收集器,是jdk9默认的收集器,基于标记-整理算法实现。可利用多核、多cpu,保留分代,实现可预测停顿,可控。具体可参考5

13、怎么打出线程栈信息。
 https://www.cnblogs.com/zhuqq/p/5938187.html

14、请解释如下jvm参数的含义:-server -Xms512m -Xmx512m  -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxTenuringThreshold=20 -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly。

-server Server模式启动
-Xms512m 初始堆内存512m
-Xmx512m 最大堆内存512m
-Xss1024K 线程栈空间1m
-XX:PermSize=256m 永久代256m
-XX:MaxPermSize=512m 最大永久代512m
-XX:MaxTenuringThreshold=20 最大转为老年代检查次数20
-XX:CMSInitiatingOccupancyFraction=80 CMS回收开启时机:内存占用80%
-XX:+UseCMSInitiatingOccupancyOnly 命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期

猜你喜欢

转载自blog.csdn.net/zhengzhaoyang122/article/details/82183523
今日推荐