Java面试——JVM知识

一、什么情况下会发生栈内存溢出


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

二、JVM 的内存结构


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

JDK8版本的 JVM内存结构,将堆内的方法区替换成了堆外的元数据。

堆空间JVM 初始分配的内存由 -Xms指定,默认是物理内存的 1/64;JVM最大分配的内存由 -Xmx指 定,默认是物理内存的1/4。默认空余堆内存小于 40%时,JVM就会增大堆直到 -Xmx的最大限制;空余堆内存大于 70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置 -Xms、 -Xmx相等以避免在每次 GC 后调整堆的大小。可以利用 JVM提供的 -Xmn -Xms -Xmx等选项可进行堆内存设置,一般的要将 -Xms和 -Xmx选项设置为相同,而 -Xmn为1/4的 -Xmx值,建议堆的最大值设置为可用内存的最大值的 80%。初始化堆的大小是 JVM在启动时向系统申请的内存的大小。一般而言,这个参数不重要。但是有的应用程序在大负载的情况下会急剧地占用更多的内存,此时这个参数就是显得非常重要,如果 JVM启动时设置使用的内存比较小而在这种情况下有许多对象进行初始化,JVM 就必须重复地增加内存来满足使用。由于这种原因,我们一般把 -Xms和 -Xmx设为一样大,而堆的最大值受限于系统使用的物理内存。一般使用数据量较大的应用程序会使用持久对象,内存使用有可能迅速地增长。当应用程序需要的内存超出堆的最大值时 JVM就会提示内存溢出,并且导致应用服务崩溃。所以,如果 Xms超过了 Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。

【栈】:栈是线程私有的内存区域,每个方法执行的时候都会在栈创建一个栈帧,方法的调用过程就对应着栈的入栈和出栈的过程。每个栈帧的结构又包含局部变量表、操作数栈、动态连接、方法返回地址。局部变量表用于存储方法参数和局部变量。当第一个方法被调用的时候,他的参数会被传递至从0开始的连续的局部变量表中。操作数栈用于一些字节码指令从局部变量表中传递至操作数栈,也用来准备方法调用的参数以及接收方法返回结果。动态连接用于将符号引用表示的方法转换为实际方法的直接引用。

元数据/方法区也叫永久保存的区域,用于存放 Class和 Meta信息,Class在被 Load的时候被放入该区域。它和存放类实例(Instance)的 Heap区域不同,GC(Garbage Collection)不会在主程序运行期对 PermGen space 进行清理。JVM使用 -XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的1/4。 GC不会对 PermGen space进行清理,所以如果你的APP会 Load很多 Class的话,就很可能出现 PermGen space错误。元数据与方法区的区别就是,元数据在堆外分配的内存,方法区在对内分配的内存。

JVM内存限制】(最大值)首先 JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就 32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说 Windows系统下为1.5G-2G,Linux系统 下为2G-3G),而 64bit以上的处理器就不会有限制了。

【本地方法栈】:主要用于执行本地 native方法的区域。
【程序计数器】:也是线程私有的区域,用于记录当前线程下虚拟机正在执行的字节码的指令地址。

三、JVM 内存为什么分成新生代,老年代,持久代。新生代中为什么要分为 Eden 和 Survivor


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

四、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)

五、你知道的垃圾收集器,各自的优缺点,重点讲下 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(不需要连续)的集合。

六、垃圾回收算法的实现原理


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


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

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

【4】分代思想:根据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选择合适的收集算法:少量对象复制算法,大量对象标记-压缩或标记-清除算法。

七、当出现了内存溢出,你怎么排错


首先分析是什么类型的内存溢出,对应的调整参数或者优化代码。链接

八、JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等


【1】重排序:在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。(譬如:a=1;b=2;重排后b=2;a=1;)。
  ◀ 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  ◀ 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  ◀ 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    
