JVM概念汇总

2016, Mar 26    

JVM

垃圾收集

它基本上通过暂停它周围的世界来操作,标记所有根对象(由运行线程直接引用的对象),并遵循它们的引用,标记它沿途看到的每个对象

Java基于分代假设-实现了一种称为分代垃圾收集器的东西,该假设表明创建的大多数对象被快速丢弃,而未快速收集的对象可能会存在一段时间

分代描述

  • Young Generation -这是对象的开始。它有两个子代

    • Eden Space -对象从这里开始。大多数物体都是在Eden Space中创造和销毁的。

      • 在这里,GC执行Minor GCs,这是优化的垃圾收集。执行Minor GC时,对仍然需要的对象的任何引用都将迁移到其中一个survivors空间(S0或S1)。
    • Survivor Space (S0 and S1)(from and to)-幸存Eden Space的对象最终来到这里。

      • 其中有两个,在任何给定时间只有一个正在使用(除非我们有严重的内存泄漏)。

      • 一个被指定为空,另一个被指定为活动,与每个GC循环交替。

    • 为什么会有Young Generation?

      • 分代的唯一理由就是优化GC性能。

      • 如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。

    • 为什么要有Survivor区?

      • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。

      • 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。

        • 解决思路

          • 增加老年代空间

            • 更多存活对象才能填满老年代。降低Full GC频率

            • 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长

          • 减少老年代空间

            • Full GC所需时间减少

            • 老年代很快被存活对象填满,Full GC频率增加

      • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

      • 为什么一个Survivor区不行?

        • 刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化

        • 内存碎片化

          • 碎片化带来的风险是极大的,严重影响Java程序的性能。

          • 堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存,就会发现没有足够的内存给予分配。

        • 刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。

          • 这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生

          • 最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

    • 年轻代中的GC

      • 新生代大小(PSYoungGen total 9216K)= eden大小(eden space 8192K)+ 1个survivor大小(from space 1024K)

        • eden : survivor = 8 : 1
      • 复制算法

        • 因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法.

        • 复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。

        • 复制算法不会产生内存碎片。

        • 步骤

          • 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。

          • 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。

          • 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。

          • 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。

          • 不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

  • Tenured Generation -也被称为老年代(图2中的旧空间),这个空间容纳存活较长的对象,使用寿命更长(如果它们活得足够长,则从Survivor空间移过来)。填充此空间时,GC会执行完整GC,这会在性能方面降低成本。如果此空间无限制地增长,则JVM将抛出OutOfMemoryError - Java堆空间。

  • PerGen (永久代)
    Permanent Generation

    • 作为与终身代密切相关的第三代,永久代是特殊的,因为它保存虚拟机所需的数据,以描述在Java语言级别上没有等价的对象。例如,描述类和方法的对象存储在永久代中。

    • java8移除

