JVM基础知识[个人总结]

    声明: 1. 本文为我的个人复习总结, 并那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
              2. 由于是个人总结, 所以用最精简的话语来写文章
              3. 若有错误不当之处, 请指出

JVM 内存结构:

JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区(堆, 虚拟机栈, 本地方法栈, 方法区, PC寄存器)

JVM基础知识[个人总结]

方法区、永久代、元空间:

​ 逻辑上的规范叫做方法区, 真正具体的实现 在1.7叫做永久代, 在1.8叫做元空间(不使用JVM内存了, 而是使用操 作系统的物理内存)

1.7时 永久代的常量池这部分 搬到堆中; 1.8时又从堆中搬出来, 永久代也彻底消失, 改为叫元空间

栈是线程私有, 堆和方法区是线程共享

程序计数器 不会出现内存溢出

方法区存储: 普通常量, final修饰的常量引用, 字符串, 类元数据信息、字节码、即时编译器需要的信息等

​ 注意 final修饰的常量引用在方法区, 指向的对象在堆中

类文件结构:

  1. 魔数:class 文件标志

  2. 文件版本

  3. 常量池(class常量池):存放字面量和符号引用

    ​ 字面量是类相关的常量,如字符串类型的属性 或 声明为final的常量值等

    ​ 符号引用包含三类:类和接口的全限定名 & 方法的名称和描述符 & 字段的名称和描述符

  4. 访问标志:识别一些类的访问信息

    ​ 包括:这个 Class 是类还是接口,是否为 public, abstract, final类型

  5. 当前类索引 this_class:类索引用于确定这个类的全限定名

  6. 属性表集合:字段表, 方法表中都可以携带自己的属性表集合, 以用于描述一些信息

class常量池 运行时常量池 和 字符串常量池:

运行时常量池 =class常量池内容+字符串常量池内容

class常量池 只是个中间媒介场所, 存放字面量和符号引用, 在运行时它会被加载到 运行时常量池

字符串常量池 存放 普通的字符串常量

运行期间动态生成的常量 如 String 类的 intern( )方法,会被放入运行时常量池

字符串常量池的字符串 在内存不足时也是可以被回收的

逻辑上都是方法区的一部分

1.7时 永久代的常量池这部分 搬到堆中, 1.8时又从堆中搬出来, 并称为元空间

使用栈存储对象, 栈上分配:

栈其实也可以用来存储对象, 弹栈操作就像是垃圾回收;

不过需要先进行内存的逃逸分析, 如果这个内存逃出了局部方法的作用范围, 就不应该使用栈上分配了;

因为弹栈是要清空栈帧的, 这个栈上对象也就被回收了, 而此对象内存逃逸出去了可能还依旧被别人使用着

解释器 & JIT即时编译器:

  1. 解释器是把字节码解释成二进制码, 每一遍都要重新解释

  2. JIT是后端编译器, 把字节码编译成二进制码, 那样以后遇到相同的字节码就不需要重新编译了;

    JIT进行即时编译也是消耗较大的, 所以只用来将热点代码进行编译缓存, 普通非热点代码用JIT编译反而会拖慢速度

Java对象:

Java对象=对象头+实例数据+对齐填充

对齐填充 是为了 凑够8的倍数, 使寻址起来方便

JVM只要堆内存不超过32G, 默认都是开启指针压缩的

对象头:

markword(8Byte)+klass(压缩后4Byte, 否则8Byte)+length(4Byte, 这个只有数组才有)

所以new一个Object 即最小对象, 占用16字节(32位JVM 或 开启指针压缩的64位JVM)

  1. markword存储 哈希码, 对象分代年龄, 锁相关信息

JVM基础知识[个人总结]

  1. klass指针, JVM通过此指针来 确定对象属于哪个类

Calss是元数据模板

Class实例的位置在堆中, 方法区中Class的数据结构(类元数据信息) 指向了 堆中的Class实例

类加载器 什么时候被回收?

此类加载器加载过的所有 放在堆中的Class实例都被回收完了, 此类加载器才会被回收

类什么时候被卸载(即元空间的内存什么时候回收)?

  1. 该类所有的对象都已经被回收
  2. 且 该类不再被别的类使用
  3. 且 加载该类的 ClassLoader 已经被回收

一个函数对应一个栈帧, 栈帧拥有: 局部变量表、操作数栈、动态链接、方法出口信息

  • 部分符号引用 在类加载阶段的时候就转化为直接引用(调用属性),这种转化就是静态链接
  • 部分符号引用 在运行期间转化为直接引用(调用方法),这种转化就是动态链接

对象的访问定位:

  1. 使用句柄, 多了一步先找到句柄, 句柄里存的是指针
  2. 使用直接指针

JVM 内存参数:

堆空间内存设置:

按大小设置:

  • -Xms 最小堆内存

  • -Xmx 最大堆内存

  • -Xmn 设置新生代的大小

