浅析Java 虚拟机
从为什么要学JVM开始
- 深入了解Java,了解JVM,才能进一步了解Java内部运作
- 解决JVM的问题,如内存溢出、GC频繁等
了解Java历史
JavaSE、JavaEE、JavaME
-
Java SE(J2SE,Java 2 Platform Standard Edition,标准版) Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。
-
Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版) Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。
-
Java ME(J2ME,Java 2 Platform Micro Edition,微型版) Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。
JDK与JRE
JRE(Java Runtime Environment)仅包含运行 Java 程序的必需组件,包括 Java 虚拟机以及 Java 核心类库等。而 JDK(Java Development Kit)除了包含 JRE外,还附带了一系列开发、诊断工具。
如果只需要运行 Java 程序,那么你安装 JRE 即可。但如果你要进行 Java 程序的开发,那么你就需要安装 JDK。
JVM架构
- 子系统
- Class loader(类装载器)
- 根据全限定名类名来装载class文件到Runtime data area
- Execution engine(执行引擎)
- 执行字节码指令
- 会使用即时编译技术将方法编译后译成机器码后再执行
- Class loader(类装载器)
- 组件
- Runtime data area (运行时数据区)
- 运行时数据存储的地方,即JVM的内存
- Native interface (本地接口)
- 与native libraries交互的接口
- Runtime data area (运行时数据区)
流程
Java 源代码 被编译器转换为 字节码 类加载器 将字节码 加载到运行时数据区 执行引擎 将字节码转为底层系统指令 交由CPU执行
Java内存模型
公有
(非线程安全)
Java堆
Java程序最主要的工作区域,在JVM启动时建立,专门用于实例对象内存分配的区域(但也有直接在栈上进行分配的情况)
Java NIO库会像直接内存区申请空间,直接内存访问速度优于Java堆,频繁读写的情况会考虑使用直接内存
方法区
存储 Java 类字节码数据的一块区域,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。储存类级别数据
私有
PC寄存器(程序计数器)
Program Counter 寄存器,指的是保存线程当前正在执行的方法。线程没有记忆,当发生线程切换时,需要回复线程状态(即执行到哪里),需要依靠计数器完成
Java虚拟机栈
Java 虚拟机栈,这个栈与线程同时创建,用来存储栈帧,即存储局部变量与一些过程结果的地方。栈帧存储的数据包括:局部变量表、操作数栈。
保存:局部变量、方法参数、Java方法的调用、返回
- 栈帧
- 局部变量数据
- 操作数栈
- 帧数据
本地方法栈
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的
Java类加载机制
开始加载
- 启动类加载器 – 负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。
- 扩展类加载器 – 负责加载ext 目录**(jre\lib)内的类.**
- 应用程序类加载器 – 负责加载应用程序级别类路径,涉及到路径的环境变量等etc.
将字节码转为二进制字节流加载到内存中
链接
校验
--字节码是否正确
- JVM规范校验
- JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
- 代码逻辑校验
- JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
准备
--分配内存和初始化默认值给静态变量
- 内存分配的对象
- 初始化的类型
解析
--将符号内存引用替换为原始引用
JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
初始化
- 最后阶段,赋值静态变量、执行静态块
垃圾回收
对已经分配的内存空间进行分配
判断垃圾对象
不再被引用的的对象
引用计数法
设置一个计数变量,当被引用时计数+1,被出去引用时-1;当计数变量为0时,表示对象不能再被引用
实现简单 但无法解决循环引用的问题
GC Root Tracing
可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
内存分区
回收策略
分代回收
Young Generation
Eden
新创建的类 C1
From
一次minor GC后,C1存活 进入 From
To
两次minor GC 后 C1 存活,age+1
再一次minor GC
- 如果C1同龄的类没有达到 survivor 空间的一半 互换From 和to
- 一直到达到survivor的一半以上(toSuv的区域已经满了) C1 进入老年代
Old Generation
老年代空间比率超过阈值 出发marjor GC
算法
复制算法
复制算法。复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收。
标记清除算法
在标记阶段,标记所有由 GC Root 触发的可达对象。此时,所有未被标记的对象就是垃圾对象。之后在清除阶段,清除所有未被标记的对象。
标记压缩算法
分别是:标记结算、压缩阶段。在标记阶段,从 GC Root 引用集合触发去标记所有对象。在压缩阶段,其则是将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。
| 算法 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 复制算法 | 内存空间分配连续,效率高 | 内存空间折半,降低使用率,而且需要移动对象 | 存活对象少 |
| 标记清除算法 | 实现简单,不用移动对象地址 | 空间碎片,内存空间不连续,分配效率降低 | 存货对象多 |
| 标记压缩算法 | 优化了标记清除算法,减少了空间碎片的产生 |