Java虚拟机总结上篇

Java 虚拟机一直是 Java 的重难点,一方面由于系统封装得太好,你平常写程序的时候几乎感觉不到它的存在,另一方面了解必要的 Java 虚拟机工作原理才能对真实工作环境下的 bug 进行对症下药,另外虚拟机这一部分也一直是面试考官爱问的问题。于是这篇博客就针对 Java 虚拟机的各个知识点进行归纳。

一. Java 内存区域

运行时数据区域

程序计数器

程序计数器是当前线程执行的字节码的行号指示器,线程私有,独立存储

Java 虚拟机栈

Java 虚拟机栈是线程私有,与 Java 的方法执行模型有关,描述 Java 方法执行的内存模型: 方法执行时创建栈帧用于储存局部变量表等信息,方法调用返回对应栈帧再虚拟机中的入栈出栈。

既然是栈那么深度就是一定的,若线程请求栈深度大于虚拟机所规定的深度,则抛出 StackOverflowError 异常。若虚拟机栈请求扩展时无法申请到足够的内存,则抛出 OOM 异常。

本地方法栈

就是 Native 方法所用到的栈,与虚拟机栈作用类似。

Java 堆

Java 堆是被所有线程共享的一块内存区域,属于线程共享区,在虚拟机启动时创建。它主要作用是存放对象实例和进行垃圾收集管理。

方法区

方法区也是各个线程共享的内存区域,用于储存已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

运行时常量池

运行时常量池其实属于方法区,它主要用于存放编译期生成的各种字面量和符号引用,并且具有动态的特点。

new 关键字的创建流程

  1. 检查指令的参数能否在常量池中定位到一个类的符号引用
  2. 检查是否已经加载解析和初始化
  3. 从 Java 堆中划分内存给新生对象,使用 CAS 保证分配的原子性
  4. 将内存空间初始化为零值
  5. 对对象进行设置,存放在对象头中
  6. 执行方法,按照程序员的意愿进行初始化

分配方式

  1. 指针碰撞

    若 Java 堆中的内存都是规整的,用过的内存都在左边,没用过的都在右边,中间指针指向临界点,分配内存就很简单,只用把指针往右移动和待分配对象一样的内存区域就行了。

  2. 空闲列表

    如果内存不是规整的,用过的和没用过的内存交错在一起,就不能使用指针碰撞了,需要维护一个列表记录可用的内存块,分配内存时就从列表中找一块足够大的内存记录下来。

对象的内存布局

对象头

储存对象自身的运行时数据,eg:哈希码,GC 分代年龄,锁状态标志等。还有类型指针指向它的类元数据的指针,通过这个指针确定这个对象是哪个类的实例。若是 Java 数组则对象头还有一块记录数组长度的数据。

实例数据

程序代码中所定义的各种类型的字段内容,相同宽度的字段分配到一起

对象访问定位

虚拟机通过栈上的 reference 数据来操作堆上的具体对象。

访问方式

  1. 使用句柄

    包含对象实例数据与类型数据各自的地址信息,reference 中储存的就是对象的句柄地址。句柄地址稳定,对象移动时只改变句柄中的实例数据指针,reference 本身不修改。

  2. 直接指针

    reference 中储存的就是对象地址,速度更快

二. 垃圾收集器与内存分配策略

引用计数算法

给对象添加一个引用计数器,有一个地方引用它时,计数器值就加一,引用失效时就减一,任何时刻计数值为 0 的对象就死了。这个算法虽然简单但是有一个致命的缺点就是无法解决对象之间相互循环引用的关系。可达性分析算法应运而生。

可达性分析算法

GC Roots 作为起点向下搜索,若一个对象到 GC Roots 没有引用链的话,则证明此对象不可用,可以回收。搜索的对象有:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象

对象的回收经历