标记清除算法 (Mark-Sweep)

  • 过程

      1. 标记出来所有需要回收的对象
      1. 标记完成后统一回收所有被标记的对象
  • 性能

    • 标记,清除效率低

    • 空间利用率低(清除后有空间碎片,导致有大的对象时,需要先回收一次内存

  • 算法细节

    • Partial GC:并不收集整个GC堆的模式

    • Young GC:只收集young gen的GC

      • Minor GC

        • 从年轻空间(包括Eden和幸存者空间)收集垃圾称为Minor GC。这个定义既清晰又统一。但是在处理小的垃圾收集事件时,你仍然需要注意一些有趣的信息:

          • Minor GC总是在JVM无法为新对象分配空间时触发,例如Eden已经满了。因此,分配率越高,执行Minor GC的频率就越高。

          • 每当池被填满时,它的整个内容都会被复制,并且指针可以再次从零开始跟踪空闲内存。所以代替了传统的标记,清扫和紧凑,清洁Eden spaces 和 Survivor spaces进行标记和复制代替。因此,在Eden spaces 和 Survivor spaces内实际上不会发生分裂。写指针总是驻留在使用的池的顶部。

          • 在Minor GC事件期间,终身生成实际上会被忽略。从成熟代到年轻代的引用被认为是事实上的GC根。在标记阶段,从年轻代到终身代的引用被简单地忽略。
            与人们普遍认为的相反,所有较小的GCs都会触发“停止世界”暂停,从而停止应用程序线程。对于大多数应用程序,暂停的长度在延迟方面可以忽略不计。如果Eden中的大多数对象都被认为是垃圾,并且永远不会复制到Survivor/Old spaces,那么就会出现这种情况。如果相反,并且大多数新生对象都不适合GC,那么小的GC暂停将花费更多的时间。
            因此,使用Minor GC时,情况相当清楚——每个Minor GC清除年轻代。

    • Old GC:只收集old Gen / Tenured Gen 的GC。只有CMS的concurrent collection是这个模式

      • Major GC

        • 首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

        • 指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
          就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
          倍以上。

        • 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

        • Major GC is cleaning the Tenured space.

      • CMS

        • 适合对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation(终生代)的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

      • G1(Garbadge First Collector)

        • 可以解决CMS中Concurrent Mode Failed问题,尽量缩短处理超大堆的停顿,在G1进行垃圾回收的时候完成内存压缩,降低内存碎片的生成。

        • G1在堆内存比较大的时候表现出比较高吞吐量和短暂的停顿时间,而且已成为Java 9的默认收集器。未来替代CMS只是时间的问题。

        • 原理

          • Region

            • G1将内存划分成了多个大小相等的Region(默认是512K),Region逻辑上连续,物理内存地址不连续。

            • 同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。

              • H (Humongous)

                • H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。

                • 当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作。

    • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

        1. system.gc();
        1. 旧生代空间不足
          旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space
          为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
        1. Permanet Generation空间满
          Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space
          为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
        1. CMS GC时出现promotion failed和concurrent mode failure
          对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
          promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
          应对措施为:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
        1. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
          这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
          例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
          当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
        1. 对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。
          可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
      • Full GC is cleaning the entire Heap – both Young and Tenured spaces.
  • gc弃用一些组合

    • DefNew + CMS

    • ParNew + SerialOld

    • Incremental CMS

    • 对应的命令行选项会产生警告信息并且建议你不要使用这样的组合。这些命令行选项会在将来的某个主要版本中移除。

      • 命令行选项-Xinggc被弃用

      • 命令行选项-XX:CMSIncrementalMode被弃用。注意,这个命令行选项会影响所有的CMSIncremental选项。

      • 命令行选项-XX:+UseParNewGC被弃用。除非你同时使用选项-XX:+UseConcMarkSweepGC。

      • 命令行选项-XX:-UseParNewGC只有在和-XX:+UseConcMarkSweepGC选项一起使用时被弃用。

      • 想要获得更多的信息,请查看 http://openjdk.java.net/jeps/173

数据区域

直接内存

(Direct Memory)

  • JDK1.4-NIO-基于Channel和缓冲区的I/O方式

    • 使用Native函数库直接分配堆外内存,通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

      • 在一些场景中提升性能,避免了在java堆中和Native堆中来回复制数据
  • 异常

    • outOfMemory

      • 配置时忽略直接内存,使得各个内存区域总和大于物理内存限制(物理的、操作系统级的限制),从而导致动态扩展时报错
  • 不是运行时数据区的一部分

  • 本机直接内存的分配不会受到java堆块大小的限制,但是收到本机总内存(RAM、SWAP或者分页文件)大小及处理器寻址空间的的限制

运行时数据区

  • 程序计数器

    • 线程私有

    • 记录当前线程所执行的字节码的行号。

    • 此 内存 区域 是 唯一 一个 在 Java 虚拟 机 规范 中 没有 规定 任何 OutOfMemoryError 情况 的 区域。

    • 每个线程对应一个计数器

      • 独立存储,互不影响

        • 这类内存成为“线程私有”
    • 概念模型(不同虚拟机会有更高效的做法)中的做法:

      • 字节 码 解释器 工作 时 就是 通过 改变 这个 计数器 的 值 来 选取 下一 条 需要 执行 的 字节 码 指令, 分支、 循环、 跳 转、 异常 处理、 线程 恢复 等 基础 功能 都 需要 依赖 这个 计数器 来 完成。

      • 一个确定的时刻,一个处理器(内核)只会处理一个线程。所以,计数器保管了线程中字节码的行号,下次调用的时候能够快速定位(恢复线程)

    • 正在执行的是Native方法,计数器值为空(Undefined)

      • 怎么恢复?
  • Java虚拟机栈
    (Java Virtual Machine Stacks)

    • 线程私有

    • 生命周期与程序计数器一致

      • 每个方法的开始到结束对应

        • 一个栈帧(在虚拟机栈中)从入栈到出栈
    • Java方法内存执行模型:

      • 每个方法再执行的同时都会创建一个栈帧(Stack Frame)

        • 栈帧

          • 存储内容

            • 局部变量表(俗称的“栈”)

              • 存放

                • 对象引用(reference)
(不等同于对象本身)

                  • 有可能是

                    • 对象起始地址的引用指针

                    • 与此对象相关的位置

                    • returnAddress 类型( 指向 了 一条 字节 码 指令 的 地址)

                    • 代表对象的句柄

                • 编译期间可知的基本数据类型
                  boolean, byte, char, short, int, float, long, double

                  • 64位长度的long, double占用2个局部变量空间(Slot)
              • 局部变量表所需的内存空间在编 译期间完成分配

                • 当进入一个方法时,这个方法需 要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
            • 动态链接

            • 方法出口

            • 操作数栈

          • 栈帧是方法运行时的基础数据结构

    • 为虚拟机执行java字节码服务

    • 异常

      • StackOverflowError

        • 线程请求的深度大于虚拟机所允许的深度,则抛出异常

          • 使用- Xss 参数 减少 栈 内存 容量。
        • 多线程

          • 不能减少线程数的前提

            • 通过减少最大堆内存和减少栈容量(减少大量的本地变量,本地变量表长度减少)来换取更多线程
          • 每个线程分配的栈容量越大,建立的线程数就越少,建立时就容易耗尽内存

          • 建立非常多的线程

            • 为每个线程分配的栈空间内存越大越容易内存溢出
        • 单线程下

          • 栈帧过大or虚拟机栈过小
      • OutOfMemoryError

        • 如果虚拟机栈可以动态扩展(当前大部分的java虚拟机都可动态扩展,只不过虚拟机规范中也允许固定长度的虚拟机栈),扩展没有申请到足够的内存,则抛出异常

          • 32位windows限制位2G

          • 系统限制内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)-程序计数器消耗的内存(很小,可忽略)

            • (虚拟机进程本身耗费不计算在内)剩下的内存由虚拟机栈和本地方法栈“瓜分”
    • 栈深度

      • 栈的高度

        • 虚拟机默认设置下,合适:1000~2000
      • 栈帧越多,高度越高

      • 栈帧越大,高度越小

  • 方法区
    (Method Area)

    • 线程共享

    • Non-Heap

    • 存储

      • 已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

        • 类信息

        • 常量

        • 静态变量

        • 即时编译器后的代码

        • etc

      • 永久代 PerGen(java8已取消)

      • 常量的回收,类的卸载

      • JDK1.7中Hotspot,已经把原本放在永久代中的字符串常量池移出

      • 对象类型数据

    • 运行时常量池
