JVM学习笔记——04运行时数据区域(二)

五. Java堆

1. 堆的核心概述

1. 说明
  1. 一个jvm实例只存在一个堆内存, 堆也是java内存管理的核心区域.
  2. Java堆区在jvm启动的时候即被创建, 其空间大小也就确定了. 是jvm管理的最大一块内存空间.
    堆内存的大小是可以调节的
  3. Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。此内存区域的唯一目的就是==存放对象实例==
  4. 所有的线程共享java堆, 堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率
  5. 几乎所有的对象实例以及数组都应当在运行时分配在堆上.
  6. 方法结束后, 堆中的对象不会马上被移除, 仅仅在垃圾收集的时候才会被移除
  7. 堆是GC(Garbage Collection, 垃圾回收器)执行垃圾回收的重要区域
  8. Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩 展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
2. 内存细分

现代垃圾护手器大部分都基于分代收集理论设计.

  1. java7及以前堆内存逻辑上分为三部分: 新生区 + 养老区 + 永久区
  2. java8及以后堆内存逻辑上分为三部分: 新生区 + 养老区 + 元空间
  3. 约定:
    新生区 == 新生代 == 年轻代
    养老区 == 老年代 == 老年区
    永久区 == 永久代

2. 堆空间大小的设置

1. 说明
  1. java堆区用于存储java对象实例, 那么堆的大小在jvm启动时就已经设定好了, 大家可以通过选项"-Xmx" 和 "-Xms"来进行设置
    • "-Xms"用于表示堆区的起始内存, 等价于: -XX:InitialHeapSize
    • "-Xmx"用于表示堆区的最大内存, 等价于:-XX:MaxHeapSize
  2. 一旦堆区中的内存大小超过"-Xmx"所指定的最大内存时, 会抛出OutOfMemoryError异常
  3. 通过将-Xms和-Xmx两个参数配置相同的值, 其目的是: 为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小, 从而提高性能.
  4. 默认情况, 初始内存大小: 物理电脑内存大小/64; 最大内存情况: 物理电脑内存大小/4
2. 案例
  1. 代码

    public class HeapSpaceInitial {
        public static void main(String[] args) {
            long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
            long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
            System.out.println("初始化内存大小: " + initialMemory);
            System.out.println("最大内存大小: " + maxMemory);
        }
    }
    
  2. 参数设置

    -Xms600m -Xmx600m
    
  3. 结果
    在这里插入图片描述

  4. 原因
    from区和to区的大小都是25M; 而且from区和to区总是只有一个区域存放内容

3. 年轻代与老年代

1. 说明
  1. 存储在JVM中的java对象可以被划分为两类

    • 一类是生命周期较短的瞬时对象, 这类对象的创建和消亡都非常迅速

    • 另一类对象的生命周期非常长, 在某些极端情况下还能够与JVm的生命周期保持一致

  2. JVM堆区进一步细分的话, 可以划分为年轻代(YoungGen) 和 老年代(OldGen)

  3. 其中年轻代又可以划分为Eden空间, Survivor0空间和Survivor1空间(有时也叫做from区, to区)
    在这里插入图片描述

  4. 几乎所有的java对象都是在Eden区被new出来的

  5. 绝大部分的java对象的销毁都在新生代进行了

  6. 可以根据"-Xmn"来设置新生代最大内存的大小, 一般不设置

2. 设置占比
  1. 默认-XX:NewRation=2, 表示新生代占1, 老年代占2, 新生代占整个堆的1/3

    官方默认说的是-XX:SurvivorRation=8, 表示Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
    在这里插入图片描述

    但是实际测试为6:1:1

    -Xms600m -Xmx600m
    在这里插入图片描述

    因为开启了自适应的原因,
    -XX:-UseAdaptiveSizePolicy: 关闭自适应还是不好使
    -XX:SurvivorRatio=8: 显示新生代中Eden区与survivor的内存空间的比例
    在这里插入图片描述

  2. 可以修改-XX:NewRation=4, 表示新生代占1, 老年代占4, 新生代占整个堆的1/5

4. 对象分配过程