保留区域: 必要时才会使用, Xmx-Xms=保留内存

建议将 -Xms 与 -Xmx 设置为大小相等, 即不需要保留内存, 不需要从小到大增长, 这样性能较好

按比例设置:

-XX:NewRatio=2:1 表示老年代占两份,新生代占一份

-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份

元空间内存设置:

JVM基础知识[个人总结]

代码缓存内存设置:

如果 -XX:ReservedCodeCacheSize >= 240m,则代码分成三个区域:

  1. non-nmethods JVM 自己用的代码

  2. profiled nmethods 部分优化的机器码

  3. non-profiled nmethods 完全优化的机器码

如果 -XX:ReservedCodeCacheSize < 240m,则它们存在一起 不进行区分

GC垃圾回收:

GC回收的主要是堆区

垃圾回收时会阻塞别的线程(STW), 因为垃圾回收可能要改变对象的地址, 期间不能让别的线程使用

内存泄露: 不再使用的内存没有被回收

​ 造成原因:

​ 1. 可能是短期内来不及回收(不停创建不可变的String对象),

​ 2. 或者是它还被别的没必要的强引用指向着(ThreadLocal key设为弱引用原理)

​ 3. 静态属性过多, 可能导致内存泄漏(生命周期太长了, 所指向的对象就无法被回收)

内存溢出: 内存不够了

  1. 使用Executors造的线程池, 等待队列容量太大, 或者允许开辟的线程数太多
  2. 查询数据量太大, 如MyBatis中没有使用过滤或分页查询

判断是否为垃圾:

  1. 引用计数法: 记录此对象被多少引用指向着; 当对象没有被任何引用指向着时, 就视为垃圾

    这种方法不好, 无法解决对象之间互相循环引用的问题:

    ​ 当A使用着B, B也使用着A时, 就会导致A, B两个对象永远无法被回收

  2. 可达性分析法: 没被GCRoots对象 直接或间接使用着的对象, 就视为垃圾

  3. 三色标记法: 即用三种颜色记录对象的标记状态

    • 黑色 已标记
    • 灰色 标记中
    • 白色 未标记

并发漏标问题:

先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作

缺点: 如果标记存活对象后 用户线程修改了引用指向, 使引用指向了原本的垃圾对象, 那么就存在漏标问题, 将会把存活对象给回收掉

解决方案:

进行重新标记: GC会将并发标记过程中修改了 引用指向 的引用记录下来(即记录了漏标的对象);

​ 然后阻塞用户线程, 对这部分漏标的对象进行重新标记

可作为GCRoots对象的有哪些?

  1. 虚拟机栈中 引用的对象
  2. 方法区中 static属性引用的对象
  3. 方法区中 final常量引用的对象
  4. native 方法中引用的对象

比例划分:

新生代:老年代 = 1:2

eden区:from区:to区 = 8:1:1

三种垃圾回收算法:

标记的是存活的对象, 而不是垃圾

  1. 标记清除

    清除未标记的 对象占用的内存

    优点: 省空间, 回收速度快

    缺点: 产生大量内存碎片

  2. 标记整理

    在标记清除后, 多了一步整理操作, 即将存活对象向一端移动,可以避免内存碎片产生

    优点: 省空间, 不会产生内存碎片

    缺点: 回收速度慢, 因为多了一步整理操作

  3. 标记复制

    1. from 存储新创建的对象, to 处于空闲

    2. 将 未被标记的存活对象从 from区 复制到 to 区, 复制的过程中完成了碎片整理

    3. 复制完成后,交换from和to的位置

    优点: 回收速度快, 不会产生内存碎片

    缺点: 占用2倍的内存空间

优缺点是从 占用空间, 回收速度, 和内存方面考虑的

分代:

堆区分为不同的代: 新生代(伊甸园+from区+to区)+老年代

​ from区又称为幸存者0区, to区称为幸存者1区

不同的代(新生代, 老年代)使用不同的垃圾回收算法:

  1. 新生代 采用标记复制算法

    因为标记复制算法 在对象存活率较高时 会进行比较多的复制操作, 效率会变低; 所以此算法适用于对象存活率低的

  2. 老年代 采用标记整理算法

从新生代 晋升到老年代的条件:

  1. 年龄达到15岁
  2. 或 幸存区内存不足

老年代对象存活率高, 不好回收, 且空间大

GC 规模:

  1. Minor GC 新生代 的垃圾回收,暂停时间短
  2. Mixed GC 新生代+老年代部分区域 的垃圾回收,是G1垃圾回收器特有的
  3. Full GC 新生代 + 老年代所有区域 的垃圾回收,暂停时间长,应尽力避免

直接内存

并不是虚拟机运行时数据区的一部分

使用JDK的Unsafe类进行操作

NIO的DirectBuffer用的就是直接内存