(runtime constant pool)

      • 异常

        • outOfMemory

          • 当无法申请到内存时会抛出
      • 常量池(runtime constant table),存放编译时期生成的各种字面量和符号引用,也在运行时常量池中保存

      • 一般来说

        • 翻译出来的直接引用

        • 保存class文件中描述的符号引用

      • Java 语言并不要求常量一定要只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中

        • String.intern()
    • 异常

      • outOfMemoryError

        • 方法区无法满足内存分配需要
  • java堆
    (java heap)

    • 所有线程共享

    • 虚拟机中管理内存最大的一块

    • 唯一目的存放对象

      • 几乎所有的对象都在这里存放
    • 垃圾收集器管理的主要区域

      • “GC堆”
    • 内存回收角度来看
      (基本采用“分代收集算法”)

      • 新生代

      • 旧生代

    • 内存分配角度来看

      • 多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)
    • Java堆可以存储在不连续的物理空间中,只要逻辑是连续的即可

      • -Xmx和-Xms控制

      • 大多数虚拟机都是按照可扩展来实现的

    • 进一步的划分是为了更好的分配内存或回收内存

    • 虚拟机规范描述:所有的对象及数组都要在堆上分配

      • 但是,随着…
        所有对象并不是绝对分配在堆上了

        • JIT编译器的发展

        • 栈上分配

        • 逃逸分析技术逐渐成熟

        • 标量替换优化技术逐渐成熟

    • 在虚拟机启动时创建

    • 异常

      • outOfMemoryError

        • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展的时,则抛出异常

          • 只要 不断 地 创建 对象, 并且 保证 GC Roots 到 对象 之间 有可 达 路径 来 避免 垃圾 回收 机制 清除 这些 对象, 那么 在 对象 数量 到达 最 大堆 的 容量 限制 后 就会 产生 内存 溢出 异常
        • (内存溢出)进一步提示:Java heap space

          • 内存 映像 分析 工具( 如 Eclipse Memory Analyzer) 对 Dump 出 来的 堆 转储 快照 进行 分析, 重点 是 确认 内存 中的 对象 是否 是 必要 的, 也就是 要 先 分清 楚 到底 是 出现 了 内存泄漏( Memory Leak) 还是 内存 溢出( Memory Overflow)

            • 掌握 了 泄露 对象 的 类型 信息 及 GC Roots 引用 链 的 信息, 就可以 比较 准确 地 定位 出 泄露 代码 的 位置
          • 不存在泄露

            • 就是 内存 中的 对象 确实 都 还 必须 存活 着, 那就 应当 检查 虚拟 机 的 堆 参数(- Xmx 与- Xms), 与 机器 物理 内存 对比 看 是否 还可以 调 大, 从 代码 上 检查 是否 存在 某些 对象 生命 周期 过长、 持有 状态 时间 过长 的 情况, 尝试 减少 程序 运 行期 的 内存 消耗
      • 堆 参数(- Xmx 与- Xms),一旦设定自动扩展就不可用

  • 本地方法栈

    • 为虚拟机制执行Native方法服务

    • 没有限制,可由虚拟机自行设计