1. 一般过程
  1. new的对象先放伊甸园区, 此区有大小限制

  2. 当伊甸园的空间填满时, 程序又需要创建对象, JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC), 将伊甸园区中的不再被其他对象所引用的对象进行销毁. 再加载新的对象到伊甸园区.

  3. 然后将伊甸园中的剩余对象移动到幸存者0区

    在这里插入图片描述

  4. 如果再此触发垃圾回收, 此时上次幸存下来的放到幸存者0区, 如果没有回收, 就放到幸存者1区

  5. 如果再次经历垃圾回收, 此时会重新放回幸存者0区, 接着再去幸存者1区.

    在这里插入图片描述

  6. 啥时候能去养老区呢? 可以设置次数. 默认是15次

    可以设置参数: -XX:MaxTenuringThreshold=进行设置

  7. 在养老区, 相对悠闲. 当养老区内存不足时, 再次触发GC: MajorGC, 进行养老区的内存清理

    在这里插入图片描述

  8. 若养老区执行了Major GC之后发现依然无法进行对象的保存, 就hi产生OOM异常java.lang.OutOfMemoryError: Java heap space

  9. 总结:

    针对幸存者s0, s1区的总结: 复制之后有交换, 谁空谁是to

    关于垃圾回收: 频繁在新生区收集, 很少在养老区收集, 几乎不在永久区/元空间收集

2. 特殊情况
  1. 流程图
    在这里插入图片描述

5. Minor GC, Major GC 与 Full GC

1. 说明

JVM在进行GC时, 并非每次都对上面三个内存(新生代, 老年代, 放啊去)区域一起回收的, 大部分时候回收的都是指新生代

针对HotSpot VM的实现, 它里面的GC按照回收区域又分为两大类: 一种是部分收集(Partial GC), 一种时整堆收集(Full GC)

  1. 部分收集: 不是完整收集整个Java堆的垃圾收集. 其中又分为:

    • 新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集

    • 老年代收集(Major GC / Old GC): 只是老年代的垃圾收集
      目前, 只有CMS GC会有单独收集老年代的行为
      注意: 很多时候Major GC会和Full GC混淆使用, 需要具体分辨时老年代回收还是整堆回收.

  2. 混合收集(Mixed GC): 收集整个新生代以及部分老年代的垃圾收集

    目前只有G1 GC会有这种行为

  3. 整堆收集(Full GC): 收集整个java堆和方法区的垃圾回收

2. 年轻代GC(Minor GC)触发机制
  1. 当年轻代空间不足时, 就会触发Minor GC, 这里的年轻代满指的时Eden代满, Survivor满不会引发GC.(每次Minor GC会清理年轻代的内存)
  2. 因为java对象大多都具备朝生夕灭的特性, 所以Minor GC非常频繁, 一般回收速度也比较快.
  3. Minor GC会引发STW, 暂停其它用户的线程, 等垃圾回收结束, 用户线程才恢复运行.
3. 老年代GC(Major GC)触发机制
  1. 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对)

    也就是在老奶奶带空间不足时, 会先尝试触发Minor GC. 如果之后空间还是不足, 着触发Major GC

  2. Major GC速度一般会比Minor GC慢10倍以上, STW的时间更长.

  3. 如果Major GC后, 内存还是不足, 就报OOM

4. Full GC触发机制
  1. 调用System.gc()时, 系统建议执行Full GC, 但是不必然执行
  2. 老年代空间不足
  3. 放啊去空间不足
  4. 通过给Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区, survivor space0(From Space)区向survivorspace1(To Space)区复制时, 对象大小大于To Space可用内存, 则把该对象转存到老年代, 且老年代的可用内存小于该对象的大小
  6. 说明: full gc时开发或调优过程中尽量要避免的. 这样暂时时间会短一些

6. 堆空间分代思想

  1. 分代的唯一理由就是优化GC性能. 如果没有分代, 那所有的对象都在一块, GC的时候要找到哪些对现象没用, 就需要对堆的所有区域进行扫描.
  2. 而很多对象都是朝生夕死的,如果分代的话, 把新创建的对象放到某一地方, 当GC的时候先把这块存储"朝生夕死"对象的区域进行回收, 这样就可以腾出很大的空间出来

7. 内存分配策略

  1. 如果对象在Eden出生并经过第一次MinorGC后仍然存活, 并且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将对象年龄设为1, 对象在Survivor区中每熬过一次MinorGC, 年龄就增加1岁, 当它的年龄增加到一定程度(默认为15岁, 其实每个JVM, 每个GC都有所不同)时, 就会倍晋升到老年代中
  2. 对象晋升老年代的年龄阈值, 可以通过选项 -XX: MaxTenuringThreshold来设置.

