JVM内存模型

JVM :

Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙 墙外的人想进去 墙里的人想出来

  1. 运行时数据区 :
    在这里插入图片描述

    • 程序计数器:是一块较小的内存空间 它的作用就是看做当前的线程所执行的字节码的行号的指示器。 java中的虚拟机的多线程是通过线程轮流切换并分配处理器执行的时间的方式来实现的程序计数器是线程私有的。各程序计数器之间是相互不影响的 如果线程执行的是java方法 程序计数器记录的是正在执行的虚拟机字节码指令的地址 要是正在执行的是native方法 计数器则为空 本地方法栈是没有规定OutOfMemoryError
    • Java虚拟机栈:也是线程私有的 虚拟机栈的生命周期和线程相同 每一个方法执行的时候都会创建一个栈帧 用于存储局部变量表 操作栈 动态链接 方法出口等信息。 方法调用的过程就是虚拟机栈的从入栈到出栈的过程(注意的一点就是方法执行完成时出栈不是由GC进行销毁的)局部变量表存放的就是编译时期可知的各种基本数据类型 (8种)和 引用数据类型 需要注意的是long类型 和 double类型会占用2个局部变量空间其余的占一个空间 虚拟机栈中规定了两种异常 :一种就是线程请求的最大栈深度超过了 虚拟机中允许的深度 会出现StackOverflowError 要是无法申请到足够的内存时会抛出OutOfMenoryError异常。
    • 本地方法栈:和虚拟机栈的作用非常的相似 但是虚拟机栈执行的是java方法(也就是字节码) 但是本地方法栈中执行的是虚拟机使用到的native方法
    • Java中的堆:是虚拟机所管理的内存中最大的一块 也是所有的线程共享的一块区域 所有的对象实例 和 数组都在堆上分配 (但是现在随着JIT 的编译器的发展导致不是绝对的) Java中堆是垃圾回收器所管理的主要区域 现在的垃圾回收器使用的基本上都是分代收集算法 根据Java虚拟机规范规定:Java堆可以是处于物理上不连续的内存空间中 要是堆中没有内存完成对象的实例的分配(同时堆也无法进行扩展) 就是抛出OutOfMenoryError异常
    • 方法区: 同Java堆一样也是线程共享的内存区域存储已经被虚拟机加载的类信息 常量 和 静态变量 即时编译器编译后的代码等数据 方法区 是堆的一个逻辑部分 方法区中无法满足内存分配时将抛出OutOfMenoryError异常。有人也将方法去称为永久代 但是这两者不等价
    • 运行时常量池: 方法区中的一个区域 常量池中存储区编译期生成的各种字面量 和 符号引用 运行常量池中最重要的一个特性就是具备动态性
    • 对象访问: 对象访问的方法主流的有两种一种就是使用句柄 另一种就是直接指针
      • 要是使用句柄的形式 Java堆中将会划分出一块内存最为句柄池 引用中存储的是对象的句柄地址 句柄中包含了对象实力数据 和 类型数据各自的具体地址信息
        在这里插入图片描述
      • 另一种就是使用直接指针的形式 引用中直接存储的是对象地址
        在这里插入图片描述
      • 这两种形式的优缺点:
        • 使用句柄的访问方式 引用中存储的是稳定的句柄地址 在对象移动的时候只会改变句柄中实例数据指针 而引本身不需要进行修改
        • 但是使用直接指针的方式就是速度快 能节省一次指针定位的时间 Java中对象的访问是非常的频繁的
  2. GC(需要注意的就是垃圾回收器不是Java独有的 也不是Java所伴生的 )

    • 管理内存的两种方式:
      • 一种就是引用计数法:就是给对象添加一个引用计数器 每有一个地方引用它 计数器的值就加一 当引用失效的时候 计数器的值减一 引用计数器为零的时候就表示对象不在被引用 但是这样的方式最致命就是没有办法解决对象之间的循环引用问题
      • 第二种方式就是使用根搜索算法:来判定对象是否是存活 就是通过一系列的“GC Roots”对象来作为起始点 从这些开始向下搜索 走做的路径就是成为引用链 要是一个对象没有任何引用链相连接 就证明这个对象是不可用的 这样的方式能解决循环引用的问题(这样的方式也就是常说的引用可达法)
        • 在Java语言中 可以作为GC Roots对象包括以下几种:
          • 虚拟机栈中所引用的对象
          • 方法区中类静态属性引用的对象
          • 方法区中常量引用的对象
          • 本地方法栈中引用的对象(一般的就是Native方法)
    • Java中引用的类型(四种):
      • 强引用: 就是使用new关键字进行创建的对象的这类引用 只要强引用还存在垃圾回收器就永远不会回收掉的引用 哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收 只有显示的设置为 null 才能适时的进行回收
        Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
        obj = null;  //手动置null
        
      • 软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等 java.lang.ref.SoftReference类进行表示
      • 弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收 也就是说弱引用关联的对象只能生存在下一次垃圾回收发生之前在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用
      • 虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
    • 生存还是死亡 --缓刑阶段:finalize()方法:
      • 判断一个对象死亡所经历的过程:
        • 第一步:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行” 也就是判断对象已经确定死亡
        • 第二步:要是判断有必要实行finalize方法 也就是标题中的缓刑阶段:如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了 最后的一点就是不要使用finalize方法 进行资源的关闭
      • Finalize方法具有以下四个特点:
        • 永远不要主动调用某个对象的finalize方法,该方法应该交给垃圾回收机制调用(非常重要的一点)
        • Finalize方法合适被调用,是否被调用具有不确定性,不要把finalize方法当做一定会执行的方法
        • 当JVM执行课恢复对象的finalize方法时,可能是改对象或系统中其他对象重新变成可达状态
        • 当JVM调用finalize方法出现异常时,垃圾回收机制不会报告异常,程序继续执行
    • 判断一个类是否是无用类:
      • 该类的所有的实例都已经被回收 也就是java堆中不存在该类的实例
      • 加载该类的ClassLoader已经被回收
      • 该类对应的Class对象在任何地方都没有引用 也就是在任何地方无法通过反射进行访问该类方法
    • 垃圾收集算法:
      • 标记-清除算法(Mark-Sweep)标记-清除算法从根集合(GC ROOTS)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片 这就是缺点之一 另一点就是效率问题 标记和 清除的效率较低
        在这里插入图片描述
      • 复制算法(copying)复制算法的提出是为了克服句柄的开销和解决内存碎片的问题(也就是为了消除上面的标记 -清除算法的缺点)它开始时把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就是从根集合中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞)但是这样的算法的缺点也是十分的明显 就是将原有的可使用内存空间减少了一半 只有一半的空间时可以使用的 现在商用的虚拟机使用这种算法进行新生代的回收
        在这里插入图片描述
      • 标记-整理算法(Mark-compact)标记整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有活动对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是解决了内存碎片的问题
        在这里插入图片描述
      • 分代收集算法:分代收集算法是目前大部分jvm的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)也称为方法区 老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最合适的收集算法
        • 年轻代的回收算法(回收主要以copying为主 上面已经提到过了)
          • 所有新生的对象首先都是放在年轻代的,年轻代的目标就是尽可能快速的收集那些生命周期短的对象
          • 新生代内存按照8:1:1的比例分为Eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将Eden区存活的对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也放满了时,则将Eden区和Survivor0区存活的对象复制到另一个Survivor1区,然后清空Eden区和这个Survivor0区,此时Survivor0区是空的,然后将survivor0区和Survivor1区交换,即保持Survivor1区为空,如此往复
          • Survivor1区不足存放Eden区和Survivor0区存活的对象时就将存活对象直接存放到老年代(这里进行的是分配担保)若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收
          • 新生代发生的GC也叫做Minor GC,Minor GC发生频率比较高(不一定等Eden区满了才触发)
        • 老年代(old Generation)的回收算法(回收主要以Mark-Compact为主)
          • 在年轻代经历了N次垃圾回收然后让然存活的对象,就会被放到老年代。因此可以认为老年代中存放的都是生命周期比较长的对象
          • 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高
        • 持久代(Permanet Generation)的回收算法
          • 用于存放静态文件,如java类,方法等。持久代堆垃圾回收没有明显影响,但是有些应用可能动态生成或调用一些class,例如Hibernate等,这时候需要设置一个比较大的持久代空间存放这些运行过程中新增的类,持久代也称方法区
    • 垃圾回收器 - 垃圾收集 算法的实现 下图中是HotSpot虚拟机中所包含的垃圾回收器(不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别)
      在这里插入图片描述
      • 不同种垃圾回收器的详解:
        • Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了(新生代采用复制算法,老生代采用标志整理算法)。大家看名字就知道这个收集器是一个单线程收集器了 它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” :将用户正常工作的线程全部暂停掉),直到它收集结束 优点就是 简单高效 没有线程交互锁带来的开销 缺点也是 明显的 会将所有的工作线程停掉
          • 特点 针对新生代;采用复制算法;单线程收集;进行垃圾收集时,必须暂停所有工作线程,直到完成;
            在这里插入图片描述
        • ParNew收集器: ParNew垃圾收集器是Serial收集器的多线程版本 和 Serial所使用的的控制参数 收集算法 Stop The World 对象分配规则 回收策略 都是一样的 ParNew垃圾收集器在单CPU甚至是两个CPU环境中都不一定能超越Serial收集器 但是随着CPU数量的增加 对系统资源的利用还是好处的 这也是它在server环境中作为首选的新生代垃圾回收器的原因
          在这里插入图片描述
        • Parallel Scavenge收集器:Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。 Parallel Scavenge收集器关注点是吞吐量(如何高效率的利用CPU)CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。(吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间) 这种垃圾回收器适合在后台运算不需要太多交互的任务
        • Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法 主要意义也是在于给Client模式下的虚拟机使用 如果在Server模式下,那么它主要还有两大用途: 一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用 另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
          在这里插入图片描述
        • Parallel Old收集器: Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本 JDK1.6中才开始提供 特点就是:针对老年代 采用"标记-整理"算法 多线程收集 在吞吐量优先的时候可以优先考虑 Parallel Old + Parallel Scavenge收集器
          在这里插入图片描述
        • 并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器 从名字上就知道使用的是标记-清除算法 特点:就是同样针对老年代 基于"标记-清除"算法(不进行压缩操作,产生内存碎片) 以获取最短回收停顿时间为目标 并发收集(非常重要的一点就是CMS收集器 的内存回收过程是与用户线程一起并发的执行的 )、低停顿
          在这里插入图片描述
          • 垃圾回收的过程分为四个步骤:
            • 初始标记(CMS initial mark) 仅标记一下GC Roots能直接关联到的对 速度很快 但是同样需要"Stop The World";
            • 并发标记(CMS concurrent mark) 进行GC Roots Tracing的过程 刚才产生的集合中标记出存活对象 应用程序也在运行 并不能保证可以标记出所有的存活对象
            • 重新标记(CMS remark) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短 采用多线程并行执行来提升效率;
            • 并发清除(CMS concurrent sweep)回收所有的垃圾对象
          • CMS回收器的缺点:
            • 对CPU资源非常敏感 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低 CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受
            • 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败 从而导致另一次的Full GC 的 发生 浮动垃圾:就是CMS进行并发清理的时候用户线程还在运行 伴随着线程的运行 可能会有新的垃圾产生 CMS 无法在本次收集中处理 只能等待到下一次的GC 再将其进行清理掉 这一部分垃圾就叫做是浮动垃圾
            • 产生大量内存碎片 由于CMS基于"标记-清除"算法,清除后不进行压缩操作 会产生大量的空间碎片 (上面已经将的非常的清楚了 这里就是不在赘述)空间碎片较多的时候 将会给对象的分配带来比较大的麻烦 可能会触发一次Full CC
        • G1收集器: G1(Garbage-First)是JDK7才推出商用的收集器在1.6的时候提供的试用 和上面的CMS相比有两个显著的改进: 一就是使用标记-整理算法 这样的话不会产生空间碎片 二就是能非常精确的控制停顿
          • 特点:
            • 并行与并发能充分利用多CPU、多核环境下的硬件优势 可以并行来缩短"Stop The World"停顿时间 也可以并发让垃圾收集与用户程序同时进行;
            • 分代收集,收集范围包括新生代和老年代 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象 虽然保留分代概念,但Java堆的内存布局有很大差别 将整个堆划分为多个大小相等的独立区域(Region) 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合
  3. 内存分配与回收策略

    • 对象的内存分配,从大方向讲就是在堆上分配,对象主要分配在新生代的Eden区上,当然分配的规则并不是固定的,其细节取决于使用的是哪一种收集器组合,还有虚拟机中与内存相关的参数的设置。垃圾收集器组合一般就是Serial+Serial Old和Parallel+Serial Old,前者是Client模式下的默认垃圾收集器组合,后者是Server模式下的默认垃圾收集器组合。

    • Minor GC和Full GC的区别

      • 新生代Minor GC:指发生在新生代的垃圾收集动作,因为Java对象大多数都具有朝生夕灭的特性,多以Minor GC非常频繁,一般回收速度也比较快。
      • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常都伴随着至少一次的Minor GC(但并非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。MajorGC的速度一般会比Minor GC慢10倍以上
    • 对象优先在Eden分配:在大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC

    • 大对象直接进入老年代 所谓大对象就是需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串及数组。虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这些设置值的对象直接在老年代中分配。这样做的目的是在避免Eden区及两个Survivor区之间发生大量的内存拷贝 新生代采用复制算法收集内存Eden区和两个survivor(survivor0,survivor1)区

    • 长期存活的对象将进入老年代 虚拟机既然采用分代收集的思想来管理内存,那么内存回收时就必须能够识别哪些对象应当放在新生代,哪些对象应该放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1.对象在Survivor区每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁)时,就会被晋升到老年代中通过参数 -XX:MaxTenuringThreshold 进行设置

    • 动态对象年龄判定 为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

    • 空间分配担保在发生Minor GC时,虚拟机就会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则改为直接进行一次Full GC.如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC:如果不允许,则也要改为进行一次Full GC(空间分配担保 在复制算法中提到过)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

上山打卤面

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值