# Java虚拟机

# 预备知识

# Javac是什么

javac 是jre里面提供的工具,用来把.java文件编译成.class的字节码文件。 字节码文件经过jvm处理之后变成汇编指令。 汇编指令再加载到设备的主存之中,后变成机器指令由CPU执行。 所以, 只要是能够编译成.class字节码文件,就能由java虚拟机解释执行,实现跨平台。 kotlin groovy、JRuby、jython 都编译字节码由jvm解释执行。

# 为什么jvm执行的文件叫做字节码

这是因为jvm每次执行指令大小就是1个字节。这个字节里面包括了操作码和操作数。操作码就是运算符,操作数就是要进行运算的数。

# 引用类型有哪些

在Java中,有4种引用类型

  • 强引用(new Object):存在引用,GC时不会回收。不存在引用GC时会被回收。
  • 软引用(SoftReference):与是否存在引用无关,堆内存够时不回收,不够时回收。
  • 弱引用(WeakReference):与是否存在引用无关,只要触发GC就会被回收。
  • 虚引用(PhantomReference):任何时候都可能被回收,被回收时,会存入到给定的那个队列里面。

使用场景

  • 强引用:日常业务逻辑开发,成员变量,局部变量
  • 软引用:实现缓存类业务
  • 弱引用:避免内存泄漏,handler,dialog
  • 虚引用:使用虚引用管理堆外内存,实现清理类工作

# 01 Jvm内部结构

  • 类加载器:
  • 运行时数据区:
  • 执行引擎:

类加载器装载class文件到一块内存区域,这块内存区域就是运行时数据区。执行引擎从这块内存区域中取出数据通过本地库接口运行。本地接口库再对接不同的系统平台。


# 02 类加载过程 //todo

jvm的类加载加载类信息分为5步

  • 加载:把类信息加载到jvm的内存模型中的方法区
  • 验证:验证类型信息是否符合jvm要求的规范
  • 准备:分配变量内存和初始化值
  • 解析:把符号引用处理为直接引用
  • 初始化:合并类变量和静态语句块

# 03 运行时数据区存储

根据是否共享线程数据可分为2大区域
  • 线程私有部分:程序计数器、 方法栈

    每一个线程都有一个程序计数器和一个方法栈。程序计数器用来记录当前线程执行到哪一行,所以也叫行号记录器。方法栈用来存储执行一个方法所需要的数据,每执行一个方法,就会把这个方法的数据打包成一个栈帧入栈。

    栈帧中会存储:

    • 局部变量表:保存基础类型和对象的引用的局部变量,方法执行时会先保存当前调用对象this
    • 操作数:保存方法中的要操作的数值
    • 动态连接:保存多态中具体执行方法的地址
    • 返回地址:存放调用该方法的程序计数器的值
  • 线程公有部分: 方法区数据, 堆数据

    方法区保存类信息、常量、静态变量,堆保存Java对象信息。

# 局部变量的存储

如果是java基础类型, 则变量和值存储在方法栈中。 如果是引用类型, 变量存在方法栈中,值,也就是对象实例,存储在堆中。

# 成员变量的存储

全局变量, 也就是类中的变量。 不管是基础类型还是引用类型, 成员变量和值都存储在堆中。

所以大部分的对象实例都是存在堆内存中的, 只有方法中的基础类型成员变量和其值,引用变量这三种存储在栈内存中。JVM的GC主要针对的也是堆内存的回收,所以也被叫做GC堆。


# 04 JVM的内存分配

# 新生代和老年代

根据对象的存活周期, 堆内存分为新生代区域和老年代区域,分配比例为1:2。创建一个对象时会分配在新生代,创建一个大对象新生代没有足够的内存空间时这个对象会被分配到老年代。 在新生代经历过一定GC次数的对象,会被转到老年代。 这个次数不同的垃圾回收器是不同的,常见的是6次,15次。 还有一种情况未达到这个指定次数也会被转到老年代。比如, 新生代的对象个数是10个, 有6(超过一半)个对象经历的GC次数是3,那大于3的对象就会被转到老年代。

# 内存分配方式

根据内存是否规整有两种分配方式。

  • 指针碰撞
  • 空闲列表
# 指针碰撞

如果堆中内存分配是规整的,则只需要将记录内存的指针移动对象所需大小的位置。

# 空闲列表

如果堆中内存分配是不规整的,则需要记录哪些内存块已使用, 哪些内存块未使用。 从未使用的内存块中找出合适大小的分配给对象。

内存是否规整, 取决是jvm使用的垃圾回收器,带压缩整理的回收器,内存分配是规整的。


# 05 JVM内存回收

新生代老年代算法

新生代采用复制回收算法,老年代采用标记整理回收算法。

# 回收算法

# 复制算法

新生代被分配成8:1:1来实现复制回收算法。对象创建的时候分配在8和其中的一个1上面, 触发Minor GC时,把存活的对象复制到另外一个1上面,清空原来的8和1。每次触发Minor GC时都把原来的还存活的对象复制到空闲的那一块1上面。

当8这块内存区域被写满的时候,就会触发Minor GC,对新生代区域进行垃圾回收。

当老年代内存区域被写满,或者手动调用Systemt.gc()时会触发FullGC,对整个堆内存进行回收。

# 标记清除算法

老年代采用标记清除回收算法,通过可达性分析标记需要回收的对象,放在一个集合中,标记完成后统一回收。

# 标记整理算法

和标记清除算法差不多,只是在清除前进行一次整理,再清除。 具体实现是:把所有存活的都行都向一端移动进行整理,整理完成后,清除掉边界以外的内存。


# 06 JVM怎么判断对象是否存活

判断对象是否存活有两种方法

  • 引用计数法
  • 可达性分析法

# 引用计数法

给对象设置一个引用计数器,通过引用计数器来记录引用对象的地方, 引用+1,引用失效-1。为0时,代表对象已“死去”。引用计数法无法判断相互引用但实际上已经没有在使用的对象。

# 可达性分析法

可达性分析就是通过GC Roots 作为根节点向下搜寻,找出可达和不可达的对象。
# GC Roots

GC Roots就是对象,并且是当前不可被回收的对象。 哪些对象可以作为GC Roots对象?

  1. 类信息
  2. 静态变量引用的对象
  3. 常量引用的对象
  4. 栈帧中的Java对象

为什么以上对象可作为GC Roots?

  • 类信息、静态变量和常量都存储在方法区中。在进程运行期间,不会被回收。
  • 只要方法没结束, 栈帧中引用的对象就不会被回收。
Last Updated: 1/9/2024, 11:22:13 AM