8. 对象分配过程: TLAB

1. 什么是TLAB
  1. 从内存模型而不是垃圾收集的角度, 对Eden区域区域继续进行划分, JVM为每个线程分配了一个私有缓存区域, 它包含在Eden空间内.
  2. 多线程同时分配内存时, 使用TLAB可以避免一系列的非线程安全问题, 同时还能够提升内存分配的吞吐量, 因此我们可以将这种内存分配方式称之为快速分配策略.
  3. 据我所知私有OpenJDK衍生出来的JVM都提供了TLAB的设计
2. 为什么有TLAB
  1. 堆区是线程共享区域, 任何线程都可以访问到堆区的共享数据
  2. 由于对象实例的创建在jvm中非常频繁, 因此在并发环境下从堆区中划分内存空间是线程不安全的.
  3. 为了避免多个线程操作同一地址, 需要使用加锁等机制, 进而影响分配速度
3. TLAB再说明
  1. 尽管不是所有的对象实例都能够再TLAB中成功分配内存, 但JVM确实是将TLAB作为内存分配的首选
  2. 在程序中, 开发人员可以通过选项:-XX:UseTLAB 设置是否开启TLAB空间
  3. 默认情况下, TLAB空间内存非常小, 仅占整个Eden空间的1%, 当然我们可以通过选项-XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小
  4. 一旦对象在TLAB空间分配内存失败时, JVM就会尝试着通过使用加锁机制确保数据操作的原子性, 从而直接在Eden空间中分配内存.
9. 堆空间的参数设置
1. 常用参数
  1. -XX:PrintFlagsInitial: 查看所有的参数的默认初始值

  2. -XX:+PrintFlagsFinal: 查看所有的参数的最终值(可能会存在修改, 不再是初始值)

  3. -Xms: 初始堆空间内存(默认为物理内存的1/64)

  4. -Xmx: 最大堆空间内存(默认为物理内存的1/4)

  5. -Xmn: 设置新生代的大小(初始值及最大值)

  6. -XX:NewRatio: 配置新生代与老年代在堆结构的占比

  7. -XX:SurvivorRation: 设置新生代中Eden和S0/S1空间的比例

  8. -XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄

  9. -XX:+PrintGCDetails: 输出详细的GC处理日志

    打印gc简要详细: -XX:PrintGC-verbose:gc

  10. -XX:HandlePromotionFailure: 是否设置空间分配担保

    在这里插入图片描述

10. 补充: 堆不是分配对象存储的唯一选择
1. 说明
  1. 随着JIT编译期的发展与逃逸分析技术逐渐成熟, 栈上分配, 标量替换优化技术将会导致一些微妙的变化, 所有的对象分配到堆上也渐渐变得不那么"绝对"了
  2. 如果经过逃逸分析后发现, 一个对象并没有逃逸出方法的话, 那么就可能被优化成栈上分配.这样就无需在堆上分配内存, 也无须进行垃圾回收了. 这也是最常见的堆外存储技术
2. 逃逸分析技术

如果将堆上的对象分配到栈, 需要使用逃逸分析手段

  1. 这是一种可以有效减少java程序中同步负载和内存堆分配压力了的跨函数卷曲数据流分析算法
  2. 通过逃逸分析, java hotspot编译器能够分析出一个新的对象的引用的使用范围, 从而决定是否要将这个对象分配到堆上
  3. 逃逸分析的基本行为就是分配对象动态作用域:
    • 当一个对象在方法中被定义后, 对象只在方法内部使用, 着认为没有发生逃逸
    • 当一个对象在方法中被定义后, 它被外部放啊所引用, 则认为发生逃逸.
3. 代码优化

使用逃逸分析, 编译器可以对代码做如下优化

  1. 栈上分配: 将堆分配转化为栈分配. 如果一个对象在子程序中被分配, 要使指向该对象的指针永远不会逃逸, 对象可能是栈分配的候选, 而不是堆分配
  2. 同步省略: 如果一个对象被发现只能从一个线程被访问到, 那么对于这个对象的操作可以不考虑同步
  3. 分离对象或标量替换.: 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,. 那么对象的部分(或全部)可以不存储在内存, 而是存储在CPU寄存器中.

