JVM面试题整理-Java内存区域与内存溢出异常、垃圾收集器和内存分配策略

参考:
https://blog.csdn.net/zd836614437/article/details/64126826
https://blog.csdn.net/u011225629/article/details/49000311
http://www.jsondream.com/2016/12/01/jvm-class-load-object-is-live.html
https://blog.csdn.net/zd836614437/article/details/64126826

1、Java虚拟机内存(运行时数据区域)的划分,每个区域的功能

关于JVM 运行时内存划分的实例参考:
http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referral
这里写图片描述
        概括地说来,JVM初始运行的时候都会分配好Method Area(方法区)和Heap(堆),它们都是物理上可以不连续,但是逻辑上连续的内存区域。而JVM 每遇到一个线程,就为其分配一个Program Counter Register(程序计数器), VM Stack(虚拟机栈)和Native Method Stack (本地方法栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。

  • 程序计数器(Program Counter Register)(线程私有):
    1. 作用:由于Java虚拟机的多线程机制是通过线程轮流切换和分配CPU执行时间来实现的,为了线程切换后能够恢复到正确的执行位置,每条线程都有独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
    2. 是一块较小的内存空间,谁线程创建而创建,是当前线程所执行字节码的行号指示器。如:线程正在执行的是Java方法,那么计数器记录的就是当前正在执行的虚拟机字节码指令的地址;线程正在执行的是Native方法,则这个计数器值为空(Undefined)。
    3. 是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError的区域。
  • Java虚拟机栈(Java Virtual Machine Stacks)(线程私有):
    1. 作用:也没成为“栈”,为虚拟机执行java方法(字节码)服务,是描述Java方法执行的模型:每个方法执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    2. 异常:1、StackOverflowError:当线程访问的栈深度大于虚拟机允许的深度时抛出该异常;2、OutOfMemoryError:如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出该异常。
      线程虚拟机栈示意图
  • 本地方法栈(Native Method Stack)(线程私有)
    1. 作用:和虚拟机栈类似,java虚拟机栈是为虚拟机执行Java方法服务的,本地方法栈是为虚拟机使用到的Native方法服务。
    2. Java虚拟机规范没有对本地方法栈中的方法使用的语言、使用方法与数据结构没有强制规定,可以自由实现。Sun HotSpot直接把本地方法栈和虚拟机栈合二为一。
    3. 异常:StackOverflowError和OutOfMemoryError。(原理和虚拟机栈一样)
  • Java堆(Java Heap)(线程共享)
    1. 作用:被所有线程共享,Java虚拟机启动时创建,几乎所有的对象实例都存放在堆中。
    2. 特点:虚拟机管理的内存中最大的一块;是GC管理的主要区域,所以又名“GC堆(Garbage Collection Heap)”;逻辑上连续,物理上可以不连续;可以动态扩展;如果堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OutOfMemoryError。
    3. JVM堆内存划分(分代)
  • 方法区(Method Area)(线程共享)
    1. 作用:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量(final)、静态变量(static)、即时编译器编译后的代码等数据。
    2. Sun HotSpot虚拟机中把方法区叫做永久代(Permanent Generation)
    3. 异常:受到方法区的限制,抛出OutOfMemoryError。
  • 运行是常量池(Runtime Constant Pool,简称 RCP)

    1. 作用:是方法区的一部分,即为方法区的运行时常量池,常量池是这个类型(类)用到常量的一个有序的集合,这些常量包括:所有基本类型、String、其他类型(类和接口的全限定名)、其他方法(方法的名称和描述符)、其他字段(字段的名称和描述符)的符号引用。

    2. 常量池例子:Integer.valueOf中有个内部类IntegerChache,他实现了Integer类的运行时常量池,它维护了一个chache[256]数组,用来存放-128-127之间的常量。在IntegerChache中的static block(静态块)中把chache进行初始化成-128-127对应的Integer对象。当使用Integer.valueOf(num)或者Integer i = num (在编译时封装成Integer i1=Integer.valueOf(num))时,如果 -128<num <127,则返回Integer类常亮池(缓冲数据)中找到等于该值得引用并返回。为什么叫运行时常量池?因为只有类被触发并加载到内存中时,才会创建这个常量池(缓存),所以叫运行时常量池(个人瞎掰)。还有String.intern()也有存放字符串的常量池。
      可以参考的运行时常量池实例http://www.cnblogs.com/DreamSea/archive/2011/11/20/2256396.html

    3. 异常:运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时,抛出OutOfMemoryError。

2、OOM错误、StackVoerflowError、Permgen space错误

Java内存泄漏

        在Java中,内存泄漏就是内存中存在一些被分配的对象,这些对象具有以下两个特点:a、这些对象是可达的,即GC Roots到对象之间有引用链相连,那么GC无法回收。b、这些对象是无用的,即程序以后不会再用到这些对象。满足了这两个条件的对象被判定为内存泄漏(无用但是占用内存,又不能被GC回收的对象称为内存泄漏)。
这里写图片描述

OutOfMemoryError(OOM)异常

        Java虚拟机规范中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。

  • Java堆异常:
    1. 原因:Java堆用于储存对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象占用空间达到最大堆的容量限制后就会产生OOM。
    2. 堆溢出判断:异常栈信息“java.lang.OutOfMemoryError”后面会提示“Java heap space”,即:java.lang.OutOfMemoryError:Java heap space。
    3. 处理该错误:
          3.1. 通过参数-XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照
          3.2. 用内存映像分析工具(如:Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。找出原因是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
          3.3. 如为内存泄漏(Memory Leak),则通过工具查看对象到GC Roots的引用链,找出泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。掌握了泄漏对象的类型信息及GC Roots引用链的信息后,就可以比较准确的定位出泄漏代码的位置,进行修改。
          3.4. 如为内存溢出(Memory Overflow),则说明内存中的对象都是有用的对象,必须存活着。改进方法:a、根据机器物理内存调整虚拟机堆参数(-Xmx(堆最大值)和-Xms(最小值)),来扩大堆内存。b、检查代码中是否存在某些对象生命周期过长、持有状态时间过长等情况,减少程序运行期的内存消耗
  • 虚拟机栈和本地方法栈溢出
    1. 原因:建立过多线程导致内存溢出OOM。操作系统分配给每个进程的内存空间是有限的,每个线程分配到的栈容量越大,可以建立的线程数就越少,建立线程时容易把空间耗尽,出现OOM。分配给虚拟机栈和本地方法栈的内存空间是从剩余空间中瓜分的。 剩余空间 = 机器总内存 - Java最大容量堆(Xmx) - 最大方法区容量(MaxPermSize)- 程序计数器消耗(很小)- 虚拟机进程本身消耗。
      PS:虚拟机只提供了用户对Java堆方法区这两块内存区域的管理方法(提供相应参数调整内存大小),其他的内存区域由操作系统管理。
    2. 溢出判断:异常栈信息“java.lang.OutOfMemoryError”后面跟着会提供具体原因,比如:unable to create new native thread。参看JVM P56
    3. 处理该错误:在不能减少线程数和更换64位虚拟机的情况下,只能通过减小Java堆的最大堆容量和方法区的容量来换取更多的线程可利用的空间-虚拟机栈。这种方式称之为“减少内存”。

PermGen space异常

        在HotSpot虚拟机中方法区被称为“永久代”(Permanent Generation),所以运行过程中抛出PermGen space异常就是指方法区或运行时常量池(是方法区的一部分)发生了OOM,即方法区或者运行时常量池发生OOM会抛出PermGen space提示信息。PermGen space异常是发生在方法区的OutOfMemoryError。

  1. 原因:
    a、运行时常量池溢出,比如使用List保持对常量池引用,避免FullGC回收常量池,导致常量池OOM 参看 JVM P56。
    b、方法区用于存放Class的相关信息,运行时当大量的类需要加载,把方法区填满后,就会发生OOM。
  2. 方法区填满溢出场景:
    a、主流框架Spring、Hibernate(经常动态生成大量Class的应用)在对类进行增强时,会使用GCLib这类字节码增强技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
    PS:CGLib对类增强:运行中以该类作为父类生成该类的动态代理子类,动态代理子类中可以添加方法达到增强类的效果。
    b、运行在JVM上的动态语言(如Groovy)、大量JSP或者动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一类文件,被不同的加载器加载也会视为不同的类)。
  3. 解决方案:经常动态生成大量Class的应用中,应该特别注意类的回收,由于一个类是否要被垃圾回收器回收掉,判定条件比较苛刻,所以方法区溢出是一种常见的内存溢出异常。

StackOverflowError异常

  1. 原因:在单线程的情况下,当栈帧太大或者虚拟机栈容量太小,持续调用方法,向栈中放入栈帧导致线程所请求的栈深度大于虚拟机允许的最大栈深度时,虚拟机抛出StackOverflowError。
  2. 解决方案:出现StackOverflowError异常时有错误堆栈可以阅读,直接查看抛出的异常信息,能够比较容易找到问题所在。
    PS:虚拟机的默认设置下,栈深度在大多数情况下达到1000-2000万全没有问题,对于正常的方法调用(包括递归),这个深度是够用的。

3、JVM内存分带和垃圾回收

        垃圾回收器基本都采用分代收集策略,这是基于:不同的对象生命周期不一样。因此,内存进行代的划分,把不同生命周期的对象放在不同代,不同代上采用最合适的垃圾回收方式进行回收,以提高回收效率。
参考:http://www.importnew.com/19255.html

如何分代

这里写图片描述
        如图所示,虚拟机共划分为三个代:年轻代(Yong Generation)、老年代(Old Generation)和持久代(Permanent Generation)。其中的持久代指的就是方法区,持久代的内存回收目标主要是针对常量池的回收和对类的卸载,垃圾回收难度大,效果不理想。垃圾回收主要针对年轻代和老年代(Java 堆)中的对象回收。
新生代(年轻代)
        新生成的对象首先放在新生代中(如果虚拟机设置了PretenureSizeThreshold(直接晋升到老年代对象的大小),那么大于该值的对象直接在老年代中生成)。由于大多数对象都符合“朝生夕灭”的特点,所以新生成的对象放在新生代中,垃圾回收时能尽可能快速的收集掉这些生命周期短的对象。
        新生代还可以继续细分为三个区:一个Eden区和两个Survivor区(From Survivor区和To Survivor区),Eden区和Survivor区的比例可以通过SurvivorRatio来调节,默认为8。
老年代
        在年轻代中经历了N次(MaxTenuringThreshold所指定的晋升到老年代的阈值)垃圾回收(Mirror GC)的对象,就会被放到老年代中。因此,老年代中存放的都是一些生命周期较长的对象。比如Http请求中的Session对象、线程对象、Socket连接对象它们和业务直接挂钩,生命周期比较长。
永久带(持久代)
        只有HotSpot虚拟机存在永久代(使用永久代来实现方法区,把GC分代管理扩展到方法区中),其他的虚拟机不存在永久代。可以通过-XX:MaxPermSize = <N>设置永久代的上限。从JDK1.7开始HotSpot开始逐步放弃永久代,开始使用Native Memory来实现方法区。
PS:虚拟机规范中没有规定方法区的实现细节,不同的虚拟机有不同的实现方式,HotSpot采用永久代来实现方法区。

分代垃圾回收

由于进行了分代处理,针对不同代采用合理的垃圾回收机制。
Minor GC

  • 特点:新生代中的垃圾收集动作,GC次数频繁(使用速度快、效率高的算法),时间较短,采用复制算法
  • 何时触发:年轻代满时(指Eden区满,Survior区满不会引发GC,但是Survivor满可以进行分配担保)会触发Minor GC一般情况下,生成新对象时在Eden申请空间失败时,就会触发Mirror GC。回收时会将Eden区和Survior区©中还存活的对象一次性复制到另外一块Survivor空间上,然后清理掉Eden区和Suvivor区©的空间。

Full GC

  • 特点:Full GC同时回收年轻代、老年代和永久带的内存。对整个内存区域进行回收,Full GC发生频率低于Minor GC,Time(Full GC) > Time(Minor GC ),采用标记-清除/标记-整理算法。因此要尽量减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对Full GC的调节。
  • 何时触发:老年代(Tenured)满、永久带(Perm)满、System.gc()被显示调用、上次GC之后Heap的各域分配策略动态变化。

几条普通的(生成对象)内存分配规则

参考:JVM p91-p100

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间分配担保

4、Java 8内存分带改进

        在JDK1.8中,HotSpot已经没有永久代了,取而代之的是元空间(Metaspace),元空间和永久代类似,都是对JVM规范中的方法区的一种实现,方法区被移动至Metaspace,字符串常亮移动至Java Heap。

移除永久带的原因(元空间的优点):

  • 由于Permanent Generation内存经常不够用或者发生内存泄漏,常常引发Java.lang.OutOfMemoryError: PermGen异常。由于JVM在启动时会根据-XX:PermSize以及-XX:MaxPermSize配置来分配一块内存,但是随着动态类加载越来越多,内存空间不够用,时常发生OutOfMemoryError,开发者不知道设置为多大算合适。基于这个原因,希望内存的管理不再受到限制,产生了Metaspace,Metaspace使用本地内存,默认只受本地内存大小的限制,即:本地内存剩余的都是元空间的。那么元空间的这种思想就解决了内存受限问题。
  • 由于Oracle可能会将JRockit VM和HotSpot JVM合并,然而JRockit中没有Permanent Generation,所以移除它可以促进HotSpot JVM和JRockit VM的合并。
            虽然元空间只受本地内存的限制,但是也不能让它无限大,提供了以下参数约束Metaspace:
    相关参数详见:https://yq.aliyun.com/articles/73601
    https://www.cnblogs.com/paddix/p/5309550.html

5、说下强引用、软引用、弱应用、虚引用,以及它们和gc的关系

        JDK 1.2之后,Java对引用(Reference)的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Week Reference)、虚引用(Phantom Reference)这4种,这4种引用强度逐渐减弱。Reference中引用的对象Object叫referent。

  • 强引用:强引用就是指在程序中普遍存在的,类似“Object obj = new Object()”,这类引用,只要引用存在,就算内存空间不足了,GC也永远不会回收掉被引用的对象,而是抛出OOM异常。
  • 软引用:描述一些还有用但并非必要的对象。如果内存空间足够,GC就不会回收软引用关联的对象,如果内存空间不足了将要发生OOM异常时,GC将会把referent列入回收范围之中,进行第二次回收。如果回收了这些referent(第二次回收)后内存空间还不足够,则抛出OOM异常。JDK1.2后,提供了SoftReference类来实现软引用。软引用可以和引用队列(ReferenceQueue)联合使用(SoftReference有构造函数可以传入ReferenceQueue来监听GC对referent的处理)。软引用可以用来实现内存敏感的高速缓存(在软引用的referent中添加缓存数据,当内存足够时,就可以直接从内存中取数据,提高运行效率。当内存不足时缓存数据就会被GC回收掉)。
  • 弱引用:也是用来描述非必要对象的,但是它的强度比软引用更弱。更弱体现在:被弱引用关联的对象只能生存到下次GC之前,当发生GC时,会无条件的回收掉这部分内存,即:无论内存是否足够,弱引用关联的到的对象都会被回收掉。JDK1.2后,提供了WeakReference类来实现弱引用,弱引用也可以和引用队列联合使用。
    PS:在GC时,SoftReference和WeakReference先clear对referent的引用(关联),对应的referent变为Finalizable,该referent生存还是死亡,就和是否与finalize()方法相关了。注意在finalize()方法中,referent可以重生(详情参看JVM P66)。然后在把SoftReference/WeakReference放入ReferenceQueue中。
    GC时,PhantomReference引用(关联)的referent先被回收内存,然后再把PhantomReference放入ReferenceQueue中。详情可参考:https://blog.csdn.net/kjfcpua/article/details/8495199
  • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。虚引用的特点:一个对象是否被虚引用关联着,完全不会印象它的生存时间,即:虚引用不像其他三种引用一样会对对象的生存周期存在影响。也无法通过虚引用来获取一个对象实例。虚引用必须与引用队列(ReferenceQueue)联合使用。设置虚引用的唯一用处:被虚引用关联的对象能在被GC回收时收到一个系统通知(因为:当GC准备回收一个对象时,发现该对象被虚引用关联着,就会在回收该对象之前,把该对象关联的虚引用加入到虚引用关联的引用队列中,通过访问虚引用队列,那么就可以采取一些行动)。

6、垃圾回收算法(标记-清除、复制、标记-整理、分代收集)

参考:http://www.jsondream.com/2016/12/01/jvm-class-load-gc-arithmetic.html

7、垃圾收集器(重点讲CMS、G1)

参考:http://www.jsondream.com/2016/12/02/jvm-garbage-collector.html

8、JVM中一次完整的GC流程(从YGC到FGC)是怎样的,重点讲讲对象如何晋升到老年代,其中主要的JVM参数等

5、JVM常用参数
9、GC的引用可达性分析算法中,哪些对象可作为GC Roots对象?

猜你喜欢

转载自blog.csdn.net/u011523796/article/details/80182583
今日推荐