jvm 学习笔记与总结

707 阅读9分钟

jvm 总结

jvm 体系

定义:jvm是一台运行java字节码文件的虚拟计算机,拥有独立的运行机制,其字节码也未必由java编译而来
jvm虚拟机
特点:
jvm 只认编译好的字节码文件,并不只是与java 绑定,因此其他语言编译结果 只要满足并包含jvm的内部的指令集、符号集、以及其他辅助信息,就可以被jvm识别并装载运行

img_2.png

jvm 内存结构

包含五个模块:
1、程序计数器
2、java 虚拟机栈
3、本地方法栈
4、堆
5、方法区

img.png

程序计数器

定义:是一块比较小的内存空间,是当前线程正在执行的那条字节码指令的地址。
作用:
1、通过改变程序计数器依次读取指令,从而实现代码的流程控制
2、在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。
特点:
1、线程私有,每条线程都有自己的程序计数器。
2、生命周期:随着线程的创建而创建,随着线程的结束而销毁。
3、是唯一一个不会出现OutOfMemoryError的内存区域。

虚拟机栈

定义:java 虚拟机栈 是描述Java 方法运行过程的内存模型
压栈出栈过程:
当方法运行过程中需要创建局部变量时,会将局部变量的值存入栈帧中的局部变量表中
java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,pc寄存器会指向这个地址,只有这个活动的栈帧的本地变量可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。
方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。
特点:
1、虚拟机栈与线程是对应的,数据不是线程共享的,不用关心数据一致性问题,也不存在同步锁的问题
2、Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
    StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
    OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。

img_1.png

定义:堆是用来存储对象的内存空间,几乎所有的对象都存储在堆中
特点:
1、线程共享,整个java 虚拟机中只有一个堆,所有的线程都访问一个堆
2、在虚拟机启动时创建
3、分为:老年代、新生代
4、Java 堆所使用的内存不需要保证是连续的。而由于堆是被所有线程共享的,所以对它的访问需要注意同步问题,方法和对应的属性都需要保证一致性。

深入:
1、堆内存设置:
    -Xms 堆起始内存 -Xmx 堆最大内存,一般设置 Xmx = Xms,目的是为了在java 垃圾回收机制清理完堆后 不需要重新分隔计算堆区的大小,从而提高性能
2、年轻代与老年代:
    

方法区

定义:是堆的一个逻辑部分,存放(已加载的类信息、常量、静态变量、编译后的代码)
特点:
1、线程共享,整个虚拟机中只有一个方法区
2、永久代。 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。
3、内存回收效率低。 方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。
4、Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。

运行时常量池

方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。

当类被 Java 虚拟机加载后, .class 文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如 String 类的 intern() 方法就能在运行期间向常量池中添加字符串常量。

直接内存

直接内存是除 Java 虚拟机之外的内存,但也可能被 Java 使用。

操作直接内存
在 NIO 中引入了一种基于通道和缓冲的 IO 方式。它可以通过调用本地方法直接分配 Java 虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,从而提高了数据操作的效率。

直接内存的大小不受 Java 虚拟机控制,但既然是内存,当内存不足时就会抛出 OutOfMemoryError 异常。

直接内存与堆内存比较
直接内存申请空间耗费更高的性能
直接内存读取 IO 的性能要优于普通的堆内存。
直接内存作用链: 本地 IO -> 直接内存 -> 本地 IO
堆内存作用链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO
服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

对象内存布局、对象创建过程以及访问方式

对象内存布局

在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:

  • 对象头(Header)

  • 实例数据(Instance Data)

  • 对齐填充(Padding)

    对象头:哈希码,gc分代年龄,线程持有的锁,线程id 实例数据:实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。

img_3.png

对象创建过程

1、类加载:首先判断常量池中该类是否被加载、初始化了,如果没有先执行相应的类加载过程
2、为对象分配内存:类加载完成后便可以完全确定 对象所需要的内存大小,给对象分配相应的堆内存
3、初始化:分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。

对象的访问方式

对象的存储空间放在堆中,而对象的引用放在栈中,

垃圾收集策略与算法

Gc Root 的定义:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

可达性分析发

所有与 GC Roots 相关的对象是有效对象,无关则为 无效对象(需回收)

标记回收算法

遍历所有的GC Roots,所有 Gc roots 可达的对象标记为存活的对象,其他对象全部清除,存在两缺点

  • 效率问题:标记和清除两个过程的效率都不高
  • 空间问题:清除后产生大量不连续的内存碎片,后续非配大对象时,无法找到连续的内存

复制算法(用于新生代)

将内存分为两块,当需要进行垃圾回收时,将存活的对象复制到另外一块上、另一块完全清除,优劣势如下:

  • 优点:不会有内存碎片问题
  • 缺点:内存缩小为原来的一半,浪费空间

为了解决空间利用率的问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。

但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。

标记-整理算法(老年代)

标记:它的第一个阶段与标记-清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。

整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。

内存分配规则

对象主要分配到 新生代的 Eden,少数分配老年代,当Eden区没有足够空间进行分配时,虚拟机将发起一次 Minor Gc

  • Minor GC:回收新生代(包括 Eden\Survivor),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。

对象进入老年代:

  • 大对象直接进入老年代,大对象是指需要大量连续内存空间的Java 对象,如:很长的字符串或数据
  • JVM 给每个对象都定义了年龄计数器,每一次 Minor GC 存活下来的对象 年龄 +1,达到阈值进入老年代,可配置