六. 方法区

1.栈, 堆, 方法区的交互关系

  1. 运行时方法区
    在这里插入图片描述

  2. 从线程共享与否的角度来看​ 在这里插入图片描述

  3. 实例
    在这里插入图片描述

2. 方法区的理解

1. 基本理解
  1. 尽管所有的方法区在逻辑上是属于堆的一部分, 但一些简单的实现可能不会选择区进行垃圾收集或进行压缩, 所以方法区看作是一块独立于Java堆的内存空间

  2. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域

  3. 方法区在JVM启动的时候被创建, 并且它的实际的物理内存空间中和java堆区一样都可以是不连续的

  4. 方法区的大小, 跟堆空间一样, 可以选择固定大小或者可扩展

  5. 方法区的大小决定了系统可以保存多少个类, 如果系统定义了太多的类, 导致方法区一户, 虚拟机同样会抛出内存溢出错误: OutOfMemoryError异常。

    • 加载大量的第三方jar包
    • 部署的工程过多(30-50)
    • 大量动态的生成反射类
  6. 关闭jvm就会释放这个区域的内存

2. Hotspot方法区的演进
  1. 在jdk7及以前, 习惯上把方法区, 称为永久代, 在jdk8开始, 元空间取代了永久代

  2. 本质上, 方法区和永久代并不等价. 仅是堆hotspot而言的.
    在这里插入图片描述
    在这里插入图片描述

  3. 元空间的本质和永久代类似, 都是堆jvm规范中方法区的实现. 不过元空间与永久代最大的区别在于: 元空间不在虚拟机设置的内存中, 而是使用本地内存

  4. 永久代, 元空间二者并不只是名字变了, 内部结构也调整了

  5. 如果方法区无法满足新的内存分配需求时, 将抛出OOM异常.

3. 设置方法区大小与OOM

方法区的大小不必是固定的, JVM可以根据应用的需要动态调整

1. jdk7及以前
  1. 通过-XX:PermSize来设置永久代初始分配空间. 默认值时20.75M

  2. -XX:MaxPermSize来设定永久代最大可分配空间. 32位机器默认时64M, 64位机器默认是82M

  3. 当jvm加载的类信息容量超过这个值, 会报异常: OutOfMemoryError:PermGen space

2. jdk8及以后
  1. 元数据去大小可以使用参数-XX:MetaspaceSize-XX:MaxMetaspaceSize指定, 替代上述原有的两个参数
  2. 默认值依赖于平台. windows下, -XX:MetaspaceSize是21M, -XX:MaxMetaspaceSize的值是-1, 即没有限制
  3. 与永久代不同, 如果不指定大小, 默认情况下, 虚拟机会耗尽所有的可用系统内存. 如果元数据区发生溢出, 虚拟机一样会抛出异常: OutOfMemoryError:metaspace
  4. -XX:MetaspaceSize: 设置初始的元空间大小, 对于一个64位的服务端jvm来说, 其默认值位21M. 这就是初始的高水位先, 一旦触及这个水位线, Full GC将会被触发并卸载没用的类, 然后这个高水位西线将会重置. 新的高水位线的值取决于GC后释放了多少元空间. 如果释放的空间不足, 那么在不超过MaxMetaspaceSize时, 十点过提高该值. 如果释放空间过多, 则适当降低该值.
  5. 如果初始化的高水位线设置过低, 上述高水位线调整情况会发生多次. 通过垃圾回收器的日志可以观察到Full GC多次调用. 为了避免频繁的GC, 建议-XX:MetaspaceSize设置位一个相对较高的值.
3. 如何解决这些OOM
  1. 要解决OOM异常或heap space的异常, 一般的手段时首先通过内存映像分析工具对dump出来的堆转储快照进行分析, 重点是确认内存中的对象是否是必要的, 也就是要先分清楚到底是出现了内存泄漏(Memory Lear)还是内存溢出(Memory Overflow)
  2. 如果是内存泄漏, 可进一步通过工具查看泄漏对象到GC Roots的引用链. 于是就能找到泄漏对象是通过怎样的路径与GC Roots习惯联并导致垃圾收集器无法主动回收它们的. 掌握了泄漏对象的类型信息, 以及GC Roots引用链的信息, 就可以比较准确地定位出泄漏代码的位置
  3. 如果不存在内存泄漏, 换句话说就是内存中的对象确实都还必须存活着, 那就应当检查虚拟机的堆参数(-Xmx 与 -Xms), 与机器物理内存对比看是否还可以调大, 从代码上检查是否存在某些对象生命周期过长, 持有状态时间过长的情况, 尝试减少程序运行期的内存消耗