JAVA8

PermGen

(永久代)

  • 此内存空间已完全删除

  • PermSize和MaxPermSize JVM参数将被忽略,如果在启动时出现警告,则会发出警告

  • 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

一些其他数据已移至Java堆空间。 这意味着在将来的JDK 8升级之后,您可能会发现Java堆空间的增加

新生代:Eden+From Survivor+To Survivor

老年代:OldGen

永久代(方法区的实现) : PermGen

替换为Metaspace(本地内存中)

Metaspace 元空间

  • 元空间的内存大小

    • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

    • 理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数

  • 常用配置参数

    • MetaspaceSize

      • 初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数
    • MaxMetaspaceSize

      • 限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)
    • MinMetaspaceFreeRatio

      • 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
    • MaxMetasaceFreeRatio

      • 当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%
    • MaxMetaspaceExpansion

      • Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
    • MinMetaspaceExpansion

      • Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)

对象

创建

(虚拟机角度,java角度,init还没有执行)

  • 在常量池找寻类的符号引用

    • 检查这个符号引用代表的类是否被加载和初试化过

      • 没有

        • 执行类的加载过程
        • 新生对象分配内存

          • 分配内存大小在类加载的时候就可以完全确定
(在java堆中分出确定大小的内存来)

          • 分配方式
            (java堆是否规整决定)

            • 内存是规整

              • “指针碰撞”(Bump the Pointer)

                • 指针是使用的及未使用的分界

                • 划分:指针向未使用的区域移动对象大小的距离

            • 内存不是规整的

              • “空闲列表”(Free List)

                • 已用未使用相互交错

                • 虚拟机维护一个列表,记录那些是可用的

                • 分配的时候,在列表中找一块足够大的内存给对象,并更新列表的记录

            • 是否规整

              • 采用的垃圾回收器是否有压缩功能
  • 分配内存的指针在并发下并不是线程安全的

    • 解决方案

      • 分配内存空间的动作进行同步处理

        • 失败重试的保证更新操作的原子性
      • 分配内存的动作按照不同线程,划分在不同的空间中进行处理

        • 每个java线程在java堆中预先分配一小块内存,这个内存称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

        • TLAB用完重新分配,才需要同步锁定

        • 是否启用TLAB,参数:-XX:+/-UseTLAB

        • 对象实例字段在不出初始化的时候就可以使用,初始化零值(不包过对象头)

设置

  • 保存在对象头(Object header)