【2】内存屏障(Memory Barrier):又称内存栅栏,为了保障执行顺序和可见性的一条 cpu 指令。
  ◀ 保障执行顺序:编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
  ◀ Memory Barrier 保证可见性是指:强制刷出各种 CPU cache,如一个 Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入cache 的数据,因此,任何 CPU上 的线程都能读取到这些数据的最新版本。例如:Volatile 就是基于 Memory Barrier 实现的。
【3】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。
【4】主内存:共享变量存储的区域即是主内存。
【5】工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本。

九、简单说说你了解的类加载器,可以打破双亲委派么,怎么打破


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

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

十、讲讲 JAVA 的反射机制


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

十一、你们线上应用的 JVM 参数有哪些


标配参数(了解):-version 等等。X参数(了解):-Xmixed 等等。XX参数:下面主要学的参数。

【1】Boolean 类型:格式<-XX:+/-某个属性值>,+表示开启,-表示关闭。
  问题:如何查看一个正在运行中的 java程序,它的某个 jvm参数是否启动?或者具体的值是多少?展示如下:
 
【2】KV 设置类型:格式<-XX:属性key=属性值value>通过上述方法也可以查看默认参数,同时也可以设置属性值的大小。

【3】除了上述使用的:<jinfo -flag 配置项 进程编号> (查看固定的 jvm 属性),还可以通过 <jinfo -flags 进程编号> 查看所有的默认 jvm Boolean属性是否开启和 KV 属性的值大小。

两个经典的参数:-Xms和-Xmx 也是-XX 参数。因为 -Xms 等价于 -XX:InitialHeapSize,-Xmx 等价于 -XX:MaxHeapSize

盘点家底查看 JVM 默认值 【1】-XX:+PrintFlagsInitial:查看初始默认值(无需程序运行)。命令:java -XX:+PrintFlagsInitial

【2】-XX:PrintFlagsFinal:查看修改更新的值。命令 java -XX:+PrintFlagsFinal -version 修改运行的 java 程序的属性值命令:

java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m 程序名

【3】-XX:+PrintCommandLineFlags:打印命令行参数。命令 java -XX:+PrintCommandLineFlags -version

-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 日志文件的输出路径

十二、G1 和 CMS 区别,吞吐量优先和响应优先的垃圾收集器选择


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

十三、怎么打出线程栈信息


 jstack主要用来查看某个 Java进程内的线程堆栈信息。语法格式如下:

jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip

命令行参数选项说明如下:

-l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况 
-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)

jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在 JVM性能调优中使用得非常多。下面我们来一个实例找出某个Java进程中最耗费 CPU的 Java线程并定位堆栈信息,用到的命令有jps、top、printf、jstack、grep第一步先找出 Java进程ID,拿 tomcat为例:得到进程ID为 21251;

[root@localhost ~]# ps -ef | grep tomcat 

第二步找出该进程内最耗费 CPU的线程,可以使用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid,我这里用第三个,输出如下:

TIME列就是各个 Java线程耗费的CPU时间,CPU时间最长的是线程ID为 21553的线程,用 printf "%x\n" 21553 得到21553的十六进制值为5431,下面会用到。  下一步就轮到 jstack上场了,它用来输出进程 21251的堆栈信息,然后根据线程ID的十六进制值grep,如下:

root@ubuntu:/# jstack 21251 | grep 5431

​ 可以看到CPU消耗在 com.wzc.manage.meta.vehicle.listener.DeviceOnlineMsgListener-1 这个类的Object.wait(),就可以定位到改段代码。

遇到的问题:进程下无内容显示。 解决方案:对权限配置进行了修改,即可打印出线程堆栈信息。

十四、请解释如下 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垃圾收集周期

十五、JVM 垃圾回收的时候如何确定垃圾?是否知道 GC Roots


什么是垃圾:简单的说就是内存中已经不能被使用的空间就是垃圾。