4. 方法区内部结构

方法区用于存储已被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等.

在这里插入图片描述

1. 类型信息

对每个加载的类型(类class, 接口interface, 枚举enum, 注解annotation), JVM必须在方法区存储以下类型信息:

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于interface或是java.lang.Object, 都没有父类)
  3. 这个类型的修饰符(public, abstract, final的某个子集)
  4. 这个类型直接接口的一个有序列表
2. 域(Field)信息
  1. java必须在方法区中保存类型的所有域的相关信息以及域的声明顺序.
  2. 域的相关信息包括: 域名称, 域类型, 域修饰符(public, private, protected, static, final, volatile, transient的某个子类)
3. 方法(Method)信息

jvm必须保存所有方法的以下信息, 同域信息一样包括声明顺序

  1. 方法的名称

  2. 方法的返回类型(或 void)

  3. 方法参数的数量和类型(按顺序)

  4. 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)

  5. 方法的字节码(bytecodes), 操作数栈, 局部变量表及大小(abstract和native方法除外)

  6. 异常表(abstract和native方法除外)

    每个异常处理的开始位置, 结束位置, 代码处理在程序计数器中的偏移地址, 被捕获的异常类的常量池索引

4. non-final的类变量
  1. 静态变量和类关联在一起, 随着类的加载而加载, 它们成为类数据在逻辑上的一部分.
  2. 类变量被类的所有实例共享, 即使没有类实例时也可以访问它.
5. 全局常量: static final

被声明位final的类变量的处理方法则不同, 每个全局常量在编译的时候就会被分配了.

6. 运行时常量池
  1. 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  2. 行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
  3. 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。

5. 方法区的演进细节

首先明确: 只有HotSpot才有永久代. BEA, JRockit, IBM, J9是不存在永久代的概念的

1. HotSpot中方法区的变化
  1. jdk1.6及以前:

    有永久代(permanent generation), 静态变量存放在永久代上

    在这里插入图片描述

  2. jdk1.7:

    有永久代, 但已经逐步"去永久代", 字符串常量池, 静态变量移除, 保存在堆中

    在这里插入图片描述

  3. jdk1.8及以后:

    无永久代, 类型信息, 字段, 方法, 常量保存在本地内存的元空间, 但字符串常量池, 静态变量仍在堆

    在这里插入图片描述

2. 永久代为什么要被元空间替换
  1. 位用接待设置空间大小是很难确定的.

    元空间和永久代之间最大的区别在于: 元空间并不在虚拟机中, 而是使用本地内存. 因此, 默认情况下, 元空间的大小仅受本地内存限制

  2. 对永久代进行调优是很困难的

6. 方法区的垃圾回收

  1. 一般来说方法区的回收效果比较难令人满意, 尤其是类型的卸载, 条件相当苛刻. 但是这部分区域的回收有时又确实是必要的.

  2. 方法区的垃圾回收主要回收两部分内容: 常量池中废弃的常量和不再使用的类型

  3. Hotspot虚拟机对常量池的回收策略是很明确的, 只要常量池中的常量没有被任何地方引用, 就可以被回收

  4. 判断一个类是否不再使用, 需要同时满足下面3个条件:

    • 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生之类的实例
    • 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 否则通常是很难达成的
    • 该类对应的java.lang.Class对象没有再任何地方被引用, 无法在任何地方通过反射访问该类的方法.
  5. java虚拟机被允许对满足上述三个条件的无用类进行回收, 这里说的仅仅是"被允许", 而不是和其他对象一样, 没有引用了就必然会回收, 关于是否对类型进行回收, Hotspot虚拟机提供了-Xnoclassgc参数进行控制, 还可以使用-verbose:class以及-XX:+TraceClass-Loading, -XX:+TraceClassUnLoading查看类加载和卸载信息

  6. 在大量使用反射, 动态代理, CGLib等字节码框架, 动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, 通常都需要Java虚拟机具备类型卸载的能力, 以保证不会对方法区造成过大的内存压力

7. 常见面试题

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值