优点: 速度快, 因为

  • 避免了 堆内内存 到 堆外内存的数据拷贝操作
  • 避免了GC垃圾回收损耗

对于 需要频繁进行内存间数据拷贝生命周期较短的 暂存数据,建议存储到直接内存

内存不足溢出时, 也可能导致 OutOfMemoryError 错误的出现

垃圾回收器:

  1. Parallel GC

    多个GC线程并行处理

    • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
    • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
    • 注重吞吐量

    GC吞吐量: 单次暂停时间长点,但是总的来说一段时间内处理数据量多

  2. ConcurrentMarkSweep GC (CMS)

    Gc&用户线程 并发运行

    • 工作在老年代,支持并发标记,采用并发清除算法
    • 并发标记时 不需暂停用户线程; 重新标记时 仍需暂停用户线程
    • 新生代回收代价小,没必要并发标记,STW时长是可以忍受的
    • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

    注重响应时间

  3. G1 (是最好的)

    • 划分成多个区域,每个区域都可以充当 eden, survivor, old, humongous(专为大对象准备)

      跨代引用 & 记忆集优化:

      跨代引用:

      由于对象之间会存在少量的跨代引用(老年代对象依赖着新生代对象时),如果要进行一次新生代垃圾收集,除了需要遍历新生代对象,还要额外遍历整个老年代的所有对象,这会给内存回收带来很大的性能负担。

      记忆集优化:

      没必要为了少量的跨代引用去扫描整个老年代,只需在新生代建立一个Remembered Set(记忆集)

      这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。

      此后发生GC时,老年代只有那些包含了跨代引用的小块内存才会被扫描

    • 分成三个阶段:新生代回收、并发标记 & 重新标记、Mixed Gc

    • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

    • 老年代对象占用超过堆空间的45%时, 才进行部分回收那些 回收性价比最高的垃圾(垃圾密度更高的区域)

    响应时间与吞吐量兼顾

类加载:

类加载器 & 双亲委派机制 单独写成一个文档

沙箱安全机制:

沙箱 是一个限制程序运行的环境, 对权限进行限制, 对系统资源的访问进行限制

类加载的过程:

  1. 加载: 将类的字节码载入方法区,并创建 类.class实例

    ​ 加载是懒惰执行

  2. 链接

    1. 验证 验证类是否符合 Class 规范,并进行合法性、安全性检查

    2. 准备 为static变量 分配空间赋默认值

      ​ 为 static final 的基本数据类型 或String类型 的常量 分配空间赋指定的值(编译期间就 已经可以计算出来了)

    3. 解析 将常量池的 符号引用(class文件里的字符符号) 解析为 直接引用(实际分配内存后的物理地址)

  3. 初始化: 为static变量赋指定的值

    static final 的非String的其他引用类型 的常量赋指定的值

  • 使用 static final 的基本数据类型 或String类型 的常量 (即准备阶段就赋指定的值的那部分, 在编译期就已经可以计算出值了)时, 不会触发类加载
  • 使用 static final 的非String的其他引用类型 的常量时, 会触发类加载
  • 使用static 非final的变量时, 都会触发类加载

如果整数较小不超过short最大值, 则直接就以字面量形式写到字节码上; 否则, 数据就放到常量池里, 然后去常量池里取

类的生命周期:

加载、验证、准备、解析、初始化、使用 和卸载

四种引用:

  1. 强引用 正常new的对象, Student stu=new Student( )这样

  2. 软引用 仅有软引用指向该对象时, 首次垃圾回收时不会回收该对象, 如果下一次回收时内存仍不足才会回收该对象

    ​ 引用自身的释放, 需要配合引用队列来释放

    ​ 实际应用: 反射时的那些Method类Field类

  3. 弱引用 仅有弱引用指向该对象时, 只要发生垃圾回收,就会回收该对象

    ​ 引用自身的释放, 需要配合引用队列来释放

    ​ 实际应用: ThreadLocalMap 中的 Entry类的对象

  4. 虚引用 仅有虚引用指向该对象时, 在任何时候都可能被垃圾回收, 用来 释放外部内存&自身占用内存

    ​ 流程:

    ​ 1: 传入到虚引用类的构造器里的Java对象, 在GC时 会将这些Java对象加入到引用队列

    ​ 2: 后续借此队列找到这些Java对象调用其clean的逻辑, 清理释放掉外部不再使用的内存

    ​ 引用自身的释放 和 外部内存的释放, 需要配合引用队列来释放

​ 实际应用: Cleaner 释放 DirectByteBuffer 关联的直接内存

当一个对象既被弱引用指向着, 又被强引用指向着时, 是不会在下一次GC时被当作垃圾进行回收的

上一篇:electron 截图,两种方式:desktopCapturer.getSources 与 navigator.mediaDevices.getUserMedia


下一篇:Java IO输入输出流 FileWriter 字符流