内存分布

  • 对象头
    (object header)

    • 存储自身的运行时数据
      (Make word)

      • 如 哈 希 码( HashCode)、 GC 分 代 年龄、 锁 状态 标志、 线程 持 有的 锁、 偏向 线程 ID、 偏向 时间 戳 等

        • Mark Word 被 设计 成 一个 非 固定 的 数据 结构 以便 在 极小 的 空间 内存 储 尽量 多的 信息, 它 会 根据 对象 的 状态 复 用 自己的 存储 空间。
    • 类型指针

      • 对象指向类元数据的指针

        • 虚拟机通过这个指针确定对象所属类实例
      • 并不是 所有 的 虚拟 机 实现 都 必须 在 对象 数据 上 保留 类型 指针, 换句话说, 查找 对象 的 元 数据 信息 并不 一定 要 经过 对象 本身,

    • 如果 对象 是 一个 Java 数组, 那 在 对象 头中 还 必须 有 一块 用于 记录 数组 长度 的 数据,因为可以从对象的元数据判断对象大小,但从数据的元数据无法判断数组大小

  • 实例数据
    (instance data)

    • 也是 在 程序 代码 中 所 定义 的 各种 类型 的 字段 内容。 无论是 从父 类 继承 下来 的, 还是 在 子类 中 定义 的, 都 需要 记录 起来。

      • 这 部分 的 存储 顺序 会受 到虚拟 机 分配 策略 参数( FieldsAllocationStyle) 和 字段 在 Java 源 码 中 定义 顺序 的 影响。 HotSpot 虚拟 机 默认 的 分配 策略 为 longs/ doubles、 ints、 shorts/ chars、 bytes/ booleans、 oops( Ordinary Object Pointers), 从 分配 策略 中 可以 看出, 相同 宽度 的 字段 总是 被 分配 到一起。 在 满足 这个 前提 条件 的 情况下, 在 父 类 中 定义 的 变量 会 出现 在 子类 之前。 如果 CompactFields 参 数值 为 true( 默认 为 true), 那么 子类 之中 较窄 的 变量 也可能 会 插入 到 父 类 变量 的 空隙 之中。
  • 对齐填充
    (padding)

    • 并不是必然存在

    • 没有特别含义仅仅是占位符

      • 由于 HotSpot VM 的 自动 内存 管理 系统 要求 对象 起始 地址 必须 是 8 字节 的 整 数倍, 换句话说, 就是 对象 的 大小 必须 是 8 字节 的 整 数倍。 而对 象头 部分 正好是 8 字节 的 倍数( 1 倍 或者 2 倍), 因此, 当 对象 实例 数据 部分 没有 对齐 时, 就 需要 通过 对齐 填充 来 补 全。

访问定位

  • 通过栈上reference来访问堆上的对象数据

    • 虚拟机只规定了reference是一个指向对象的引用

    • 指针访问

      • Hot spot的实现方式

        • 速度快
(节省了一次指针定位的时间开销)

          • Reference存储的是对象直接地址

            • 到对象类型的指针
    • 句柄访问

      • 更稳定
(垃圾回收的时候改变的只是句柄中的实例指针,而reference本身不需要修改)

        • Reference存储的是对象的句柄地址

          • 句柄地址包含:
            1.(java堆.实例池)对象实例数据(地址)
2.(方法区)类型数据各自的具体地址信息(地址)
        • Java堆划分出来内存做句柄池

基础概念

字节码

  • 通常情况下它是已经经过编译,但与特定机器码无关。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。

  • 字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码

符号引用

  • 软链接

字面量

机器码

  • 机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据

分析

使用Java VisualVM远程分析堆

  • VisualVM是一种工具,它提供了一个可视化界面,用于查看有关基于Java技术的应用程序运行时的详细信息。

MemLeak

泄漏诊断

识别症状

  • 通常,如果Java应用程序请求的存储空间超过运行时堆提供的存储空间,则可能是由于设计不佳导致的。例如,如果应用程序创建映像的多个副本或将文件加载到数组中,则当映像或文件非常大时,它将耗尽存储空间。这是正常的资源耗尽。该应用程序按设计工作(虽然这种设计显然是愚蠢的)

  • 但是,如果应用程序在处理相同类型的数据时稳定地增加其内存利用率,则可能会发生内存泄漏。

  • 此GC跟踪文件中的每个块(或节)按递增顺序编号。要理解这种跟踪,您应该查看连续的分配失败节,并查找随着时间的推移而减少的释放内存(字节和百分比),同时总内存(此处,19725304)正在增加。这些是内存耗尽的典型迹象

启用详细垃圾回收

  • verbosegc

    • 断言确实存在内存泄漏的最快方法之一是启用详细垃圾回收。通常可以通过检查verbosegc输出中的模式来识别内存约束问题。

      具体来说,-verbosegc参数允许您在每次垃圾收集(GC)过程开始时生成跟踪。也就是说,当内存被垃圾收集时,摘要报告会打印到标准错误,让您了解内存的管理方式。

启用分析

  • 不同的JVM提供了生成跟踪文件以反映堆活动的不同方法,这些方法通常包括有关对象类型和大小的详细信息。这称为分析堆。

分析踪迹

  • 跟踪可以有不同的格式,因为它们可以由不同的Java内存泄漏检测工具生成,但它们背后的想法总是相同的:在堆中找到不应该存在的对象块,并确定这些对象是否累积而不是释放。特别感兴趣的是每次在Java应用程序中触发某个事件时已知的临时对象。应该仅存少量,但存在许多对象实例,通常表示应用程序出现错误。