要进行垃圾回收,如何判断一个对象是否可以被回收?
【1】引用计数法(了解)Java 中,引用和对象是有关联的,如果要操作对象则必须用引用进行。因此,可以通过引用来判断一个对象是否可以回收。简单的说,就是给对象添加一个引用计数器,每当有一个地方引用它,计数器加1,每当一个地方失效,计数器减1。任何时刻计数器为0的对象就是不可能再被使用的对象,此时这个对象就是回收对象。但 Java 虚拟机中没有选择这种算法,因为很难解决对象之间相互循环引用的问题。
【2】枚举根节点做可达性分析(根搜索路径)为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法:基本思想就是通过一些列名为“GC Roots”的对象作为起始点(后续介绍什么是GC Roots),从这个被称为 GC Roots 的对象开始向下搜索,如果一个对象与GC Roots 没有任何引用链相连时,则说明对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达的)对象判定为存活,没有被遍历到的就判定为死亡。如下的对象C 就是未遍历到的,因此此对象就是垃圾对象。
 
【3】Java 中可以作为 GC Roots 的对象(很重要,就是GC 标记时的起点)①、虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。②、方法区中的类静态属性引用的对象。③、方法区中常量引用对象。④、本地方法中 JNI(Native 方法)引用的对象。

 1 public class JvmDemo {
 2 
 3     //方法区中的类静态属性引用的对象。
 4     private static SemaphoreDemo semaphoreDemo;
 5 
 6     //方法区中常量引用对象。
 7     private static final Integer A = 1;
 8     public static void m1(){
 9         //虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
10         JvmDemo demo = new JvmDemo();
11     }
12     
13     public static void main(String[] args) {
14         //虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。前提一
15         m1();
16         
17         //本地方法中 JNI(Native 方法)引用的对象。
18         new Thread().start();
19     }
20 }

十六、你平时工作用过的 JVM 常用基本配置参数有哪些


基础回顾:JDK1.8 之后将最初的永久代取消了,由元数据取代。元数据的本质和永久代类似。最大的区别是:永久带使用的是 JVM 的堆内存,元数据并不在虚拟机中而是使用本机物理内存。因此默认情况下,元数据的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入堆中,这样可以加载多少类的元数据就不再有 MaxPermSize 控制,而由系统的实际可用空间来控制。

根据程序获取默认 JVM 初始化的内存大小:

1 public class Test {
2     public static void main(String[] args) {
3         //获取初始的内存大小 -Xms   默认占总内存的1/64
4         Runtime.getRuntime().totalMemory();
5         //获取最大的内存大小 -Xmx   默认占总内存的1/4
6         Runtime.getRuntime().totalMemory();
7     }
8 }

【1】-Xms:初始大小内存,默认为物理内存的 1/64;
【2】-Xmx:最大分配内存,默认为物理内存的 1/4;
【3】-Xss:等价于 -XX:ThreadStackSize,设置单个线程栈的大小,一般默认为512~1024(与操作系统相关);
【4】-Xmn:设置年轻代大小,一般使用默认值;
 
【5】-XX:MetaspaceSize:设置元数据大小。JDK8使用本地内存,默认获取21M左右,因此需要调整。

【6】-XX:+PrintGCDetails:输出详细GC收集日志信息。准备工作:1)、首先设置 VM 参数信息:

  2)、构建垃圾回收代码:我们设置了最大内存为10M,代码中分配20M,因此程序会进行GC

1 public class Test {
2     public static void main(String[] args) {
3         //分配 20M 大小的内存
4         byte[] bytes = new byte[20 * 1024 * 1024];
5     }
6 }

 3)、运行程序后,当进行垃圾回收时,会打印如下 GC 的详细信息:

[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->939K(9728K), 0.0386935 secs] [Times: user=0.00 sys=0.00, real=0.04 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 495K->852K(7168K)] 983K->852K(9728K), [Metaspace: 3308K->3308K(1056768K)], 0.0081041 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

 4)、GC 打印的信息解释如下:

 5)、FullGC 打印的信息解释如下:

【7】-XX:SurvivorRatio:设置新生代中Eden 和 S0/S1 空间的比例,默认 8:1:1。例如:-XX:SurvivorRatio=4,表示比例为4:1:1,SurvivorRatio 值就是 Eden 区的比例占多少。
【8】-XX:NewRatio:配置年轻代与老年代在堆结构的占比。默认=2,新生代占1,老年代占2,年轻代占整个堆的1/3。例如设置此值 -XX:NewRatio=4,新生代占1,老年代占4,年轻代占整个堆的1/5。
【9】-XX:MaxTenuringThreshold:设置垃圾最大年龄,默认=15(从年轻代到老年代)。如果设置为0,则年轻代对象不经过Survivor 区,直接进入老年代。对于老年代较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代会在 Survivor 区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论。此值设置的区间为0~15(JDK8)。

十七、谈谈你对 OOM 的认识


【1】java.lang.StackOverflowError:该异常表示栈溢出(表示方法的深度不够);

1 @org.junit.Test
2 public void test(){
3     //java.lang.StackOverflowError
4     stackOverflowError();
5 }
6 
7 public void stackOverflowError(){
8     stackOverflowError();
9 }

【2】java.lang.OutOfMemoryError: Java heap space:该异常表示堆溢出,无限循环的 new 对象就会导致此问题;
【3】java.lang.OutOfMemoryError: GC overhead limit exceeded:GC 回收时间过长时会抛出 OutOfMemoryError。过长的定义是,超过98% 的时间来做 GC并且回收了不到2%的堆内存。连续多次GC,都只回收了不到2% 的极端情况下才会抛出。加入不抛出 GC overhead limit 错误会发生什么情况?那就是 GC清理完后,这点内存很快会再次填满,迫使GC 再次执行,这样就形成了恶性循环,CPU 的使用率永远都是 100%,而GC却没有任何成果。

 1 //JVM 参数配置演示:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 2 @org.junit.Test
 3 public void test(){
 4     ArrayList<String> list = new ArrayList<>();
 5     int i = 0;
 6 
 7     while(true){
 8         list.add(String.valueOf(++i).intern());
 9     }
10 }

输出结果展示:

【4】java.lang.OutOfMemoryError: Direct buffer memory:直接内存溢出。例如:ByteBuffer.allocateDirect(capability) 分配操作系统本地内存,不属于 GC管理范畴。但如果不断的分配本地内存,当本地内存使用光的时候,再次尝试获取本地内存就会出现 OutOfMemoryError,那程序就直接崩溃了。

1 //JVM 参数配置演示:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
2 @org.junit.Test
3 public void test(){
4     //分配 6m 的直接内存,上面设置运行环境的最大直接内存为 5m
5     ByteBuffer.allocateDirect(6*1024*1024);
6 }

输出结果展示:

