【Java】Java 04 Java虚拟机
Java
是一门面向对象编程语言,不仅吸收了 C++ 语言的各种优点,还摒弃了 C++ 里难以理解的多继承、指针等概念,因此 Java 语言具有功能强大和简单易用两个特征。Java 语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。
1 运行时数据区域
- 程序计数器
- 是一块较小的内存,是当前线程所执行的字节码的行号指示器。
- Java 虚拟机栈
- 生命周期和线程相同。
- 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量和操作数(对象引用)。
- 本地方法栈
- 与 Java 虚拟机栈类似,但它为虚拟机使用到的本地方法服务。
- Java 堆
- 虚拟机启动时创建。
- 存放对象实例和数组。所占内存最大。
- 分为新生代(Young),老年代(Old)。新生代分 Eden 区,Servior 区。Servior 区又分为 From space 区和 To Space 区。
- 方法区
- 用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
- 方法区又称“永久代”。
- 运行时常量池
- 运行时常量池是方法区的一部分。
- Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
- 直接内存
- 不是运行时数据区域的一部分,但是经常使用。
- 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。
1.1 内存分配策略
- 对象优先在 Eden 分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定
- 空间分配担保
1.2 内存泄漏和内存溢出
- 内存泄露:memory leak,是指程序在申请内存后,无法释放已申请的内存空间
- 内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用
- 内存泄漏最终会导致内存溢出
内存泄漏的场景
- 静态集合类,它们的生命周期和应用程序一致,所引用的所有对象也不能被释放。
- 监听器。
- 各种连接,需要显示调用 close() 方法。
- 单例模式,单例对象在初始化后将在 JVM 的整个生命周期中存在。
2 垃圾回收
回收区域:堆内存、方法区
分类
- 部分收集 Partial GC
- 新生代收集 Minor GC / Young GC:目标是新生代
- 老年代收集 Major GC / Old GC:目标是老年代,只有 CMS
- 混合收集 Mixed GC:目标是整个新生代和部分老年代,例:G1
- Full GC:收集整个 Java 堆和方法区
触发条件
- Minor GC:当 Eden 区满时
- Full GC
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
- 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
2.1 垃圾回收算法
对象回收判断
- 引用计数法:在对象中添加一个引用计数器,有引用则加一,引用失效则减一。
- 主流 Java 虚拟机中都不使用。
- 可达性分析算法:通过 GC Roots 根对象作为起始节点集,判断从 GC Roots 到某个对象是否可达。
- 方法区的回收:主要是对常量池的回收和对类的卸载。
分代收集理论
- 新生代(Young Generation):用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的 1/15。新生代的特点是产生大量的死亡对象。
- 老年代(Old Generation):里面的对象基本都是在 Survivor 区域中熬过来的,不会轻易死亡。
- 每次垃圾收集时都发现有大批对象死亡,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
垃圾回收算法
- 标记-清除:首先标记所有需要回收的对象,统一回收掉所有被标记的对象。
- 标记-复制:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面然后再把已使用的内存空间一次清理掉。
- 标记-整理:首先标记所有需要回收的对象,然后让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
- 分代收集
- 新生代使用:标记-复制
- 老年代使用:标记-清除 / 标记-整理
Java 提供了四种强度不同的引用类型。
- 强引用:被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。
- 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。使用 SoftReference 类来创建软引用。
- 弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用 WeakReference 类来创建弱引用。
- 虚引用:在这个对象被回收时收到一个系统通知。使用 PhantomReference 来创建虚引用。
2.2 垃圾回收器
- Serial 收集器
- 新生代
- 单线程
- 标记-复制算法
- 适合客户端模式下的虚拟机
- ParNew 收集器
- 新生代
- Serial 收集器的多线程版本
- 标记-复制算法
- 适合服务端模式下的虚拟机
- Parallel Scavenge 收集器
- 新生代
- 多线程
- 标记-复制算法
- 目标是达到一个可控制的吞吐量
- Serial Old 收集器
- Serial 收集器的老年代版本
- 单线程
- 标记-整理算法
- 适合客户端模式下的虚拟机
- Parallel Old 收集器
- Parallel Scavenge 收集器的老年代版本
- 多线程
- 标记-整理算法
- CMS 收集器(Concurrent Mark Sweep)
- 老年代
- 标记-清除算法——会产生内存空间碎片
- 过程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- G1 收集器(Garbage First)
- 面向服务端应用的垃圾收集器,不需要和其他收集器配合,可以独立管理 GC 堆
- 过程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
3 虚拟机类
3.1 平台无关性
平台无关性:一次编译,到处执行(Write Once, Run Anywhere)。
- Java 虚拟机:扮演了运行时 Java 程序与其下的硬件和操作系统之间的缓冲角色。
- Class 文件:Java 虚拟机只与由自己码组成的 Class 文件进行交互。
- Java 语言规范:规定 Java 语言中基本数据类型的取值范围和行为。
Javac 编译过程:解析与填充符号表、注解处理、分析与字节码生成
3.2 类文件结构
Class 文件格式采用类似 C 语言结构体的伪结构来存储数据,有两种数据类型
- 无符号数:属于基本数据类型,可以描述数字、索引引用、数量值、字符串值
- 类索引、父类索引、接口索引
- 表:由多个无符号数或其它表作为数据项构成的复合数据类型,命名以
_info
结尾- 常量池 —— Class 文件的资源仓库,主要存放两大类常量,每一项常量都是一个表
- 字面量:文本字符串、final 常量值等
- 符号引用:方法名称和描述符、字段名称和描述符等
- 字段表:描述接口或类中声明的变量
- 方法表
- 属性表
- 常量池 —— Class 文件的资源仓库,主要存放两大类常量,每一项常量都是一个表
3.3 类加载机制
类加载机制:虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机使用的 java 类型。
类的生命周期
- 加载(Loading):1)获取定义此类的二进制字节流;2)将静态存储结构转化为方法区的运行时数据结构;3)生成代表该类的 java.lang.Class 对象
- 连接(Linking)
- 验证(Verification):确保 Class 文件的字节流中的信息符合 JVM 规范
- 准备(Preparation):正式为类中变量分配内存并设置类变量初始值
- 解析(Resolution):将常量池内的符号引用替换为直接引用
- 初始化(Initialization):真正开始执行 Java 代码,将主导权移交应用程序
- 使用(Using)
- 卸载(Unloading)
3.4 类加载器
类加载器:虚拟机团队把类加载阶段中通过一个类的全限定名来获取此类的二进制流放到虚拟机外部了。
- 启动类加载器(BootstrapClassLoader):负责加载
<JAVA_HOME>\lib
中的类库,由 C++ 编写 - 扩展类加载器(ExtensionClassLoader):负责加载
<JAVA_HOME>\lib\ext
中的类库 - 应用程序类加载器(ApplicationClassLoader)
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。如果父加载器无法完成此加载任务,子加载器才会尝试自己去加载。优势
- 避免类的重复加载
- 保证了安全性
破坏双亲委派模型
- 由该模型本身的缺陷造成。提供了线程上下文类加载器(ThreadContextClassLoader)来解决困境。
- 由于用户对程序动态性的追求导致的。