对象在没有引用链通往 GC Roots 时,需要经过两次标记才能真正死亡。

  1. 对象在进行可达性分析后如果没有与 GC Roots 相连接的引用链,会被第一次标记并筛选,若对象没有覆盖 finalize 方法或者已经调用过了则不会调用 finalize。如果需要调用 finalize 方法,则对象被放在 F-Queue 队列中,等待线程执行。
  2. 对象如果想存活下去,finalize 方法是最后的机会,否则 GC 对 F-Queue 队列进行第二次标记后对象真正死亡。

垃圾回收算法

标记 - 消除算法

首先标记出所有需要回收的对象,在标记完成后统一回收,缺点是效率低下而且产生大量的内存碎片。

复制算法

将内存划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后把已经使用的内存空间一次清理掉。缺点是将内存缩小为了原来的一半,代价较高,对象存活率较高时效率低。

HotSpot 实际使用(回收新生代)则是将内存划分为较大的 Eden 区和两块较小的 Survivor 区,一块 Eden 区和一块 Survivor 区大小比例为 8:1,垃圾回收时就将 Eden 区和已使用的 Survivor 区中还存活的对象移到另一块 Survivor 区中,由于根据统计,98% 的对象都是很快死亡的,所以按照 8:1:1 的比例来划分内存明显比 1:1 划分内存效率要高很多。

标记 - 整理算法

标记出需要回收的对象,然后让所有存活的对象都向一段移动,将另一端的内存区域清除掉。

分代收集算法

根据新生代和老年代的不同特点选择不同的算法,新生代使用复制算法,老年代使用标记清楚或标记整理算法,虚拟机实际使用这种算法。

内存分配与回收策略

对象优先在 Eden 上分配

GC 分类

  1. Monior GC,新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特点,所以 Monior GC 很频繁,速度也很快
  2. Major GC/Full GC,老年代 GC,指发生在老年代的垃圾回收动作,一般比 Monior GC 慢十倍以上。

大对象直接进入老年代

大对象指需要大量连续内存空间的 Java 对象,如很长的字符串以及数组。直接进入老年代避免频繁的 GC 活动。

长期存活的对象将进入老年代

对象在新生代区域每熬过一次 Minor GC,年龄就增加一岁(Age Count),超过 15 岁(默认),就会被晋升到老年代中。

动态年龄判定

如果相同年龄的对象所占内存大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。

三. 类文件结构

Class 类文件的结构

一组以八位字节为基础的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有任何分隔符。

储存结构

无符号数,用来描述数字,索引引用,数量值或 UTF-8 编码的字符串

,多个无符号数 + 表 = 表,_info 结尾,Class 实际上就是一张表

魔数

每个 Class 文件的头 4 个字节,确定这个文件是否为一个能被虚拟机接受的 Class 文件。class 文件的魔数是 0XCAFEBABE。

Class 文件的版本号

紧跟魔数的四个字节确定版本号:5,6 字节为次版本号,7,8 字节为主版本号。jdk 向下兼容,不向上兼容。

常量池

紧随主次版本号之后包含:

  • 字面量文本字符串,申明为 final 的常量值。
  • 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • 动态连接各个字段的内存信息,从常量池中获得对应的读出引用,再在类创建时或运行解析翻译到具体的内存地址之中。
  • 每一项常量都是一个表,每个表的第一位都是一个是一个 u1 类型的标志位,代表这个常量属于哪种常量类型。

访问标志

紧随常量池后面,两个字节代表访问标志,标识类或接口的访问信息。如这个 Class 是类还是接口,public 类型等。

类索引,父类索引,接口索引集合

除了接口索引是集合外,其他索引都只有一个,用这三个索引确定类的继承关系。类索引用于确定类的全限定名,父类索引用于确定父类的全限定名。

字段表集合

用于描述类或接口中声明的变量,字段包括类级变量和实例级变量,不包括方法中声明的局部变量,描述字段的属性如 public,static,final 等用一个布尔变量表示,刚好使用一个标志位,通过引用常量池中的常量来确定。

方法表集合

与字段表相似。

属性表集合

Class 文件,字段表,方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。

字节码指令

操作码长度为一个字节,所以总数最多不超过 256 条。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值