【5】java.lang.OutOfMemoryError: unable to create new native thread:不能够创建新的线程。高并发服务经常出现的一个异常。准确的讲该 native thread 异常与对应的平台有关。
导致原因:1)、你的应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限。2)、你的服务器并不允许你的应用程序创建这么多线程,Linux 系统默认允许单个进程可以创建的线程个数是 1024个,你的应用创建超过这个数字,就会报错。
解决办法:1)、想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数量降到最低。2)、对于有的应用,确实需要创建很多线程,远超过 Linux 默认的1024个线程,可以通过修改 Linux 服务器配置,扩大 Linux 默认限制。

 1 //Linux 系统中运行如下代码
 2 public class Test {
 3     public static void main(String[] args) {
 4         for (int i=0;;i++){
 5             new Thread(()->{
 6                 try {
 7                     TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
 8                 } catch (InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11             }).start();
12         }
13 }

【6】java.lang.OutOfMemoryError: Metaspace:元空间溢出(JDK8之后使用元空间代替了永久代)。Metaspace 是方法区在 HotSpot 中的实现,它与持久代最大的区别在于:Metaspace 并不在虚拟机内存中而是使用本地内存。永久代存放了一下信息:
   ■ 虚拟机加载的类信息;
   ■ 常量池;
   ■ 静态变量;
   ■ 即时编译后的代码;

十八、JVM内存模型,为什么 survivor需要有2个


survivor 需要2个,主要是为了规避内存碎片化。

十九、new 一个对象的过程


​ 
当虚拟机遇见 new关键字时候,首先判断当前类是否已经加载,如果类没有加载,首先执行类加载机制,加载完成后再为对象分配空间、初始化等。
【加载】:就是从字节码加载成二进制流的过程;
【验证】:当然加载完成之后,当然需要校验 Class文件是否符合虚拟机规范,跟我们接口请求一样,第一件事是先做参数校验;
【准备】:为静态变量、常量赋默认值;
【解析】:把常量池中符号引用(以符号描述引用的目标)替换为直接引用(指向目标的指针或者句柄等)的过程;
【初始化】:执行 static代码块(cinit)进行初始化,如果存在父类,先对父类进行初始化;

静态代码块是绝对线程安全的,只能隐式被 java虚拟机在类加载过程中初始化调用!(此处该有问题 static代码块线程安全吗?)

类加载完成后,就是对象分配内存空间和初始化的过程:首先为对象分配合适大小的内存空间,接着为实例变量赋默认值,设置对象的头信息,对象 hash码、GC分代年龄、元数据信息等,执行构造函数(init)初始化。

二十、什么时候会触发 YGC和 FGC


当一个新的对象来申请内存空间的时候,如果 Eden区无法满足内存分配需求,则触发YGC,使用中的 Survivor区和 Eden区存活对象送到未使用的 Survivor区,如果 YGC之后还是没有足够空间,则直接进入老年代分配,如果老年代也无法分配空间,触发FGC,FGC之后还是放不下则报出 OOM异常。

YGC之后,存活的对象将会被复制到未使用的 Survivor区,如果S区放不下,则直接晋升至老年代。而对于那些一直在 Survivor区来回复制的对象,通过 -XX:MaxTenuringThreshold配置交换阈值,默认15次,如果超过次数同样进入老年代。此外,还有一种动态年龄的判断机制,不需要等到 MaxTenuringThreshold就能晋升老年代。如果在 Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

二十一、频繁 FullGC怎么排查


这种问题最好的办法是结合具体的例子分析,如果没有就说一般的分析步骤。发生 FGC有可能是内存分配不合理,比如 Eden区太小,导致对象频繁进入老年代,这时候通过启动参数配置就能看出来,另外有可能就是存在内存泄露,可以通过以下的步骤进行排查:jstat -gcutil或者查看 gc.log日志,查看内存回收情况

S0 S1 分别代表两个Survivor区占比,E代表 Eden区占比,图中可以看到使用78%,O代表老年代,M代表元空间,YGC发生54次,YGCT代表YGC累计耗时,GCT代表GC累计耗时。

[GC [FGC 开头代表垃圾回收的类型,
PSYoungGen: 6130K->6130K(9216K)] 12274K->14330K(19456K), 0.0034895 secs代表 YGC前后内存使用情况。
Times: user=0.02 sys=0.00, real=0.00 secs,user表示用户态消耗的 CPU时间,sys表示内核态消耗的 CPU时间,real表示各种时钟的等待时间。
这两张图只是举例并没有关联关系,比如你从图里面看能到是否进行FGC,FGC的时间花费多长,GC后老年代,年轻代内存是否有减少,得到一些初步的情况来做出判断。dump出内存文件在具体分析,比如通过 jmap命令 jmap -dump:format=b,file=dumpfile pid,导出之后再通过 Eclipse Memory Analyzer等工具进行分析,定位到代码,修复这里还会可能存在一个提问的点,比如 CPU飙高,同时 FGC怎么办?办法比较类似找到当前进程的 pid,top -p pid -H 查看资源占用,找到线程 printf “%x\n” pid,把线程 pid转为16进制,比如 0x32d,jstack pid|grep -A 10 0x32d查看线程的堆栈日志,还找不到问题继续。dump出内存文件用 MAT等工具进行分析,定位到代码,修复。


----关注公众号,获取更多内容----

posted @ 2020-11-18 18:01  Java程序员进阶  阅读(53)  评论(0编辑  收藏  举报