JVM
2022-05-13 09:39:24 1 举报
AI智能生成
JVM的知识树
作者其他创作
大纲/内容
JVM概述
JVM 介绍
- 【重点】了解 JVM 的定义,了解 JVM 概念的基础
- 【重点】了解 JVM 存在的价值及意义,从使用层面了解 JVM 存在的意义,
- 了解 JVM 整体结构,该结构图是从宏观层面,介绍的虚拟机的整体结构模块
后续会对每个模块进行细致的介绍与讲解,此处可视作了解内容,为后续内容的学习奠定基础; - 了解如何查看自己所使用的 JVM 版本,安装完成 JDK 的学习者,都可以进行查看;
- 了解 JVM ,JRE 和 JDK 三者直接的区别,学习 JVM 前需要掌握的基础知识。
JVM 定义
JVM (Java Virtual Machine 简称 JVM)
亦可称之为: Java 虚拟机。
它是运行所有 Java 程序的抽象计算机
Java 语言的运行环境
JVM 是 Java 语言的一大关键亮点
它是 Java 最具吸引力的特性之一
虚拟机
从字面意义上来理解,虚拟机是一个虚拟化出来的计算机。
举例说明
Windows 操作系统上安装 Linux 的虚拟机,然后在 Linux 虚拟机上进行 Shell 脚本的编写练习
从这个角度上来说, Linux 虚拟机就类似于 JVM
不同的是, Linux 虚拟机支撑了 Shell 脚本的运行环境,而 JVM 支撑了 Java 语言的运行。
特性
JVM 是一种抽象化的计算机,通过在实际的计算机上,仿真模拟各种计算机功能,来实现的。
Java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java 虚拟机屏蔽了与具体操作系统平台相关的信息
JVM 的作用
跨平台性
为什么咧?
Java 语言之所以有跨平台的优点,完全是 JVM 的功劳
跨平台性是 JVM 存在的最大的亮点。
这大大提升了 Java 语言的平台灵活性,能够在众多语言争鸣的时代,脱颖而出。
举例说明
Windows 操作系统安装上 JVM 之后,可以支持 Java 程序的运行;
Linux 操作系统安装上 JVM 之后,可以支持 Java 程序的运行;
Unix 操作系统安装上 JVM 之后,可以支持 Java 程序的运行。
为什么Java具有跨平台性?
Java 虚拟机屏蔽了与具体操作系统平台相关的信息
使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码)
这样就可以在多种平台上不加修改地运行。
垃圾回收机制
JVM具有优秀的垃圾回收机制
Java语言的优势
Java 语言的诞生,极大降低了学习难度
除了 Java 面向对象编程的特性能够降低学习难度以外
【重点】 Java 编程时,可以更少的去考虑垃圾回收机制。
JVM 拥有自己的垃圾回收机制,为开发人员分担了部分工作。
C语言的缺点
【编程难度升级】要通过代码手动实现内存垃圾的回收与空间释放
【高门槛】因为考虑内存空间释放,更多的会涉及到底层的知识
如何查看自己的 JVM
如果需要运行 Java 程序,必须要安装 JDK,这说明 JDK 中就包含了支持 Java 语言运行的JVM
如何查看本机的 JVM 信息
无论是 Windows 操作系统还是 Linux 操作系统,正确安装 JDK 并且配置好环境变量后
在命令行,输入如下命令查看
java -version
以本人的机器为例,可以看到如下的执行结果:
C:\Users\Wayne.WangTJ>java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
仔细看下最后一句执行结果,Java HotSpot™ 64-Bit Server VM (build 25.191-b12, mixed mode)
这就是当前电脑中 Jvm 虚拟机操作系统的版本。 当然了,安装不同的版本,结果是有所区别的。
JVM,JRE 和 JDK 联系
定义
JDK
JDK
全称 java development kit ,开发工具包
【Java的核心】面向开发者,为开发者提供开发类库
JDK 包含了JRE,一堆工具类(javac、java)以及 Java 的基础类库(Object,string);
Java开发工具包(JDK)是完整的Java软件开发包
包含了JRE,编译器和其他的工具(比如:JavaDoc,Java调试器),
可以让开发者开发、编译、执行Java应用程序。
JRE
Java运行时环境(JRE)是将要执行Java程序的Java虚拟机。
它同时也包含了执行applet需要的浏览器插件。
全称 java runtime environment。
包含了JVM 实现和需要的类库
JRE 是一个运行环境,并非开发工具
JVM
JVM(Java虚拟机)
JVM 是可运行 Java 代码的假想计算机
它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java源文件被编译成能被Java虚拟机执行的字节码文件。
JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。
Java虚拟机是一个可以执行Java字节码的虚拟机进程。
JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java 语言最重要的特点就是跨平台运行。使用 JVM 就是为了实现跨平台。
区别
JDK、JRE、JVM的区别是什么?
JDK 和 JRE 的区别
JDK 是开发工具包,包含了JRE
JRE 是运行环境,不提供开发工具包。
JRE 和 JVM 的区别
JRE 包含了JVM,JRE = JVM+lib。
JRE 为 Class 文件提供了运行的环境,但是需要 JVM 进行 Class文件的翻译
JVM 将翻译好的文件传给操作系统或者是 CPU 映射指令集,才能够最终完成运行。
联系
JVM 不能够单独的搞定 class 文件
解释 Class 时,JVM需调用所需类库lib
JVM(安装路径\Java\jre1.8.0_144\bin)
所需类库(安装路径\Java\jre1.8.0_144\lib)
笼统的来说,JVM + lib = JRE
JDK 是基于 JRE 基础之上进行的。
总体来说,我们利用 JDK 开发了属于我们自己的程序
通过 JDK 的 javac 工具包进行了编译
将 Java 文件编译成为了 class 文件(字节码文件)
在 JRE 上运行这些文件的时候
JVM 进行了这些文件(字节码文件)的翻译
翻译给操作系统,映射到 CPU 指令集或者是操作系统调用,最终完成了我们的代码程序的顺利运行。
三者互相配合不可分割。
Java 为什么能够跨平台的原因
为什么Java被称作是“平台无关的编程语言”?
Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。
Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了
当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。
程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
什么是值传递和引用传递?区别?
值传递(传递的是对象的副本)
对象被值传递,意味着传递了对象的一个副本。
就算是改变了对象副本,也不会影响源对象的值。
引用传递(传递的是对象的引用)
对象被引用传递,意味着传递的并不是实际对象,而是对象引用。
外部对引用对象所做的改变,会反映到所有对象上。
JVM 整体架构
前言
- 认识 JVM 整体结构,对 JVM 的各个模块作用有一个初步了解
- 对「类加载子系统模块」,进行更加细粒度的模块划分介绍,从概念层面,掌握类加载的步骤,
- 对「运行时数据区」进行更加细粒度的模块划分介绍,从概念层面,了解运行时数据区的五大模块定义及作用
- 对「执行引擎」进行更加细粒度的模块划分介绍
- 了解 JVM 的生命周期
JVM 整体架构
Class 文件
主要指编译成字节码的 Java 文件
Class 文件才是 JVM 可以识别的文件
Java 文件需要先进行编译成Class文件,才可进入 JVM 执行
类加载子系统、类装载器子系统
(ClassLoader)
类的加载
主要负责从文件系统,或者网络中加载 Class 信息,并与运行时数据区进行交互
运行时数据区
方法区
堆区域
栈区
PC寄存器、程序计数器
本地方法栈
执行引擎
分配给运行时数据区的字节码将由执行引擎执行,执行引擎读取字节码并逐个执行
垃圾回收器就是执行引擎的一部分
本地方法接口
本机方法库进行交互
提供执行引擎所需的本机库
本地方法库
它是执行引擎所需的本机库的集合
一个 Java 文件在 JVM 中的流转过程
一个 Java 文件在 JVM 中的流转过程
步骤 1 : Demo.java 文件,通过 JDK 的 javac 命令,成功的被编译成为了 Demo.class 文件;
步骤 2 :JVM 有自己的类加载器,将编译好的 Demo.class文件进行了加载;
步骤 3 :类加载器将加载的 Demo.class文件投放到了运行时数据区,供程序执行使用;
步骤 4 :运行时数据区将字节码文件,交给执行引擎执行;
步骤 5 :执行引擎执行完毕,会对运行时数据区的数据进行操作
比如说垃圾回收机制是执行引擎的一部分,
垃圾回收机制,针对的是运行时数据区的堆空间
步骤 R :图中有很多步骤 R ( 代表 Random,随机发生的步骤 )。
其实就是在执行过程中的一个本地方法的调用,只要程序在运行过程中需要调用本地方法,那么步骤R就会发生。
类加载子系统的三大步骤
类加载子系统的处理过程
类加载子系统的处理过程
Java 的动态类加载功能,由类加载器子系统处理,处理过程包括加载、链接和初始化
类加载子系统的三个步骤
加载
通过三种不同的类加载器对 Class 文件进行加载
【自定义类加载器】通过复写 classLoader 方法
链接
对加载好的 Class 文件进行字节码、静态变量、方法引用等
进行验证和解析,为初始化做准备。
初始化
类加载的最后阶段
对类进行初始化
运行时数据区的五个模块
方法区
Method Area
存储
静态变量
类级数据
数量
只有一个
线程不安全
它是一个共享资源
由于方法区域共享多个线程的内存,所存储的数据不是线程安全的
为共享内存区域,多线程环境下共享这两块内存区域。
堆区域
Heap Area
存储
对象
对象对应的实例变量
数组
数量
只有一个
线程不安全
由于堆区域共享多个线程的内存,所存储的数据不是线程安全的
为共享内存区域,多线程环境下共享这两块内存区域。
栈区
Stack Area
对于每个线程,将创建单独的运行时栈。
对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。
存储
局部变量
所有局部变量将在栈内存中创建。
线程安全
栈区域是线程安全的,因为它不共享资源;
线程私有部分,私有数据对其他线程不可见。
PC寄存器、程序计数器
PC Registers
也称作程序计数器
每个线程都有单独的 PC 寄存器
用于保存当前执行指令的地址。
一旦执行指令,PC 寄存器将被下一条指令更新
线程安全
线程私有部分,私有数据对其他线程不可见。
本地方法栈
Native Method stacks
对于每个线程,将创建一个单独的本地方法栈。
存储
本地方法信息
线程安全
线程私有部分,私有数据对其他线程不可见。
执行引擎的三个模块
解释器
作用于字节码的解释。
缺点:当一个方法被调用多次时,每次都需要一个新的解释
JIT 编译器
消除了解释器的缺点
执行引擎将在转换字节码时,使用解释器的帮助,但是当它发现重复代码时,将使用 JIT 编译器
好处:提高了系统性能
垃圾回收器
GC,Garbage Collector
收集和删除未引用对象
可通过调用 System.gc() ,触发垃圾收集
JVM生命周期
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap)创建一个初始类(initail)来完成的这个类是由虚拟机具体指定实现的
虚拟机的执行
一个运行着的java虚拟机有一个清晰的任务:执行java程序
程序开始,虚拟机执行,程序结束,虚拟机也停止
所谓执行一个java程序,其实上就是执行一个叫java虚拟机的进程
虚拟机退出
程序正常执行结束,虚拟机退出
程序在执行过程中,遇到异常或错误而异常终止
由于操作系统的错误而导致系统终止
某线程调用Runtime类或System类中的exit方法,或Runtime中的halt方法
图解JVM知识
子主题
运行时数据区/JVM内存模型/内存结构/线程内存区域
前言
java虚拟机在执行程序的过程中会将它所管理的内存分成若干个部分管理
运行时数据区可细分为五个模块
方法区
堆
栈(虚拟机栈)
本地方法栈
寄存器
了解栈的基本概念及特点【基础】
理解并掌握栈帧的概念以及栈帧的数据结构,并对栈帧结构中的局部变量表,操作数栈,动态链接以及返回地址做详细的讲解【重点】
理解并掌握寄存器的概念及作用【重点】
理解并掌握栈帧的概念以及栈帧的数据结构,并对栈帧结构中的局部变量表,操作数栈,动态链接以及返回地址做详细的讲解【重点】
理解并掌握寄存器的概念及作用【重点】
图解
运行时数据区图解
运行时数据区图解
JVM的内存模型
JVM的内存模型
JVM内存的分类
组成Java内存模型JDK1.7
运行时数据区的五个模块组成(JVM内存区域)
方法区
前言
了解方法区的作用及意义【基础】
了解方法区存放数据类型【重点】
了解运行时常量池【重点】
了解方法区与堆内存结构的关系,对比讲解JDK 1.8 版本【重点】
简介、基本概念
Method Area
方法区,也称非堆(Non-Heap)
方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是连续的
关闭JVM就会释放这个区域的内存
《Java虚拟机规范》中明确说明:
“尽管所有方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”
但对于HotSpotJVM而言,方法区还有一个别名叫作Non-Heap(非堆),目的就是要和堆分开。、
所以,方法区看作是独立于Java堆的内存空间
在方法区也是存在垃圾回收的,尽管它很少出现
该区域的内存内存回收目标主要是针对
常量池的回收
类型的卸载
数量
只有一个
线程不安全
是一个被线程共享的内存区域。
它是一个共享资源
方法区与Java堆一样,是各个线程共享的内存区域
由于方法区域共享多个线程的内存,所存储的数据不是线程安全的
为共享内存区域,多线程环境下共享这两块内存区域。
存储内容(已被虚拟机加载的信息)
方法区存放的数据与堆内存之间的关系
方法区存放的数据与堆内存之间的关系
【看图】方法区存放了 ClassLoader 对象的引用,也存放了一个到Class对象的引用
【看图】这两个引用的对象实例会存放到堆内存中。
加载的类字节码
一个到类 ClassLoader 的引用
对 ClassLoader 的引用,这个引用指向对内存
JIT 编译器编译后的代码
即时编译器编译后的代码
(JTL)即时编译器编译后的代码缓存等数据
静态变量
静态变量
final类型常量
static 变量
static-final 常量
类中的静态变量信息
static+final
静态常量,编译期常量,编译时就确定值。
(Java代码执行顺序,先编译为class文件,在用虚拟机加载class文件执行)
放于方法区中的静态常量池。
在编译阶段存入调用类的常量池中
如果调用此常量的类不是定义常量的类,那么不会初始化定义常量的类
因为在编译阶段通过常量传播优化,已经将常量存到调用类的常量池中了
区别final
常量,类加载时确定或者更靠后。
当用final作用于类的成员变量时,成员变量必须在定义时或者构造器中进行初始化赋值
(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是它指向的对象的内容是可变的
这里只的静态变量的引用,即,public static Person person = new Person();
存放的是 person部分(即,静态类型),后面的实体是存放在堆中。
存放的是 person部分(即,静态类型),后面的实体是存放在堆中。
JDK7以后,静态变量就移动至堆中保存,概念上逻辑上(概念上)依然是属于方法区
域信息
所有域的相关信息
域名称
域类型
域修饰符
域申明顺序
元数据对象
具体:class/method/field 等
方法信息
方法信息:类中方法的信息;
方法代码
方法名称
方法名
方法的放回类型(或 void)
方法参数的数量和类型(按顺序)
方法修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
方法的字节码、操作数栈大小、局部变量表及大小(abstract和native方法除外)
异常表 (abstract和native方法除外)
记录着每个异常处理的
开始位置
结束位置
代码处理在程序计数器中的偏移地址
被捕获的异常类的常量池索引
类型信息或类信息
类型全限定名
类的完整有效名
类型的完整有效名称(全名 = 包名.类型)
全限定名为 package 路径与类名称组合起来的路径;
类型的直接超类的全限定名
类型的直接父类的完整有效名
该类型直接父类的完整有效名
父类或超类的全限定名;
(对于interface或是Object类,都没有父类)
除非这个类型是interface或是 java.lang.Object,两种情况下都没有父类
类型是类类型还是接口类型?
类型直接接口的一个有序列表
类的直接接口的一个有序列表
判定当前类是 Class 还是接口 Interface;
类型的访问修饰符
判断修饰符,如 pulic,private 等;
修饰符(public,private...)
类型的修饰符
public,abstract,final的某个子集
类级数据
返回值类型
被JVM加载的
一个到 Class 类的引用
对对象实例的引用,这个引用指向对内存。
字段信息
变量名
类中字段的信息
包含了一个特殊的区域 “运行时常量池”
常量
类型的常量池
Class文件中的常量池表和运行时常量池区别
运行时常量池是方法区的一部分
运行时常量池具备动态性
动态性:Java语言并不要求常量一定只有编译器才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中(String类的intern()方法)
常量池表是Class文件的一部分
这里的常量池可以比喻为Class文件里的资源仓库,可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。具体查看在Class文件分支中的讲解
Class文件常量池不具备动态性
在加载类和接口到虚拟机后,就会常见对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护了一个常量池。
池中的数据像数据项一样是通过索引访问的
池中的数据像数据项一样是通过索引访问的
运行时常量池中包含多种不同常量
包括编译器就已明确的数值字面量
也包括到运行期解析后才能够获得的方法或字段引用
异常
当常量池无法在申请到内存是会抛出OOM异常
运行时常量池
Class 文件中的常量池
魔数
在 Class 文件结构中,最头的 4 个字节用于存储 Magic Number
用于确定一个文件是否能被 JVM 接受
次版本号与主版本号
再接着 4 个字节用于存储版本号
前 2 个字节存储次版本号,后 2 个存储主版本号
常量池计数器与常量池
再接着是用于存放常量的常量池
常量池计数器
由于常量的数量是不固定的
常量池的入口放置一个 u2 类型的数据
存储常量池容量计数值
(constant_pool_count)
定义
Class 文件中的资源仓库
它是 Class 文件结构中与其他项目关联最多的数据类型
它是占用Class文件空间最多的数据项目之一
它还是 Class 文件中,第一个出现的表类型数据项目。
表结构示意图
常量池表结构示意图
cp_info类型
cp_info 又可细分为 14 种结构类型。下表中第二列所说的标志,是指每一种数据类型的标记值,此处做简单了解即可。
cp_info 又可细分为 14 种结构类型
方法区包含了一个特殊的区域 “运行时常量池”
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
(Runtime Constant Pool)是方法区的一部分。
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table)
用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
存储信息
符号引用
符号引用包含的常量
类符号引用
方法符号引用
字段符号引用
概念解释
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
字面量
文本字符串
String a = "abc",这个abc就是字面量
八种基本类型
int a = 1; 这个1就是字面量
声明为final的常量
常量池中存储的数据
常量池主要用于存放两大类常量
字面量(Literal)和符号引用(Synbolic References)。
字面量
文本字符串
例如String a = "aa"。其中"aa"就是字面量。
文本字符
被final修饰的变量
类似与平常说的常量
声明为 final 的常量值
基础数据类型的值
符号引用量
类和接口的全限定名
例如对于String这个类,它的全限定名就是java/lang/String。
字段的名称和描述符
所谓字段就是类或者接口中声明的变量
包括类级别变量和实例级的变量。
方法的名称和描述符
所谓描述符就相当于方法的参数类型+返回值类型。
运行时常量池
其实两者关系非常容易理解
Class 文件中的常量池
运行时常量池
Class 文件中的常量池
【编译期】用于存放,编译期生成的各种字面量和符号引用
运行时常量池
【运行时】字面量和符号引用 在类加载后,进入方法区的运行时常量池中存放。
【重要特征】具备动态性
Java并不要求常量一定只有编译期才能产生
并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池
运行期间,也可能将新的常量放入池中
比如:String 类的 intern() 方法。
常量池的分类
Class文件常量池
在Class文件中,除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),
用于存放编译期生成的各种字面量和符号引用。
字面量
符号引用
运行时常量池
类加载器会加载对应的Class文件,Class文件常量池中的数据,会在类加载后进入方法区中的运行时常量池。
运行时常量池是全局共享的,多个类共用一个运行时常量池。
运行时常量池存在于方法区中。
字符串常量池
存放字符串
Class文件常量池中的文本字符串,会在类加载时,进入字符串常量池。
Class文件常量池中的字面量,会在类加载后,进入运行时常量池,其中字面量中也包括文本字符串,
字符串常量池。存在于运行时常量池中,也就存在于方法区中。
字符串常量池和运行时常量池是什么关系呢?
但是到了JDK1.7时,字符串常量池被移出了方法区,转移到了堆里了。
注意:字符串常量池中,存放的并不是字符串本身,而是字符串对象的引用。
程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。
常量池的优势
【性能提升,对象共享】
为避免频繁创建和销毁对象,而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段,就把所有的字符串文字放到一个常量池中。
【节省内存空间】
常量池中所有相同的字符串常量被合并,只占用一个空间。
【节省运行时间】
比较字符串时,== 比 equals () 快。
对于两个引用变量,只用 == 判断引用是否相等,也就可以判断实际值是否相等。
总结
编译期,使用 Class 文件中的常量池
运行期,使用运行时常量池。
实现方式(HotSpot JVM)
JDK8以前(永久代)
方法区的实现方式为"永久代",所以在此之前好多人讲方法区就曾为“永久代”,实际上两者并不等价
方法区不等于永久代
即常说的永久代(Permanent Generation),
方法区/永久代(线程共享)
HotSpot 虚拟机开发者愿把方法区称为 “永久代”(Permanent Generation)
why
本质上两者并不等价(方法区 != 永久代)
因为 HotSpot 虚拟机的设计团队
选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
对于其他虚拟机来说,是不存在永久代的概念的
BEA JRockit
IBM J9
永久代(方法区/永久代(线程共享))
指内存的永久保存区域
主要存放Class和Meta(元数据)的信息
Class在被加载时,被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。
所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
JAVA8与“元数据区”(元空间)
JDK8开始(元空间)
彻底废弃了“永久代的概念”,改用与“JRockit”,“J9”一样在本地内存中实现的“元空间”来代替
在JDK7时,HotSpot,把原来放在“永久代”的“字符串常量池”、“静态变量”等移至Java堆中
Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
大小设置
使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定方法区的大小
默认值(依赖于平台)
windows
-XX:MetaspaceSize是21M,
-XXMaxMetaspaceSize的值-1.即没有限制
-XXMaxMetaspaceSize的值-1.即没有限制
对于一个64位的服务器端JVM来说其默认的-XX:MetaspaceSize值就是初始的高水位线,一旦触及这高个水位线,
Full GC将会被触发并卸载没用的类,
然后这个高水位线将会重置。新高水位线的值取决于GC后释放了多少元空间
Full GC将会被触发并卸载没用的类,
然后这个高水位线将会重置。新高水位线的值取决于GC后释放了多少元空间
高水位线会引发Full GC,为了避免过多的发生Full GC,建议将-XX:MeataspaceSize设置位一个相对较高的值
元空间的本质和永久代类似,最大的区别
元空间并不在虚拟机中,而是使用本地内存。
默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中
这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
HotSpot VM把GC分代收集扩展至方法区
即使用Java堆的永久代来实现方法区,
这样,HotSpot的垃圾收集器,就可以像管理Java堆一样,管理这部分内存, 而不必为方法区开发专门的内存管理器
(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)
方法区内存变更
子主题
方法区的实现,虚拟机规范中并未明确规定
目前有 2 种比较主流的方法区的实现方式:
HotSpot 虚拟机 1.8之前
在 JDK1.6 及之前版本,HotSpot 使用 “永久代(permanent generation)” 的概念作为实现
【即将 GC 分代收集扩展至方法区。】
在 JDK1.7,HotSpot 逐渐改变方法区的实现方式
如 1.7 版本,移除了方法区中的字符串常量池,但为发生本质的变化。
优点
比较偷懒,可以不必为方法区编写专门的内存管理
缺点
容易碰到内存溢出的问题
(因为永久代有 - XX:MaxPermSize 的上限)。
HotSpot 虚拟机 1.8之后
1.8 版本中【移除了方法区并使用 metaspace(元数据空间)作为替代实现】
metaspace 占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。
但这不意味着不对方法区进行限制
如果方法区无限膨胀,最终会导致系统崩溃。
用于存放被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等
异常
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机就会抛出OOM异常
堆区域/堆内存
前言
掌握堆内存空间结构图,从总体层面认识堆内存【重点】
了解 JVM 堆空间的基本概念,为本节的基础知识点【基础】
了解堆内存的分代概念,年轻代,eden区,from/to,幸存者及老年代【重点】
后续对垃圾回收讲解时,大部分的回收都是发生在堆内存中,掌握分代概念是学习垃圾回收机制的必要前提。【基础】
什么是堆内存、简介、基本概念
Heap Area
数量
只有一个
运行时数据区中非常重要的结构
堆(Heap-线程共享)- 运行时数据区
堆内存是运行时数据区中非常重要的结构
对于 JVM 来说,堆内存占据着十分重要的且不可替代的位置。
Java堆是虚拟机锁管理的内存中最大的一块
物理层面(硬件层面)
JVM启动时,从操作系统获取的一片内存空间
当 Java 程序开始运行时,JVM 会从操作系统获取一些内存。
JVM 使用这些内存,这些内存的一部分就是堆内存。
堆内存的大小在虚拟机被启动时就确定了
Java层面(开发层面)
堆内存通常在存储地址的底层,向上排列。
当一个对象通过 new 关键字或通过其他方式创建后,对象从堆中获得内存。
当对象不再使用了,被当做垃圾回收掉后,这些内存又重新回到堆内存中。
《Java虚拟机规范》规定,堆可以处于内存不连续的内存空间中,但在逻辑上应被视为连续的
作用:存放对象实例
唯一目的
Java中“几乎”所有对象实例都在这里分配内存
用于存放实例对象本身/存放对象实例
实例对象会存放于堆内存中。
创建完成的对象会放置到堆内存中。
存储内容
数组
对象
对象对应的实例变量
被所有线程共享的一块内存区域(线程不安全)
是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中
是被所有线程共享的一块内存区域,还能够划分线程私有的缓冲区(TLAB)
由于堆区域共享多个线程的内存,所存储的数据不是线程安全的
为共享内存区域,多线程环境下共享这两块内存区域。
堆内存是一块共享内存区域
堆内存的分代
概念
从上文堆内存的结构图中,我们看到了比较多的JVM堆内存中的专有名词,比如:年轻代,老年代。
那么对于堆内存来说,分代是什么意思呢?为什么要进行分代呢?
分代
将堆内存,从概念层面进行模块划分,总体分为两大部分,年轻代和老年代。
从物理层面,将堆内存进行内存容量划分,一部分分给年轻代,一部分分给老年代。
意义
【易于堆内存分类管理】
类似于Windows 操作系统,将物理磁盘划出一部分存储空间划分为 C, D, E 等磁盘
对于堆空间的分代也是如此
新创建对象会进入年轻代的生成区
生命周期未结束的且可达的对象,在经历多次垃圾回收之后,会存放入老年代
【易于垃圾回收】
将对象根据存活概率进行分类
对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。
针对分类进行不同的垃圾回收算法,对算法扬长避短。
Java堆内存,从GC的角度细分为、堆的分区、分代回收
说明
JVM 内存 与 堆内存中不同内存空间模块
不是所有虚拟机固有的
堆内存(由于现代VM采用分代收集算法,从GC角度细分)
Java堆,从GC的角度细分
非堆内存
堆内存结构详解
老年代
老年代(OldGen、OldGeneration)
老年代:新生代=2:1
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时,也会提前触发一次MajorGC,进行垃圾回收腾出空间。
MajorGC的过程
采用标记清除算法
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。
MajorGC的耗时比较长,因为要扫描再回收。
MajorGC会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来,方便下次直接分配。
当老年代也满了装不下时,就会抛出OOM异常。
新生代/年轻代(YoungGen、YoungGeneration)
用来存放新生的对象。一般占据堆的1/3空间。
新生代分为三个区
生成区/伊甸园区(Eden)
概念
伊甸园区(Eden)
Java新对象的出生地
几乎所有Java对象都是在Eden区中被New出来的
(如果新创建的对象占用内存很大,则直接分配到老年代)
当Eden区内存不够时,就会触发MinorGC,对新生代区进行一次垃圾回收。
绝大部分的Java对象的销毁都在新生代
占大容量
8
默认比例是 8:1:1
幸存者区(Survivor)
S0区(FromSpace)
占小容量
1
Survivor(From) /Survivor(from)区
ServivorFrom区
幸存者0区(Survivor 0)
上一次GC的幸存者,作为这一次GC的被扫描者。
设置Survivor是为了减少送到老年代的对象
S1区 (ToSpace)
占小容量
1
Survivor(to)区 /Survivor(To)
ServivorTo区
幸存者1区(Survivor 1)
保留了一次MinorGC过程中的幸存者。
设置两个Survivor区是为了解决碎片化的问题
由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
MinorGC的过程
(复制->清空->互换)
MinorGC采用复制算法
1:eden、servicorFrom 复制到ServicorTo,年龄+1
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域,同时把这些对象的年龄+1;
(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区)
(如果ServicorTo不够位置了就放到老年区)
2:清空eden、servicorFrom
然后,清空Eden和ServicorFrom中的对象;
3:ServicorTo和ServicorFrom互换
最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
堆内存的结构图
图解堆内存
堆内存的结构图
堆内存中的不同的代
JAVA 堆区域与垃圾收集GC操作紧密关联
垃圾收集器进行垃圾收集GC操作的最重要的内存区域
GC 垃圾回收器,绝大多数的垃圾回收都发生在堆内存中,
堆内存每个模块之间的关系
堆内存存放的是对象,垃圾收集器就是收集这些对象,然后根据 GC 算法回收;
新生成的对象首先放到年轻代 Eden 区
当 Eden 空间满了,触发执行 Minor GC,存活下来的对象移动到Survivor0 区
当 Survivor0空间满了,触发执行 Minor GC,Survivor0 区存活对象移动到 Suvivor1 区
这样保证了一段时间内总有一个 survivor 区为空。
经过多次 Minor GC 仍然存活的对象移动到老年代
老年代存储长期存活的对象,GC 期间会停止所有线程等待 GC 完成
所以对响应要求高的应用尽量减少发生 Major GC,避免响应超时。
JVM 中堆的对象转移与年龄判断/内存分配策略
前言
理解并掌握对象优先在 Eden 区分配的实验案例【重点】
理解并掌握对象直接在老年代分配的触发条件,理解什么是大对象【重点】
掌握堆内存对象转移的完整流程图及触发机制【核心】
理解并掌握年龄判断的定义,作用及默认年龄值【重点】
理解并掌握对象直接在老年代分配的触发条件,理解什么是大对象【重点】
掌握堆内存对象转移的完整流程图及触发机制【核心】
理解并掌握年龄判断的定义,作用及默认年龄值【重点】
其核心是围绕堆内存对象转移的完整流程图及触发机制
对象优先在Eden 区分配
Eden 区属于年轻代(YoungGen)。
新创建的对象 优先存储于年轻代中的Eden区的
在创建新的对象时,大多数情况下,对象优先在 Eden 区中分配。
当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
“优先” 意味着首先考虑
【特殊情况】新创建的对象还是有可能不在Eden区分配的。
【特殊情况】对于新创建的大对象,会直接进入老年代中
大对象直接进入老年代
【优先】新创建的对象是优先存放入 Eden 区的
【特殊情况】对于新创建的大对象,会直接进入老年代中
尽量避免程序中出现过多的大对象
什么是大对象
大对象的标准是可以由开发者定义的
JVM 参数中,能够通过参数设置大对象的标准
-XX:PretenureSizeThreshold
这个参数只对 Serial 和 ParNew 两款新生代收集器有效。
如果不能够设置JVM参数,那什么是大对象呢?
Eden 区容量不够存放的对象就是所谓的大对象。
长期存活的对象分配到老年代
动态对象年龄判断
如果幸存者区(Survivor)中相同年龄的所有对象大小的总和大于Survivor空间的一半,
年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
对象转移流程
图解
对象转移流程图解
Eden 区优先存放新建的独享
新生成的非大对象首先放到年轻代 Eden 区,当 Eden 空间满了,触发 Minor GC,存活下来的对象移动到 Survivor0 区
Survivor0 区满后,触发执行 Minor GC,Survivor0 区存活对象移动到 Suvivor1 区
,这样保证了一段时间内总有一个 survivor 区为空。
经过多次 Minor GC 仍然存活的对象移动到老年代。
如果新生成的是大对象,会直接将该对象存放入老年代。
新建大对象不会经过Eden区,直接进入老年代
老年代存储长期存活的对象,GC 期间会停止所有线程等待 GC 完成,所以对响应要求高的应用,尽量减少发生 Major GC,避免响应超时。
对象年龄判断
作用
JVM 通过判断对象的具体年龄,来判别是否该对象应存入老年代
JVM通过对年龄的判断,来完成从对象从年轻代到老年代的转移。
对象年龄(Age)计数器
HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,
那内存回收时,就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。
为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
年龄增加
对象通常在 Eden 区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被Survivor容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在Survivor区中每熬过一次 Minor GC,年龄就增加 1 岁。
年龄默认阈值
当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数设置。
-XX:MaxTenuringThreshold
空间分配担保
谁进行空间担保?
空间担保指的是老年代进行空间分配担保
什么是空间分配担保?
1. 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
2. 如果大于,则此次Minor GC是安全的
3. 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true(JDK 7该指令虽然还在,但是不管如何设置都为true一样的效果)
那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于
则进行一次Full GC
如果HandlePromotionFalure=false
则进行一次Full GC
为什么要进行空间担保?
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
异常
OutOfMemoryError
如果在Java堆中没有内存完成实力分配,并且堆也无法再扩展时,Java虚拟机将会抛出OOM异常
栈区
前言
了解栈的基本概念及特点【基础】
理解并掌握栈帧的概念以及栈帧的数据结构,【重点】
对栈帧结构中的局部变量表,操作数栈,动态链接以及返回地址做详细的讲解【重点】
特点
对于每个线程,将创建单独的运行时栈。
开发者主要关注的是栈内存
主要关注的栈内存,就是虚拟机栈中局部变量表部分。
栈内存的消耗,是因为每个方法执行的同时,会创建一个栈帧
占用空间最大的部分就是栈帧的局部变量表部分。
存储内容
局部变量
所有局部变量将在栈内存中创建。
线程安全
栈区域是线程安全的,因为它不共享资源;
线程私有部分,私有数据对其他线程不可见。
Java 虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭);
异常
如果线程请求的栈深度 > 虚拟机所允许的深度,将抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常;
两个栈区(Stack Area)
虚拟机栈
虚拟机栈是什么?
Java 虚拟机栈,
Java虚拟机栈(栈)
虚拟机栈(线程私有)
java方法执行的内存模型
描述的是 Java 方法执行的内存模型
描述的是Java方法执行的线程内存模型:
虚拟机栈(JVM执行Java方法)
Java虚拟机栈是线程私有的
栈帧
栈帧的定义
Stack Frame
用于支持虚拟机,进行方法调用和方法执行的数据结构。
用来存储数据和部分过程结果的数据结构
它是虚拟机运行时数据区中的 java 虚拟机栈的栈元素。
栈帧的作用
用于存储以下信息
局部变量表
操作数栈(表达式栈)
动态链接
方法返回地址(方法出口)
栈帧( Frame)示意图
栈帧的组成结构示意图(栈帧存储的内容)
栈帧
栈帧与方法之间的关系
每个方法执行都会创建一个栈帧
对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。
每个方法执行的同时,会创建一个栈帧。
每个方法在执行的同时都会创建一个栈帧
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧。
当有一个方法需要被执行是就会入栈操作,执行结束则出栈
栈帧随着方法调用而创建,随着方法结束而销毁
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
每个方法,从开始调用到结束,都对应一个栈帧在虚拟机栈中入栈到出栈的过程。
无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
图解栈帧与方法之间的关系
图解栈帧与方法之间的关系 : 每个方法执行都会创建一个栈帧
栈帧弹出的方式
正常的结束return;
抛出异常(未捕获)
栈帧的内部存储内容
局部变量表
方法的局部变量表(栈帧 - 局部变量表)
基本概念
在栈帧中,局部变量表占用了大部分的空间
所有局部变量将在栈内存中创建。
每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。
特点
局部变量表的容量以变量槽(Variable Slot)为最小单位;
在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程;
局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体
如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间。
局部变量表
局部变量就是定义在每个方法中的变量。定义在类中的叫类变量(static),或者属性(对象属性、实例变量)
所需容量是在编译器就确定下来了的
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
存放的信息
基本数据类型
对象的引用(并不等同于对象本身)
returnAddress类型(指向了一条字节码指令的地址)
方法的形参
基本存储单位
slot(槽)
32位(4个字节)占一个slot(包括returnAddress类型)
byte,short、char、引用类型在存储前被转化为int类型,boolean 0等于false,非零true
64(8个字节)占用两个slot(double,long)
也可以看作是一个数字数组
在非静态方法(静态方法中不允许调用this),或构造方法中,以“this.xx”这样的形式调用的方式,会将this指向存放在局部变量表的index为0的slot处
slot槽位十可以重复利用的,如果方法中的局部变量过期了(代码运行超过了作用域),则该位置将会被重复利用
操作数栈(表达式栈)
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间
当一个方法刚开始执行时,这个方法的操作数栈是空的,但是操作数栈的大小在编译的时候就确定了
保存着Java 虚拟机执行过程中的数据
存放
32位数据类型占的栈容量为1
64位数据类型所占的栈容量为2
操作过程
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配
例如:iadd指令,只能用于整型数的加法,且最接近栈顶的两个元素的数据类型必须为int类型
操作数栈(栈帧 - 操作数栈)
基本概念
操作数栈,也是栈帧中非常重要的结构
操作数栈不需要占用很大的空间
特点
操作数栈,是一个后入先出(Last In First Out)栈
方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程
操作数栈的每一个元素可以是任意的 Java 数据类型
32 位数据类型所占的栈容量为 1
64 位数据类型所占的栈容量为 2
当一个方法刚执行时,这个方法的操作数栈是空的
在方法执行的过程中,通过一些字节码指令,从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中。
不同的栈帧作为不同方法的虚拟机栈元素,是完全独立的。
但是,在大多数虚拟机的实现里都会有进行一些优化处理,令下面的栈帧部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样不仅节约了空间,更重要的是在方法调用时就可以直接公用一部分数据,无需额外的参数复制传递。
但是,在大多数虚拟机的实现里都会有进行一些优化处理,令下面的栈帧部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样不仅节约了空间,更重要的是在方法调用时就可以直接公用一部分数据,无需额外的参数复制传递。
动态链接
动态链接 (Dynamic Linking)、
动态链接(栈帧 - 动态链接)
动态链接(指向运行时常量池的方法引用)
每个栈帧都包含一个指向“运行时常量池”中栈帧所属方法的引用,
持有了这个引用就可以支持方法在调用过程中的动态链接
持有了这个引用就可以支持方法在调用过程中的动态链接
当前Class文件常量池符号引用,这些引用指向方法区中的“运行时常量池”中的方法引用
这些符号引用一部分会在类加载阶段或第一次使用的时候被转化为直接引用,这种转化被称为“静态解析”。
另外一部分将在每一次运行期间转化为直接引用,这部分就称为“动态连接”
另外一部分将在每一次运行期间转化为直接引用,这部分就称为“动态连接”
符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
基本概念
每个栈帧都包含一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所属方法属性的引用
持有这个引用是为了支持方法调用过程中的动态链接。
特点
在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
符号引用(1. 类的全限定名,2. 字段名和属性,3. 方法名和属性)
静态解析
这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用
(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。
动态解析
另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。
方法返回地址(方法出口)
方法被调用的位置
方法放反回地址(方法正常退出或异常退出的定义)
方法退出的过程实际上就等同于把当前栈帧出栈
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
(通俗点讲,就是回复调用该方法的之前方法)
(通俗点讲,就是回复调用该方法的之前方法)
PC寄存器就是存放着下一条需要执行的指令,该指令需要执行引擎去执行
方法退出可能包含的操作
恢复上层方法的局部变量表和操作数栈
把返回值(如果有的话)压入调用者栈帧的操作数栈中
调整PC计数器的值以指向方法调用指令后面的一条指令
一个方法有两种退出方式(出栈)
执行引擎遇到任意一个方法返回的字节码指令
在执行方法的过程中遇到了异常,并且这个异常没有在方法体中得到妥善的处理,
也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,该退出方式称为“异常调用完成”
也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,该退出方式称为“异常调用完成”
当你的异常被catch捕获就属于被合理的异常处理了
如果是抛出或者Java虚拟机内部发生了异常都属于未被异常处理
返回字节码分类
return
无返回值类型(void)
ireturn
当返回值是boolean、short、int、byte、char
lreturn
当放回值是long
freturn
当放回值是float
dreturn
当返回值是double
areturn
当返回值是引用类型
方法返回地址(栈帧 - 返回地址)
基本概念
代表方法执行结束
当一个方法开始执行后,只有两种方式可以退出这个方法。
方法退出的过程,实际上就等同于把当前栈帧出栈
方法执行结束有两种方式(退出方法、退出方法)
正常完成出口
Normal Method Invocation Completion
执行引擎遇到任意一个方法返回的字节码指令(例如:return)
这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者)
是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定
异常完成出口
Abrupt Method Invocation Completion
在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理
无论是 Java 虚拟机内部产生的异常,还是代码中使用 throw 字节码指令产生的异常
只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出
该退出方法,是不会给它的上层调用者产生任何返回值的。
退出时可能执行的操作
恢复上层方法的局部变量表和操作数栈
把返回值压入调用者栈帧的操作数栈中(如有)
调整程序计数器的值,以指向方法调用指令后面的一条指令
其他附加信息
异常分派( Dispatch Exception)
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息倒栈帧中,
例如与调试、性能收集相关的信息,这部分信息完全取决于虚拟机实现
初始化大小
编译程序代码时,栈帧中需要多大的局部变量表内存,多深的操作数栈都已完全确定了。
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
结构
在一个线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
设置栈的大小
可以使用 -Xss Size 来设置栈的大小
例如 -Xss256k
例如 -Xss256k
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
不同的线程中所包含的栈帧时不能互相引用的
方法调用
方法调用阶段唯一任务就是确定被调用方法的版本(即调用哪一个方法)
所有方法的调用的目标方法在Class文件里面都是一个符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,但是还有一部分类无法在类加载阶段被解析,因为这些方法被调用的版本不可确定,或者在运行阶段被调用时可能是可变的(例如:多态)。
符合“编译期可知,运行期不变”这个要求的方法
静态方法
私有方法
对应不同类型的方法,字节码指令
invokestatic
用于调用静态方法
invokespecial
用于调用实例构造器<init>()方法、私有方法和父类中的方法
invokevirtual
调用所有虚方法
invokeinterface
用于调用接口方法,会在运行时再确定一个实现该接口的对象
invokedynamic
先在运用时动态解析出调用点限定符所引用方法,然后再执行该方法
方法分类
虚方法
在被调用时,无法在类加载阶段转化为直接引用,也就是在类加载的解析阶段,无法被确定版本的方法
非虚方法
在被调用时,会在类加载的时候就可以把符号引用解析为该方法的直接引用。
一个概念
静态类型(外观类型)
静态类型是在编译器可知的
实际类型(运行时类型)
实际类型变化的结果在运行期才可确定
例如:Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
Human human = new Man();
Human human = new Woman();
Human human = new Woman();
这里的Man和Woman为Human的子类
Humen human就是静态类型
new Man();
new Woman();
为实际类型
new Woman();
为实际类型
分派
静态分派
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派
运用
重载
选择那哪一个重载版本是根据,静态类型来决定的,而不是实际类型
所以,Java实际上实在编译期间就已经确定了你所需要调用的方法重载版本
动态分派
调用方法的版本实在运行时才可以确定的
运用
重写
如果一个接口有两个实现类,用该接口调用该方法,则实际的方法调用版本则根据实际参数决定
需要用到指令为invokevirtual指令
invokevirtual解析过程
1.找到操作数栈顶的第一个元素所指向的对象实际类型,记作C
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,
如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回IllegalAccessError异常
如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回IllegalAccessError异常
3. 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验重过程
4.如果始终没找到何使的方法,则抛出AbstractMethodError异常
按照宗量数分
方法的接收者和方法的参数统称为方法的宗量
方法的接收者和方法的参数统称为方法的宗量
多分派
就如Java来说,在编译阶段,选择目标方法的时候需要依据两个点
一、是方法的调用者是谁,
二、根据方法的参数选择重载的对应的方法
一、是方法的调用者是谁,
二、根据方法的参数选择重载的对应的方法
单分派
在运行的阶段,由于在编译时期已经确定了重载对应的方法和调用者
这一阶段只需要确定,根据实际参数,调用方法的版本即可
这一阶段只需要确定,根据实际参数,调用方法的版本即可
异常
StackOverflowError
StackOverflowError
线程请求的栈深度大于虚拟机所允许的深度
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
OutOfMemoryError
OutOfMemoryError
JVM动态扩展时无法申请到足够的内存时
如果Java虚拟机栈容量可动态扩展,当栈扩张时无法申请到足够内存会抛出OutOfMemoryError异常
本地方法栈
是什么?
Native Method stacks
本地方法栈、本地方法区(线程私有)
JVM执行本地方法
存储
本地方法信息
线程安全
对于每个线程,将创建一个单独的本地方法栈。
线程私有部分,私有数据对其他线程不可见。
作用
管理本地方法的调用
为使用到的Native方法服务
需要用到时,将本地方法栈帧压入本地方法栈,然后通过动态链接,去调用本地方法库里面的方法
和虚拟机栈类似和区别
本地方法栈和虚拟机栈基本相同。
本地方法区和Java Stack作用类似
和虚拟机栈类似
当调用本地方法时,就不会收到Java虚拟机的限制,和Java虚拟机拥有同样的权限
区别如下
虚拟机栈为执行Java方法服务
本地方法栈则为Native方法服务
没有明确要求本地方法栈所需要用什么语言
《Java虚拟机规范中》并没有明确要求
本地方法栈所需要用什么语言,具体的实现,以及数据结构等,甚至不要求强制实现本地方法栈
如果一个VM实现使用C-linkage模型,来支持Native调用, 那么该栈将会是一个C栈
HotSpot把本地方法栈和虚拟机栈合二为一
HotSpot VM直接就把本地方法栈和虚拟机栈合二为一
在HotSpot虚拟机中,将本地方法栈和虚拟机栈合二为一
PC寄存器、程序计数器
前言
理解并掌握寄存器的概念及作用【重点】
简介、基本概念
程序计数器(线程私有)
PC寄存器
( PC register 、PC Registers 、Program Counter )
程序计数器的特点
【速度快】运行速度最快的存储区域
【生命周期】生命周期与线程的生命周期保持一致
【内存空间小】
一块较小、很小、非常小的内存空间
这块内存区域很小,几乎可以忽略不记
【线程私有/线程安全】
【线程私有】在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的
每条线程都要有一个独立的程序计数器
程序计数器是每个线程私有的
线程私有部分,私有数据对其他线程不可见。
这类内存也称为“线程私有”的内存
【基础功能的依赖】
它是程序控制流的指示器
基础功能,都需要依赖它来完成
控制着计算机指令的基础功能
基础功能包括分支、循环、跳转、异常处理、线程恢复等
【每个线程都有一个PC寄存器】
每个线程都有一个单独的PC寄存器
每个线程启动时,都会创建一个 PC寄存器
每一个线程都有它自己的 PC 寄存器,也是该线程启动时创建的。
每个线程都有一个程序计数器
【无规定OOM】没有规定任何 OOM 情况的区域
除了程序计数器外,其他区域都是可能发生OOM的区域
方法区(Method Area)
方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
虚拟机栈(VM Stack)
如果线程请求的栈深度大于虚拟机栈所允许的深度,将会出现StackOverflowError异常;
如果虚拟机动态扩展无法申请到足够的内存,将出现OutOfMemoryError异常
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈一样
堆(Heap)
Java堆可以处于物理上不连续,逻辑上连续,就像磁盘空间一样,
如果堆中没有内存 完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError。
此内存区域是唯一一个不会出现OOM情况的区域。
它是唯一一个在 JVM规范中,没有规定任何 OOM 情况的区域
此内存区域是唯一一个在JVM规范中没规定任何OOM情况的区域
唯一一个在JVM中没有规定任何OOM Error情况的区域。
OutOfMemoryError(OOM)
JVM规范《Java虚拟机规范》
图解PC寄存器、程序计数器
图解PC寄存器、程序计数器
存储内容
(1)当前执行指令的地址
【当前方法】
其实就是一个指针,指向方法区中的方法字节码
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。
PC 寄存器里,保存有当前正在执行的 JVM 指令的地址。
程序计数器会存储当前线程正在执行的 java 方法的 JVM 指令地址
(2)即将要执行的指令代码
指向下一条指令的地址
由执行引擎读取下一条指令
一旦执行指令,它会将被下一条指令更新
每个线程都有一个程序计数器,分别记录着自己线程的下一跳代码(指令)的地址,
PC寄存器的内容,总是指向下一条将被执行指令的地址
它保存着下一条将要执行的指令地址
它寄存着字节码中下一句要运行的代码的地址
当CPU在线程调度,切换时,保证每次的切换都能够切换回上次运行的地方。
【工作原理】字节码解释器工作时,就是通过改变这个计数器的值,来选取下一个条需要执行的字节码指令
调用的是一个Java方法,指明当前线程执行的字节码的行号指示器
它可以看作是当前线程所执行的字节码的行号指示器。
当前线程所执行的字节码的行号指示器
如果线程正在执行的是一个Java方法,则指明当前线程执行的代字节码行数
正在执行java方法,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)
如果应用程序执行的是Java方法,那么这个计数器记录的就是虚拟机字节码指令的地址
这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量
调用的是本地方法(Native),则该计数器记录的值为空
如果调用的是本地方法(Native),则该计数器记录的值为空(Undefined)
如果正在执行的是Native方法,这个计数器值则为空(Undefined)
如果是在执行 Native方法,则是未指定值(Undefined)
如果执行的是一个 Native 方法,那这个计数器是空的。
如果还是Native方法,则为空。
直接内存
概念
直接内存(Direct Memory)
不属于运行时数据区,也不属于内存区域
并不是JVM运行时数据区域的一部分,但是会被频繁使用
直接内存并不是JVM运行时数据区的一部分, 但也会被频繁的使用:
直接内存不是虚拟机运行时数据区的一部分
也不是《Java虚拟机规范》中定义的内存区域
Java的NIO库与直接内存之间的关系
在JDK 1.4引入的
详见: Java I/O 扩展
NIO提供了基于Channel与Buffer的IO方式
NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式
Java JDK1.4后引入的NIO库/类,允许Java程序使用直接内存,用于数据缓冲区
直接内存大小设置与限制
直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数一致
受本机总内存和处理器寻址空间的限制
在Java堆外的,直接向系统申请的内存区间
即使直接内存在Java堆外,不受虚拟机内存大小限制,但是系统内存也是有限的,因此还是会报OOM异常
使用Native函数库直接分配堆外内存
它可以使用Native函数库直接分配堆外内存
使用Native函数库直接分配堆外内存
可以使用native函数直接分配堆外内存
使用DirectByteBuffer对象作为这块内存的引用进行操作
直接通过一个的DirectByteBuffer对象作为这块内存的引用进行操作
DirectByteBuffer对象存储在Java堆里面
优缺点
优点
读写性能高
避免了在Java 堆和Native 堆中来回复制数据,能够提高效率
在一些场景中可以显著提高性能。
访问直接内存的速度会优于Java堆
缺点
分配回收成本较高
不受JVM内存回收管理
C++与Java的区别之一
Java中会把对象都放在堆上, 需要new操作符来创建对象
C++与Java的区别之一
对于刚接触Java的C++程序员而言, 理解栈和堆的关系可能很不习惯。
在C++中, 可以使用new操作符在堆上创建对象, 或者使用自动分配在栈上创建对象
下面的C++语句是合法的, 但是Java编译器却拒绝这么写代码, 会出现syntaxerror编译错误。
Java编译器拒绝这么写代码, 会出现syntaxerror编译错误。
Java和C不一样, Java中会把对象都放在堆上, 需要new操作符来创建对象。
本地变量存储在栈中, 它们持有一个指向堆中对象的引用(指针)
下面是一个Java方法, 该方法具有一个Integer变量, 该变量从String解析值
该方法具有一个Integer变量, 该变量从String解析值
这段代码我们使用堆栈分配图可以看一下它们的关系
堆栈分配图
首先先来看一下foo() 方法, 这一行代码分配了一个新的Integer对象, JVM尝试在堆空间中开辟一块内存空间。
如果允许分配的话, 就会调用Integer的构造方法把String字符串转换为Integer对象。JVM将指向该对象的指针存储在变量bz中.
上面这种情况是我们乐意看到的情况,毕竟我们不想在编写代码的时候遇到阻碍,但是这种情况是不可能出现的,
当堆空间无法为bar和baz开辟内存空间时, 就会出现OutOfMemoryError, 然后就会调用垃圾收集器来尝试腾出内存空间
这中间涉及到一个问题, 垃圾收集器会回收哪些对象?
volatile
保证不同线程对共享变量操作时的可见性
禁止指令重排序
分代收集理论
按照回收区域分类
部分收集
新生代收集(Minor GC/Young GC)
只是对新生代(Eden,S0/S1)的垃圾收集
触发机制
当新生代空间不足时,就会触发Minor GC,这里的新生代满指Eden满,Survivor满不会引发GC。
(每次Minor GC会清理年轻代内存)
(每次Minor GC会清理年轻代内存)
大多数Java对象的生命周期都比较短,所以 Minor GC会非常频繁,一般回收速度也比较快。
Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代收集(Major GC/Old GC)
只是对老年代的垃圾收集
目前,只有CMS GC会有单独收集老年代的行为
注意:很多时候Major GC会和Full GC的叫法会混淆使用,Major GC具体指代什么需要具体分辨是老年代回收还是整堆回收
触发机制
指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或 “Full GC”发生了(上面讲了Major GC 与 Full GC 很多地方叫法混淆)
出现了Major GC,经常会伴随至少一次的Minor GC
(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
也就是老年代空间不足时,会先尝试触发Minor GC。如果空间还不足,则触发Major GC
如果Major GC内存还不足,就报OOM
Mojor GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
混合收集(Mixed GC)
收集整个新生代以及老年代的垃圾收集
目前,只有G1 GC会有这种行为
整体收集(Full GC)
收集整个Java堆和方法区的垃圾收集
垃圾回收机制与算法
垃圾回收器的基本概念
前言
了解垃圾回收器在 JVM 整体架构中的位置【基础】
了解并掌握垃圾回收器的定义以及意义【基础】
了解并掌握垃圾回收器的定义以及意义【基础】
什么是垃圾回收器?
JVM 为 Java 提供了垃圾回收机制
其实是一种偏自动的内存管理机制
垃圾回收器会自动追踪所有正在使用的对象,并将其余未被使用的对象标记为垃圾
不需要开发者手动进行垃圾回收,JVM 自动进行垃圾回收,释放内容。
垃圾回收器的位置
JVM 的垃圾回收器位于执行引擎中
哪些内存需要回收?
哪些内存需要回收是垃圾回收机制第一个要考虑的问题
所谓“要回收的垃圾”,无非就是那些不可能再被任何途径所使用的对象。
无需再使用的对象,会被标记为垃圾,等待JVM回收此部分内存。
为什么进行垃圾回收?
识别并且丢弃应用不再使用的对象来释放和重用资源。
如果不进行垃圾回收,内存迟早都会被消耗空
因为我们在不断的分配内存空间而不进行回收。
除非内存无限大,可以任性的分配不回收,但是事实并非如此。
所以,垃圾回收是必须的。
Java中垃圾回收有什么目的?什么时候进行垃圾回收?
JVM 常见的垃圾回收算法
前言
标记-清除(Mark-Sweep)算法的原理及缺陷【重点】
复制(coping)算法的原理及缺陷【重点】
标记-整理(Mark-Compact)算法的原理及缺陷【重点】
分代收集理论的原理及思想【重点】
复制(coping)算法的原理及缺陷【重点】
标记-整理(Mark-Compact)算法的原理及缺陷【重点】
分代收集理论的原理及思想【重点】
安全点安全区
垃圾回收算法种类
分类A
标记-清除(Mark-Sweep)算法;
复制(coping)算法;
标记-整理(Mark-Compact)算法;
分代收集算法。
复制(coping)算法;
标记-整理(Mark-Compact)算法;
分代收集算法。
分类B
标记-清除(Mark-Sweep)算法;
复制(coping)算法;
标记-整理(Mark-Compact)算法。
复制(coping)算法;
标记-整理(Mark-Compact)算法。
标记阶段
(该阶段标记哪些对象需要被回收)
(该阶段标记哪些对象需要被回收)
如何判断对象已死/对象是否存活/如何确定垃圾
判断一个对象是否可回收的过程(两步)
1.找到GC Roots不可达的对象,如果没有重写finalize()或者调用过finalize(),则将该对象加入到F-Queue中
2.再次进行标记,如果此时对象还未与GC Roots建立引用关系,则被回收
不同的引用类型其实都是逻辑上的,而对于JVM来说,主要体现的是
不同对象的可达性状态
(reachable)
对垃圾收集的影响
(garbage collector)
可达性分析算法
该算法是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
当一个对象到GC Roots没有引用链相连(GC Roots到这个对象不可达)时,证明此对象不可用
GC Roots种类
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
GC Roots
当前虚拟机栈中局部变量表中的引用的对象
当前本地方法栈中局部变量表中的引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
可达性分析的四种引用类型/回收对象引用类型
前言
强引用的定义以及如何消除强引用【重点】
软引用的定义及使用场景【重点】
弱引用的定义及代码示例,验证任何情况下,只要发生 GC 就会回收弱引用对象【重点】
虚引用的定义以及作用【重点】
软引用的定义及使用场景【重点】
弱引用的定义及代码示例,验证任何情况下,只要发生 GC 就会回收弱引用对象【重点】
虚引用的定义以及作用【重点】
可达性分析的 GC Roots 均为引用对象
引用划分种类的意义
希望描述这样一类对象,当内存空间还足够时,则能保留在内存中;
如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。
强引用
定义
强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()这类的引用
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
当内存空间不足,Java 虚拟机宁愿抛出OOM错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
在强引用的定义中有这样一句话:“只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。”
在Java中最常见的就是强引用,
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
当一个对象被强引用变量引用时,它处于可达状态
只要强引用存在,垃圾收集器永远不会收集被引用的对象
垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止
它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。
因此,强引用是造成Java内存泄漏的主要原因之一。
代码示例
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object(); // 强引用
}
}
public static void main(String[] args) {
Object obj = new Object(); // 强引用
}
}
消除强引用示例代码
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object(); // 强引用
obj = null; //消除强引用
}
}
public static void main(String[] args) {
Object obj = new Object(); // 强引用
obj = null; //消除强引用
}
}
如果不使用强引用时,可以赋值 obj=null
显示的设置 obj 为 null,则 gc 认为该对象不存在引用,此时就可以回收此对象。
软引用
定义
软引用,用来描述一些还有用,但并非必需的对象。
对于软引用关联着的对象
如果内存充足,则垃圾回收器不会回收该对象
如果内存不够了,就会回收这些对象的内存。
软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用
如果软引用所引用的对象,被垃圾回收器回收,JVM就会把这个软引用加入到与之关联的引用队列中。
在 JDK 1.2 之后,提供了 SoftReference 类,来实现软引用。
用SoftReference类来实现
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它
对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收
在发生内存溢出之前,垃圾收集器会把这类对象进行第二次回收,如果这次回收还没有足够内存才会抛出内存溢出异常。
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
通常用在对内存敏感的程序中。
使用场景
Android 应用图片
软引用主要应用于内存敏感的高速缓存,在 Android 系统中经常使用到。
背景描述
一般情况下,Android 应用会用到大量的默认图片,这些图片很多地方会用到。
如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。
所以考虑将图片缓存起来,需要的时候直接从内存中读取。
但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OOM异常。
解决方案
这时,可考虑使用软引用技术来避免OOM问题发生。
软引用可以解决 OOM 的问题
每一个对象,通过软引用进行实例化,这个对象就以cache的形式保存起来,
当再次调用这个对象时,那么直接通过软引用中的 get() 方法,就可以得到对象中的资源数据,
这样就没必要再次进行读取了,直接从 cache 中就可以读取得到
当内存将要发生 OOM 时,GC 会迅速把所有的软引用清除,防止 OOM 发生。
弱引用
定义
描述非必需对象。
被弱引用关联的对象,只能生存到下一次垃圾回收之前
垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
Java 中的类 WeakReference 表示弱引用。
代码示例
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
结果验证
第二个输出结果是 null,这说明只要 JVM 进行垃圾回收,被弱引用关联的对象必定会被回收掉。
hello
null
null
用WeakReference类来实现
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。
ThreadLocal的key是弱引用
比软引用的生存期更短
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。
虚引用
定义
顾名思义,就是形同虚设
与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样
在任何时候都可能被垃圾回收。
虚引用在 Java 中,使用 java.lang.ref.PhantomReference 类表示。
作用
跟踪对象被垃圾回收的活动
用PhantomReference类来实现
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动
一个对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用获取对象实例,
唯一目的就是能在对象被回收时收到一个系统通知
它不能单独使用,必须和引用队列联合使用。
作用:跟踪对象被垃圾回收的状态。
虚引用与软引用和弱引用的区别
虚引用,必须和引用队列(ReferenceQueue)联合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中,是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
程序如果发现某个虚引用,已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
使用示例
虚引用必须和引用队列(ReferenceQueue)联合使用
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
Reference Queue引用队列
引用队列又称为Reference Queue
, 它位于java.lang.ref包下, 我们在建各种引用(软引用, 弱引
用, 幻象引用) 并关联到响应对象时, 可以选择是否需要关联引用队列。JVM会在特定的时机将引用入
队到队列中,程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。
用, 幻象引用) 并关联到响应对象时, 可以选择是否需要关联引用队列。JVM会在特定的时机将引用入
队到队列中,程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。
Reference引用
java.lang.ref.Reference为软(soft) 引用、弱(weak) 引用、虚(phantom) 引用的父类。
因为Reference对象和垃圾回收密切配合实现, 该类可能不能被直接子类化.
finalizers的finalize() 方法
finalize()
即使在可达性分析算法中不可达的对象,也并非是非死不可的,要真正被回收至少经历两次标记过程,
如果没有与GCRoots的引用链,对象将会被第一次标记和筛选(执行finalize()方法)
注意:并不建议使用该方法。
对象是否覆盖finalize方法
是:jvm执行finalize()方法
否:JVM回收对象
JVM是否执行过该对象的finalize方法
是:JVM回收对象(每个对象finalize方法只执行一次)
否:JVM执行finalize()方法
Java和C++的一大区别:C++允许对象定义析构函数方法
当对象超出作用范围或被明确删除时,会调用析构函数来清理使用的资源。
对于大多数对象来说, 析构函数能够释放使用new或者malloc函数分配的内存。
在Java中,垃圾收集器会为你自动清除对象, 分配内存, 因此不需要显式析构函数即可执行此操作。
这也是Java和C++的一大区别
然而, 内存并不是唯一需要被释放的资源
考虑FileOutputStream:
当你创建此对象的实例时, 它从操作系统分配文件句柄
如果你让流的引用在关闭前超过了其作用范围,该文件句柄会怎么样?
实际上, 每个流都会有一个finalizer方法
finalizer方法什么?
这个方法是,垃圾回收器在回收之前,由JVM调用的方法
对于FileOutputStream来说, finalizer方法会关闭流, 释放文件句柄给操作系统, 然后清除缓冲区, 确保数据能够写入磁盘。
任何对象都具有finalizer方法, 你要做的就是声明finalize()方法
声明finalize()方法
finalizers的finalize() 方法产生的负面影响非常大
虽然finalizers的finalize) 方法是一种好的清除方式, 但是这种方法产生的负面影响非常大
你不应该依靠这个方法来做任何垃圾回收工作
因为finalize方法的运行开销比较大, 不确定性强, 无法保证各个对象的调用顺序。
finalize能做的任何事情, 可以使用try-finally或者其他方式来做, 甚至做的更好
finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?
在释放对象占用的内存之前,垃圾收集器会调用对象的finalize()方法。
一般建议在该方法中释放对象持有的资源。
在Java中,对象什么时候可以被垃圾回收?
当对象对当前使用这个对象的应用程序变得不可触及时,这个对象就可被回收。
引用计数法/引用计数算法
给对象添加一个引用计数器,每当一个地方引用它时计数器加1;引用失效计数器减1,计数器为0说明不再引用
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
优点:实现简单,判定效率高
缺点:无法解决对象相互循环引用的问题
缺陷:循环引用会导致内存泄漏
引用计数法(理论)
在Java中,引用和对象是有关联的,如果要操作对象,则必须用引用进行。
因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。
一个对象如果没有任何与之关联的引用,即引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
没办法解决循环引用的问题
JVM 可达性分析法
前言
理解并掌握可达性分析法的原理以及意义【核心】
如何检测对象是否为垃圾
Java中通过可达性分析法,来检测对象是否为垃圾
如果不可达,则将对象标记为垃圾,会被 JVM 回收
可达性分析法基本原理
可达性分析法是通过 GC Roots 为起点的搜索
通过一系列称为"GC Roots"的对象,作为起始点
从这些节点向下搜索,搜索所走过的路径,称为引用链
GC Roots到对象不可达时,则证明,此对象是不可用的。
类似于,使用开发工具看代码
四种 GC Roots 无非是 Java 中的引用对象
从GC Roots 出发,类似于,使用开发工具看代码,发现某部分代码用不到了,就会删除这部分代码。
其实,可达性分析法也是如此,发现某些对象不可达了,就会被垃圾回收器收集。
可达性分析法示例(图示)
示意图来理解下,何为不可达对象
从上图中来看,对象 A,B,C,D,E,F 为可达对象;而对象 G,H,I,J,K 为不可达对象,会被标记为垃圾对象,最终被垃圾回收器回收。
如何选取 GCRoots 对象呢?四种类型的对象解释
【最常见】虚拟机栈中引用的对象
虚拟机栈(栈帧中的局部变量区,也叫局部变量表)
在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址,作为引用保存到虚拟机栈中
如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,
因此,如果在虚拟机栈中有引用,就说明这个对象还是有用的
这种情况是最常见的;
方法区中的,类静态变量属性引用的对象
全局的静态的对象
也就是使用了 static 关键字,
由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中
显然,将方法区中的静态引用作为 GC Roots 是必须的
方法区中,常量引用的对象
常量引用
就是使用了 static final 关键字
由于这种引用初始化之后,不会修改
方法区常量池里的引用的对象,也应该作为 GC Roots;
本地方法栈中,JNI引用的对象
Native 方法引用对象
(Native方法)
这一种是在使用 JNI 技术时,有时 Java 代码不能满足需求,需在 Java 中调用 C 或 C++ 的代码
因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用。
本地方法栈中引用的对象,也会被作为 GC Roots
GC Roots
可达性分析(实际)
下面是一个不同可达性状态的转换图
不同可达性状态的转换图
判断可达性条件, 也是JVM垃圾收集器决定如何处理对象的一部分考虑因素
所有的对象可达性引用都是java.lang.ref.Reference的子类
它里面有一个get()方法, 返回引用对象。
如果已通过程序或垃圾收集器清除了此引用对象, 则此方法返回null。
也就是说, 除了幻象引用外,软引用和弱引用都是可以得到对象的。
而且这些对象可以人为拯救,变为强引用,例如把this关键字赋值给对象, 只要重新和引用链上的任意一个对象建立关联即可,
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量变量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
活跃线程的引用对象
方法区中类静态属性引用的对象
方法区中常量变量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
活跃线程的引用对象
通过一系列的“GC roots”对象作为起点搜索。
如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
注意:不可达对象 != 可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。
两次标记后仍然是可回收对象,则将面临回收。
两种可达性判断条件
强可达
就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态:
不可达(unreachable)
处于不可达的对象就意味着对象可以被清除了。
引用计数法(Reference Counting) 的收集方式
许多人认为JVM会为每个对象保留一个引用计数
当每次引用对象时,引用计数器的值就+1
当引用失效时,引用计数器的值就-1
而垃圾收集器,只会回收引用计数器的值为0的情况。
但是这种方式无法解决对象之间相会引用的问题
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
当引用失效时,计数器值就减一;任何时刻计数器为零的就是不可能再被使用的。
当引用失效时,计数器值就减一;任何时刻计数器为零的就是不可能再被使用的。
优缺点
优点
实现原理简单
判定效率高
缺点
有许多特例的情况需要考虑
(无法解决循环引用的问题)
(无法解决循环引用的问题)
市面上的运用
Python
Squirrel等
可达性分析算法
该算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据关系引用关系向下到GC Roots间没有任何引用链相连,或者用图论的话来说就是从 GC Roots到这个对象不可到达时,则证明此对象时不可能再次被使用的。
可达性分析图解
finalize
如果一个对象没有任何引用链相连,则判断为对象不可用,作为可回收对象,通过finalize()自救。
如果一个对象本该被回收,但调用finaize()时,
为此对象赋予了与GC Roots的链接,则该对象会被“复活”
为此对象赋予了与GC Roots的链接,则该对象会被“复活”
如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过一次,那么虚拟机会视这两种情况为“没有必要执行”
调用过程
如果该对象被判定为确有必要执行finalize()方法,那么该对象会被执法置放在一个名为F-Queue的队列中
在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行他们的finalize()方法
该方法已被官方明确申明不推荐使用,所以在开发中不要使用该方法
在Java语言中,可作为GC Roots的对象包含
1.在虚拟机栈(栈帧中的本地变量表)中引用对象
2.在方法区类静态属性引用的对象
3.在方法区中常量引用的对象,譬如字符串常量池里的引用
4.在本地方法栈中JNI(即,通常所说的Native方法)引用的对象
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象等,还有系统类加载器
6.所有被同步锁(synchroonized关键字)持有的对象
7.反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合
市面上的运用
JAVA
C#
Lisp等
GC实现的方式
保守式GC
优点
这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快
缺点
因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用他们,GC也就自然不会回收他们,从而引起了无用的内存占用,造成资源浪费。
由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了
半保守式GC
准确式GC
“准确式GC”所谓的准确,关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么
实现的方式
在java中实现的方式是:从外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为OopMap(不同的虚拟机对映射表的叫法不一样)
三色标记算法
适用于,GC与用户线程并发的环境中,在不停止用户线程的情况下,对垃圾进行标记
使用
三种颜色标记
白色
表示对象尚未被垃圾收集器访问过。
在可达性分析刚开始阶段,所有对象都为白色,最后如果还都是白色则为不可达对象
黑色
表示对象已经被垃圾收集器访问过,切这个对象的所有引用都已经被扫瞄过了
如果有其他的对象引用指向了黑色对象,无需重新扫描一遍
黑色对象不可能直接(不仅过灰色对象)指向某个白色对象
灰色
表示对象已经被垃圾收集器访问过了,但这个对象上至少存在一个引用还没有被扫描过
标记的过程
会产生的问题
多标
此时,用户线程缺点了D中的E引用,E已经是一个不可达对象,却依然显示灰色,无法被回收
漏标
此时,原来G为不可达对象,被建立了与D的引用,此时G就会被回收,造成程序运行的错误
解决方式
当且仅当两个条件同时满足,才会产生"对象消失的问题"
赋值器插入了一条或多条从黑色对象到白色对象的引用
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
增量更新
破坏了第一个元素:当引用关系发生改变的时候就记录下来,然后并发扫描结束,再将这些记录过的关系引用对象重新扫描
通俗理解:黑色对象新插入了白色对象的引用,就将他编程灰色
CMS收集器就是采用了这总方法
原始快照(SATB)
该方法时破坏了第二个元素:当灰色对象插入新的指向白色对象的关系引用时,就将这个要删除的引用计入下来,在并发扫描结束之后,再将这些引用关系中的灰色的根对象重新扫描一次。
通俗理解:当一个白色对象引用断开连接,就记录一下,下次来查看这个记录,对记录中已经被删除引用的对象进行再次扫描,看看是否有新的引用对象指向它,如果有则将它变成灰色,如果没有则无需更改
引用
强引用
强引用是最传统的引用,无论任何情况下,只要强引用还在,垃圾回收器就永远不会回收掉被引用的对象
Object obj = new Object()
软引用
弱引用是用来描述一些还有有用,但非必须的对象。
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行回收。
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行回收。
1.2JDK版本后:提供SoftReference类来实现软引用
弱引用
弱引用也是用来描述那些非必须的对象,但是它的强度比软引用低一些,被弱引用关联着的对象只能生存到下一次垃圾回收发生为止。无论当前内存是否足够
JDK1.2版本后:提供WeakReference类来实现弱引用
虚引用
又叫“幽灵引用”或“幻影引用”,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
虚引用的唯一目的只是为了能够在这个对象被收集器回收时收到一个和系统通知。
JDK1.2版本后:提供PhantomReference类来实现虚引用
HotSpot VM的算法细节实现
根节点枚举
“根节点枚举”就是在确认根节点(GC Roots)的过程
而且,迄今为止所有的收集器在“根节点枚举”这一步骤时都必须暂停用户线程(STW)
虽然,现在可达性分析算法耗时最长的“查找引用链”的过程已经可以做到与用户线程一起并发,
但,“根节点枚举”始终还是必须在一个能保障一致性的快照中才得以进行
但,“根节点枚举”始终还是必须在一个能保障一致性的快照中才得以进行
什么是OopMap?
目前主流的JVM使用的都是准确式垃圾收集,
在扫描标记的时,虚拟机可以准确的知道该数据时什么类型或者是否是指针
在扫描标记的时,虚拟机可以准确的知道该数据时什么类型或者是否是指针
OopMap在准确式GC中用来保存类型的映射表
生成映射表的方式
每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。
安全点(Safe Point)
可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。
安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。
安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。
如何选定安全点
安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长
一般会在如下几个位置选择安全点
循环的末尾
方法临返回前
调用方法之后
抛异常的位置
如何让所有线程在安全点上停止下来呢?
抢断式中断
在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。
主动式中断
在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。
JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。
安全区域(Safe Region)
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,在这段时间里,虚拟机要发起垃圾收集时就不会管这些声明已经在安全区域内的线程了,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
清除阶段
Java堆内存被划分为两部分
新生代
老年代
因此,JVM中针对新生代和年老代分别提供了多种不同的垃圾收集器,
新生代
标记-清除(Mark-Sweep) 的算法
基本概念
实际上, JVM使用一种叫做标记-清除(Mark-Sweep) 的算法
标记-清除(Mark-Sweep)算法
标记-清除
标记-清除算法(Mark-Sweep)
Mark-Sweep
标记清除算法(Mark-Sweep)
最基本的算法
最基础的垃圾回收算法
分为两个阶段:标注和清除
同它的名字一样,分为“标记”和“清除”两个阶段:
【标记阶段】首先,标记出所有需要回收的对象
【清除阶段】标记完成后,统一回收所有被标记的对象
分为两个阶段:标注和清除
标记阶段,标记出所有需要回收的对象
清除阶段,回收被标记的对象所占用的空间
运行流程
1.首先标记出所有需要回收的对象
2.然后统一回收掉所被标记了的对象
过程
1.将需要回收的对象标记起来
2.清除对象
图解
适用场景
对象存活比较多的时候适用
老年代
标记清除垃圾回收背后的想法很简单
程序无法到达的每个对象都是垃圾,可以进行回收。
下面的过程中,就涉及到一个根节点CGC Roots) 来判断是否存在需要回收的对象
算法的基本思想:
通过一系列的GC Roots作为起始点, 从这些节点向下搜索, 搜索所走过的路径称为引用链(Reference Chain)
当一个对象到GC Roots之间没有任何引用链相连的话, 则证明此对象不可用。
引用链上的任何一个能够被访问的对象都是强引用对象
垃圾收集器不会回收强引用对象。
因此, 返回到foo(方法中, 仅在执行方法时, 参数bar和局部变量baz才是强引用。
一旦方法执行完成,它们都超过了作用域时,它们引用的对象都会进行垃圾回收。
标记-清除收集具有如下几个阶段
阶段一:标记
垃圾收集器会从根(root) 引用开始, 标记它到达的所有对象,
用老师给学生判断卷子来比喻即给试卷上的全部答案判断正确还是错误的过程。
图解阶段一:标记
图解阶段一:标记
阶段二:清理
在第一阶段中。所有可回收的的内容都能够被垃圾收集器进行回收
如果一个对象被判定为是可以回收的对象, 那么这个对象就被放在一个finalization queue(回收队列) 中
并在稍后,会由一个虚拟机自动建立的、低优先级的finalizer线程去执行它.
图解阶段二:清理
图解阶段二:清理
阶段三:整理(可选)
一些收集器有第三个步骤,整理。
在这个步骤中,GC将对象移动到垃圾收集器回收完对象后所留下的自由空间中。
这么做可以防止堆碎片化,防止大对象在堆中由于堆空间的不连续性而无法分配的情况。
图解阶段三:整理
图解阶段三:整理
下面来考虑一个例子
变量foo是一个强引用, 它指向一个LinkedList对象。
LinkedList(JDK.18) 是什么?
一个链表的数据结构
每一个元素都会指向前驱元素,每个元素都有其后继元素。
图解LinkedList
当调用add()方法时, 会增加一个新的链表元素, 并且该链表元素指向值为111的Integer实例。
这是一连串的强引用, 也就是说, 这个Integer的实例不符合垃圾收集条件。
一旦foo对象超出了程序运行的作用域, LinkedList和其中的引用内容都可以进行收集, 收集的前提是没有强引用关系。
缺点
从效率的角度讲
1.标记和清除的效率都不高
扫描了两次
标记存活对象
清除没有标记的对象
需要两个过程的遍历,一个是标记,一个是清除
标记和清除两个过程的的效率都不高
标记和清除效率都不高;
效率低
执行效率不稳定(较为缓慢)
从空间的角度讲
2.会产生大量的不连续的内存碎片
标记清除后,会产生大量不连续的内存碎片
会产生空间碎片化
碎片空间、内存碎片多
内存碎片太多的后果很严重
该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发一次垃圾回收动作。
提前GC
标记、清除之后会产生大量不连续的内存碎片
标记复制/复制算法/复制(coping)算法
基本概念
新生代使用的是复制算法
复制算法
Mark-Coping
复制算法(copying)
复制算法(新生代)
Eden:Survivor:Survivor=8:1:1
对象存活率低,且有老年代分配担保,因此常采用复制算法
分配担保:如果Survivor没有足够空间来存放上次新生代GC存活下来的对象,它们将通过分配担保机制进入老年代
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。
复制算法是为了解决效率问题而出现的
它将可用的内存分为两块
每次只用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面
复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。
然后,再把已经使用过的内存,一次性清理掉。
这样每次只需要对整个半区进行内存回收
需要将内存按照容量划分为大小相等的两块区域,每次只使用其中一块儿
运行流程
当这一块的内存用完了,就将还存活着的对象,复制到另外一块区域上,再把原来的区域一次清理掉
大致原理
按内存容量将内存划分为等大小的两块
每次只使用其中一块
当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
适合场景
存活对象少 比较高效
扫描了整个空间(标记存活对象并复制异动)
适合年轻代
eden
S1、 S2
优缺点
优点
实现简单,运行效率高
因为不需要两个步骤,只需要遍历一次,将存活的对象复制
简单高效,不会出现内存碎片问题
不会产生空间碎片化
缺点
需要空闲空间
内存利用率低
需要复制移动对象
对象存活率高时,复制效率较低
存活对象较多时效率明显会降低
空间开销比较大
现在的商用模拟机,都采用这种算法来回收新生代
在对象存活率较高的场景下,要进行大量的复制操作,效率还是很低。
如果,大部分的对象都能够存活则,需要复制的对象会很多,此时会产生较大的复制开销
每次只使用一半的内存空间,资源浪费严重
内存缩小为原来的一半,这样代价太高了
不过研究表明,1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。
HotSpot虚拟机,默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。
当然,没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。
老年代
标记-整理算法(Mark-Compact)
基本概念
标记整理算法(Mark-Compact)
标记-整理
Mark-Compact
标记-整理(Mark-Compact)算法
标记整理
目的:解决复制算法内存利用率的问题,并且减少了大量复制的问题。
根据老年代的特点,有人提出了另外标记-整理(Mark-Compact)算法
标记过程与标记-整理(Mark-Compact)算法一样
不过,不是直接对可回收对象进行整理,而是让所有存活对象都向一端移动
然后,清理掉边界以外的内存。
老年代使用的是标记-整理算法
结合了以上两个算法,为了避免缺陷而提出。
标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
原理和标记清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。
标记整理算法(老年代)
先执行标记-清除,让所有的存活对象都向一端移动,最后清理掉边界以外的内存
对象存活率高,且没有额外空间对它进行分配担保,因此常用标记清除或标记整理算法来实现
即在标记-清除算法的基础上,做一个整理的过程,以解决标记-清除中的碎片化问题
优缺点
优点
在标记-清除的基础上,解决了碎片化的问题
同时也避免了,复制算法的空间浪费问题
缺点
需要移动大量对象,效率不高
相对于标记-清除效率更低了
因为多了一个整理的过程
降低了系统的吞吐量
在垃圾清理的过程中需要,停止用户线程(被描述为“Stop The World”)
所以,过长的清理时间会降低系统的吞吐量
所以,过长的清理时间会降低系统的吞吐量
在CMS中为了解决不在内存分配上和访问上增加太大额外负担的方案
虚拟机在多数时间都采用标记-清除算法,暂时容忍内存碎片的存在
直到内存空间的碎片化程度已经大到影响内存分配了,在采用标记-整理算法收集一次
图解
标记-整理算法的工作过程如图
分代收集/分代回收
分代清理
分代清理到底是不是第四种算法呢?
不是,通常称之为分代收集理论,或称之为分代收集思想。
目前虚拟机基本都采用分代收集理论,来进行垃圾回收。
根据各个年代的特点选取不同的垃圾收集算法
新生代使用复制算法
老年代使用标记-整理或者标记-清除算法
分代回收算法
基本概念
分代收集理论结合了以上的 3 种算法
根据对象的生命周期的不同,将内存划分为几块,然后根据各块的特点,采用最适当的收集算法。
准确的说,分代收集理论,就是在不同的内存区域,使用不同的算法,它是 以上 3 种算法的使用者。
分代清理并非是一种单独的算法,而是一种收集理论。
Generational Collecting
分代收集算法
分代收集法是目前大部分JVM所采用的方法
其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,
分代
年轻
edan
s1
s2
minor gc
通过阈值晋升
老年
major gc 等价于 full gc
永久
一般情况下将GC堆划分为老生代和新生代(Young Generation)。
老年代
(Tenured/Old Generation)
特点:每次垃圾回收时只有少量对象需要被回收
新生代/年轻代
Eden/s1/s2
特点:每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
永久代/元空间
晋升机制
根据存活时间
新生代与标记复制算法
目前大部分JVM的GC,对于新生代,都采取Copying算法(标记复制)
因为新生代中每次垃圾回收都要回收大部分对象,
即要复制的操作比较少,但通常并不是按照1:1来划分新生代。
一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),
每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
新生代
老年代与标记整理算法
老年代,因为每次只回收少量对象,因而采用Mark-Compact算法。
JVM提到过的处于方法区的永生代(Permanet Generation)
它用来存储class类,常量,方法描述等。
对永生代的回收主要包括废弃常量和无用的类。
对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。
当新生代的Eden Space和From Space空间不足时就会发生一次GC
进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。
当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。
图解垃圾收集器搭配关系
垃圾收集器搭配关系
子主题
GC分代收集算法 VS 分区收集算法
分代收集算法
当前主流JVM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的GC算法
新生代-复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法,
只需要付出少量存活对象的复制成本,就可以完成收集.
老年代-标记整理算法
因为对象存活率高、没有额外空间对它进行分配担保,
就必须采用“标记—清理”或“标记—整理”算法来进行回收,
不必进行内存复制, 且直接腾出空闲内存.
分区收集算法
将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收.
好处:控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿。
JVM 垃圾回收器分类
7 种垃圾回收器
3 种垃圾回收器是作用于年轻代垃圾回收的收集器
3 种圾回收器是作用于老年代垃圾回收的收集器
剩余的 1 种垃圾回收器(G1收集器),能够同时作用于年轻代和老年代
串行收集器和吞吐量收集器的区别是什么?
JDK1.6中Sun HotSpot虚拟机的垃圾收集器
年轻代
Serial收集器
基本概念
单线程
串行单线程收集器
最基本、发展历史最久的收集器
是Client 模式下的默认新生代收集器
采用复制算法的、单线程的收集器
Serial收集器依然是虚拟机运行在 Client 模式下的默认新生代收集器
因为它简单而高效。
优缺点
特点:单线程收集;标记-复制算法
优点:简单高效
对线程开销少,简单有效,适用于内存资源受限,和处理器核心数教少的环境。(目前,客户端模式下默认使用收集器)
单线程的收集器
只会使用一个 CPU 或者一条线程去完成垃圾收集工作
进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。
场景:Client模式下默认新生代收集器;单核机器
图解
Serial 收集器运行过程
Serial 收集器运行过程
Serial垃圾收集器(单线程、复制算法)
Serial垃圾收集器是最基本垃圾收集器,(连续垃圾收集器)
曾经是JDK1.3.1之前,新生代唯一的垃圾收集器。
串行收集器
(serial)
对大多数的小应用就足够了。
小应用(在现代处理器上,需要大概100M左右的内存)
特点
单线程
Serial是一个单线程的垃圾收集器
只会使用一个CPU或一条线程去完成垃圾收集工作
在进行GC同时,必须暂停其他所有的工作线程,直到GC结束。
虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效
简单高效
对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,
因此,Serial垃圾收集器依然是JVM运行在Client模式下,默认的新生代垃圾收集器。
使用复制算法
Serial垃圾收集器使用复制算法,
ParNew收集器
基本概念
采用复制算法
Serial收集器的多线程版本
除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样
Server 模式下的虚拟机首选的新生代收集器。
jdk9以后唯一可以和CMS 一起搭配工作的收集器
Serial收集器的多线程版本
多线程
本质上是Serial收集器的多线程并行版本,除了多线程并发没有,其他几乎与Serial保持一致
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
特点:多线程并行收集;标记-复制算法;其他特点与Serial相似
场景:用户交互;配合CMS垃圾收集器
缺点:在单CPU场景效果不突出
特点
除了 Serial 收集器外,目前只有它,能与 CMS 收集器配合工作。
CMS 收集器第一次实现了让垃圾收集器与用户线程基本上同时工作
默认开启的收集线程数,与 CPU 数量相同
在 CPU 数量非常多的情况下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
图解
Parnew 收集器运行过程如图所示
ParNew垃圾收集器(Serial的多线程版本+复制算法)
特点
多线程
ParNew垃圾收集器,其实是Serial收集器的多线程版本
ParNew除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样
ParNew垃圾收集器在GC过程中同样也要暂停所有其他的工作线程。
ParNew垃圾收集器是很多JVM运行在Server模式下,新生代的默认垃圾收集器。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
使用复制算法
也使用复制算法
【Parallel:平行的】
Parallel Scavenge收集器(多线程复制算法、高效、吞吐量)
Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器
它重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务
主要适用于在后台运算,而不需要太多交互的任务。
吞吐量收集器
(throughput)
使用并行版本的新生代垃圾收集器
它用于中等规模和大规模数据的应用程序
吞吐量(Thoughput)
CPU用于运行用户代码的时间/CPU总消耗时间
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
自适应调节策略,也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
Parallel Scavenge收集器
多线程
他目标是达到一个可控制的吞吐量
是一个吞吐量优先的收集器
基本概念
也是一个新生代收集器,并行的多线程收集器。
采用了复制算法
目标:达到一个可控制的吞吐量。
虚拟机运行在 Server 模式下的默认垃圾收集器。
被称为“吞吐量优先收集器”。
Parallel Scavenge 收集器运行过程同 Parnew 收集器一样
多线程收集器
适合在后台运算而不需要太多交互的任务。
“吞吐量优先”收集器,更加关注系统的吞吐量
特点:目标在于达到可控吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间));标记-整理
场景:高效利用CPU,后台运算且不需要太多交互
图解
图解
控制吞吐量
CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,
而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。、
吞吐量:CPU 用于运行用户代码时间与 CPU 总消耗时间的比值
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
空间吞吐量参数介绍
-XX:MaxGCPauseMills 和 -XX:GCTimeRatio参数
虚拟机提供了-XX:MaxGCPauseMills 和 -XX:GCTimeRatio 两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小
不过不要以为前者越小越好,GC 停顿时间的缩短,是以牺牲吞吐量和新生代空间换取的。
由于与吞吐量关系密切,Parallel Scavenge 收集器也被称为“吞吐量优先收集器”。
-XX:UseAdaptiveSizePolicy 参数
这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了
虚拟机会根据当前系统的运行情况以及性能监控信息,动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量。
如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难时
可以使用 Parallel Scavenge收集器,配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。
老年代
G1收集器
同上
Serial Old收集器
Serial 收集器的老年代版本
可用于Client模式下
用于Server模式下时
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
基本概念
Serial Old 收集器同样是一个单线程收集器,作用于老年代
使用“标记-整理算法”
这个收集器的主要意义也是在于:给 Client 模式下的虚拟机使用。
特点:Serial的老年代版本,单线程;标记-整理
场景:1.5之前与Parallel Scavenge配合使用;作为CMS的后备预案
图解
Serial Old 收集器运行过程如图所示
Serial Old收集器(单线程标记整理算法 )
Serial Old是Serial垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,
这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
在Server模式下,主要有两个用途
1. 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
2. 作为年老代中使用CMS收集器的后备垃圾收集方案。
新生代Serial与年老代Serial Old搭配垃圾收集过程图
子主题
新生代Parallel Scavenge收集器与ParNew收集器工作原理类似
都是多线程的收集器
都使用的是复制算法
在垃圾收集过程中都需要暂停所有的工作线程
新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图
子主题
Parallel Old收集器
基本概念
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本
使用多线程和“标记-整理算法”进行垃圾回收
这个收集器在 JDK 1.6 之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge收集器+Parallel Old收集器 的组合。
由于在JDK6之前,Parallel Scavenge唯一能够搭配的就是 Serial Old收集器,但是 Serial Old是一个单线程的处理器,实际上Serial Old 在一定程度上拖累了 Parallel Scavenge所以 Parallel Old 就诞生了
多线程
Parallel Scavenge 收集器的老年代版本
注重程序的吞吐量
特点:标记-整理;多线程
场景:为了替代Serial Old与Parallel Scavenge配合使用
图解
Parallel Scavenge 收集器+Parallel Old 收集器 的组合运行过程如图所示
Parallel Old收集器(多线程标记整理算法+吞吐量)
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。
产生背景
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量
Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器
如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:
子主题
CMS收集器
多线程的并发收集器(能够和用户线程并发)
是一种以获取最短回收停顿时间为目标的收集器,是一种老年代的垃圾收集器
默认启动的线程数是:(处理器核心数量+3)/4
采用的算法是“标记-清除”算法
运行过程
初始标记
需要STW
该阶段主要是标记一下GC Roots能够直接关联到的对象
并发标记
可以和用户线程并发进行
该阶段从GC Roots的直接关联对象开始遍历整个对象图的过程
重新标记
需要STW
由于主要标记垃圾对象是在并发阶段进行的,如果在标记的期间用户线程的变动可能导致变动的标注出现变动
该阶段 主要就是对因用户线程运作而导致标记变动的那一部分对象进行重新标记
并发清除
可以和用户线程并发进行
对以标记的对象进行垃圾清理,由于用户线程还在运行,顾无法进行整理
优缺点
优点
CMS非常注重STW的时间,停顿时间短,所以对用户有良好的体验
缺点
由于是与用户线程并发执行,所以需要占用系统子资源(线程数,内存)所以当内存不够时,可能会导致CMS启动失败,不得不启用备用方案
在单核处理器的环境下 由于需要线程的开销,所以与用户线程同时进行意义不大,甚至还会降低性能
吞吐量相对较低
无法处理“浮动垃圾”而导致另一次完全的STW的Full GC的产生
无法处理"碎片化"的问题,而提前Full GC
处理方式:可以通过参数设置,指定多少次Full GC 时进行一次内存的整理
基本概念
CMS(Conrrurent Mark Sweep,连续标记扫描)收集器
是以获取最短回收停顿时间为目标的收集器。
使用标记-清除算法。
特点:最短回收停顿时间;标记-清除
流程
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW)。
并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。
并发清除:不需要停顿。
缺陷
吞吐量低
低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
无法处理浮动垃圾,可能出现 Concurrent Mode Failure
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
会产生空间碎片
标记 - 清除算法会导致产生不连续的空间碎片
步骤
初始标记:标记GC Roots直接关联的对象,速度快
并发标记:GC Roots Tracing过程,好市场,与用户进程并发工作
重新标记:修正并发标记期间用户进程继续运行而产生变化的标记,耗时比初始标记长,但远小于并发标记
并发清除:清除标记的对象
缺点
对CPU资源敏感
无法回收浮动垃圾
标记-清除算法,会产生内存碎片,可以通过参数开启碎片的合并整理
收集步骤
初始标记:标记 GCRoots 能直接关联到的对象,时间很短;
并发标记:进行 GCRoots Tracing(可达性分析)过程,时间很长;
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
并发标记:进行 GCRoots Tracing(可达性分析)过程,时间很长;
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
图解
CMS 收集器运行过程如下图所示:
CMS收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器
CMS- Concurrent Mark Sweep
和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
其最主要目标是获取最短垃圾回收停顿时间
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS工作机制相比其他的垃圾收集器来说更复杂
CMS整个过程分为以下4个阶段
初始标记
只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除
清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,
所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
CMS收集器工作过程
CMS收集器工作过程
缺点
对cpu资源敏感
无法处理浮动垃圾
基于标记清除算法 大量空间碎片
G1收集器
基本概念
G1 是目前技术发展的最前沿成果之一
HotSpot开发团队赋予它的使命是,未来可以替换掉 JDK1.5 中发布的 CMS 收集器
特点
并发和并行
使用多个 CPU 来缩短 Stop The World 停顿时间,与用户线程并发执行;
分代收集
独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果;
空间整合
基于标记-整理算法
无内存碎片产生
空间整合,不会产生内存碎片
可预测的停顿
可预测的停顿:
能建立可预测的停顿时间模型
能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
将整个Java堆划分为多个大小相等的独立区域Region,跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region 的集合。
在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而 G1 不再是这样。
使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
步骤/流程
初始标记:
标记GC Roots直接关联的对象
初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
并发标记:
对堆中对象进行可达性分析,找出存活对象,耗时长,与用户进程并发工作
从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
最终标记:
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。
这阶段需要停顿线程(STW),但是可并行执行。
修正并发标记期间用户进程继续运行而产生变化的标记
筛选回收:
对各个Region的回收价值排序,然后根据期望的GC停顿时间制定回收计划
首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。
此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
运行过程
初始标记
仅仅是标记一下GC Roots能够直接关联到的对象并且修改TAMS指针值
并发标记
从GC Roots开始对队中对象进行可达性分析自己,找到需要回收的对象
递归扫描完成后,还需要重新处理SATB记录下在并发时有引用变动的对象
子主题
最终标记
需要STW
对并发阶段结果后遗留下来的少量SATB记录进行处理
筛选回收
因为涉及到对象移动,所以需要STW
对Region进行数据统计,对各个Region的回收价值和成本进行排序,更具用户所期望的停顿时间来制定回收计划
可以自由选择任意多个Region进行收集,然后回收的那一部分Region的存活对象复制到空的Region中,清理掉原来的Region
G1参考
深入理解 Java G1 垃圾收集器 http://blog.jobbole.com/109170/https://blog.csdn.net/qqqqq1993qqqqq/article/details/71908145https://blog.csdn.net/lixiaotao_1/article/details/78870285https://blog.csdn.net/xiaomingdetianxia/article/details/77446762http://ifeve.com/
深入理解g1垃圾收集器https://blog.csdn.net/qq_34280276/article/details/52863551https://www.jianshu.com/p/e99000058840https://blog.csdn.net/arctan90/article/details/67648438
参数设置
-XX:+UseG1GC
-XX:MaxGCPauseMillis
多线程(可以和用户线程并发执行的 )
采用分区算法(取消了原来分代算法的物理划分,但还是保留了逻辑划分)
垃圾清理算法
从整体来看:标记-整理
从region之间来看:复制算法
既然使用了分区算法,那么在不同的分区之间对象就存在一个相互应用的问题
顾在G1收集器的每一块分区中都存在一个(记忆集)RSet,用来存放其他分区对该分区的引用记录
TAMS指针
G1在GC时用户线程时同步进行的,所以就会产生新的对象,G1为每个region设计了两个名为TAMS的指针
把region中的一部分空间划分出来用于并发回收过程中新对象的分配
把region中的一部分空间划分出来用于并发回收过程中新对象的分配
优缺点(与CMS相比较)
优点
停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU
缺点
相比Parallel Scavenge/Parallel Old 没有吞吐量优势
由于采用分区算法,所以每个区需要设计RSet,在内存空间上回尝试额外的消耗(20%左右)
G1 收集器 内存分布图
G1 收集器 内存分布图
Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果
分区概念 弱化分代
标记整理算法 不会产生空间碎片 分配大对象不会提前full gc
可以设置预设停顿时间
充分利用cpu 多核条件下 缩短stw
收集步骤
初始标记 stw 从gc root 开始直接可达的对象
并发标记 gc root 对对象进行可达性分析 找出存活对象
可达性分析算法
最终标记
筛选回收
根据用户期待的gc停顿时间指定回收计划
回收模式
young gc
回收所有的eden s区
复制一些存活对象到old区s区
mixed gc
GC模式
相比与CMS收集器,G1收集器两个最突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1与CMS的区别
g1分区域 每个区域是有老年代概念的 但是收集器以整个区域为单位收集
g1回收后马上合并空闲内存 cms 在stw时做
G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。
G1收集器
G1收集器运行示意图
G1收集器运行示意图
其他垃圾处理器
ZGC
碎片处理方式分
压缩式垃圾回收器
压缩式垃圾回收器会在垃圾回收完成之后,对存活对象进行压缩整理,消除回收后的碎片
再分配对象空间使用:指针碰撞
非压缩式垃圾回收器
非压缩式的垃圾回收器不进行这步操作
再分配对象空间使用:空闲列表
GC类型分为两种
Minor GC
发生在 young区的gc
(主要是young区的Survivor)
Major GC
发生在 old(Tenured)区的gc称为 major gc
major的速度比minor慢10倍至少。
GC
触发GC的条件
(1)System.gc()方法的调用
System.gc() 或 Runtime.getRuntime().gc()
System.gc()和Runtime.gc()会做什么事情?
会显示触发Full GC
这两个方法用来提示JVM要进行垃圾回收。
但是,立即开始还是延迟进行垃圾回收,是取决于JVM。
System.gc无法保证一定能够对垃圾收集器的调用
(2)老年代代空间(old/Tenured)不足
https://www.jianshu.com/p/1a0b4bf8d498
垃圾回收过程
垃圾回收过程
大多情况下对象在Eden分配,当Eden没有足够空间时将发起一次Minor GC
当Eden执行Minor GC后还不足以为对象分配空间,大对象直接进入老年代,可以用参数设置大对象直接进入老年代,避免频繁Minor GC
如果对象在Eden出生,发生MinorGC后仍然存活,且能被Survivor容纳,年龄加1,达到一定年龄进入老年代,默认15
占Survivor空间一半以上且年龄相等的对象,大于等于该年龄以上的对象直接进入老年代
发生Minor GC之前会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于说明MinorGC安全;否则会判断是否允许担保失败,如果允许担保失败,判断老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试MinorGC,否则执行FullGC
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
GC垃圾收集器、垃圾收集器(Garbage Collectors)
内存区域设置
XX:G1HeapRegionSize
复制成活对象到一个区域 暂停所有线程
如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
Java堆的结构是什么样子的?什么是堆中的永久代?
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。
它在JVM启动时被创建。
对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的
存活对象
应用可以访问的,不会被垃圾回收
死亡对象
应用不可访问,尚且还没有被垃圾收集器回收掉的对象。
一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
JVM的永久代中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。
这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
请参考下Java8:从永久代到元数据区
Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区
工作模式分类
并发式垃圾回收器
独占式垃圾回收器
工作的内存区间分
年轻代垃圾回收期
老年代垃圾回收器
评估GC的性能指标
吞吐量
运行用户代码时间占总运行时间的比例
(总运行时间:程序的运行时间+内存回收的时间)
(总运行时间:程序的运行时间+内存回收的时间)
运行用户代码时间/(运行代码时间+垃圾收集时间)
垃圾收集开销
吞吐量的补数,垃圾收集所用时间与总运行时间的比例
暂停时间
执行垃圾收集时,程序的工作线程被占暂停的时间
收集频率
相对于应用程序的执行,收集操作发生的频率
内存占用
Java堆区所占的内存大小
快速
一个对象从诞生被回收所经历的时间
与C++的区别,Java提供垃圾收集器来释放不再引用的对象
Java给你提供了一个new操作符来为堆中的对象开辟内存空间, 但它没有提供delete操作符来释放对象空间。
当foo0方法返回时, 如果变量baz超过最大内存, 但它所指向的对象仍然还在堆中,
如果没有垃圾回收器的话, 那么程序就会抛出OutOfMemoryError错误。
然而Java不会, 它会提供垃圾收集器来释放不再引用的对象
Java能永久不衰的一个原因就是因为垃圾收集器。
当程序尝试创建新对象,并且堆中没有足够的空间时,垃圾收集器就开始工作
当收集器访问堆时,请求线程被挂起,试图查找程序不再主动使用的对象,并回收它们的空间。
类加载机制
类加载
类加载器子系统
Class字节码文件,是存储在物理磁盘中(或加载于网络等等),最终都需要加载到虚拟机中之后才能被运行和使用
类加载机制、类加载过程、类的生命周期
JVM 类加载机制
类从被加载到虚拟机内存中开始,到卸载出内存结束。
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,
加载->验证->准备->解析->初始化->使用->卸载
JVM类加载机制的五大部分
加载
加载是类加载过程中的一个阶段
通过一个类的全限定名来获取定义此类的二进制字节流
将二进制字节流代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类各种数据的访问入口
加载(Loading)
这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。
生成一个class对象
将编译后的.Class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到
该阶段,需要完成的三件事情
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
JVM并没有规定字节码的获取来源,例如:
从ZIP压缩包中读取,JAR,EAR,WAR格式等等
从网络中获取,典型的就是Web Applet
在运行时计算生成,例如:动态代理技术,可动态生成一个“*&Proxy”的代理类
由其他文件生成,例如JSP文件生成对应的Class文件
从数据库中读取,该场景相对少见,例如:中间件服务器,可以将程序安装到数据库中完成程序代码在集群间的群发
从加密文件中获取,通过加载中进行解密Class文件
加载方式
虚拟机内置的启动类加载器来完成
用户自定义的类加载去完成
可通过自定义类加载器区控制字符流的获取方式
(重写一个类加载器的findClass()或loadClass()方法)
(重写一个类加载器的findClass()或loadClass()方法)
对于数组类而言
数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构建出来的
类加载器
类加载器类型
启动类加载器
(Bootstrap Class Loader)
(Bootstrap Class Loader)
该加载器负责加载:
存放在<JAVA_HOME>/lib目录,
或则被-XbootclassPath参数所指定的路径存放的,
而且是 "Java虚拟机" 能够识别类库加载到虚拟机的内存中
存放在<JAVA_HOME>/lib目录,
或则被-XbootclassPath参数所指定的路径存放的,
而且是 "Java虚拟机" 能够识别类库加载到虚拟机的内存中
该加载器无法被Java程序直接应用
(在Java中无法通过getClassLoader()方法获取到)
(在Java中无法通过getClassLoader()方法获取到)
拓展类加载器
(Extension Class Loader)
(Extension Class Loader)
类加载器负责加载:
<JAVA_HOME>\lib\ext目录中
或则被java.ext.dirs 系统变量所指定路径中的所有类库
<JAVA_HOME>\lib\ext目录中
或则被java.ext.dirs 系统变量所指定路径中的所有类库
“父类”加载器是启动类加载器
应用程序类加载器
(Applicatioon Class Loader)
也被称为“系统类加载器”
(Applicatioon Class Loader)
也被称为“系统类加载器”
该加载器负责加载:
用户类路径(ClassPath)上所有的类库
用户类路径(ClassPath)上所有的类库
该类加载器,为大多数开发者最常用的加载器
"父类"加载器是拓展类加载器
自定义类加载器
继承ClassLoader重写findClass()或loadClass()方法
一般来说现在重写findClass()比较多
一般来说现在重写findClass()比较多
为了方便也可以继承 URLClassLoader的方式,这样可以省去重写findClass()所带来的麻烦
"父类"加载器是系统类加载器
获得类加载器的方式
- 获取当前类的ClassLoader
- 获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
- 获取系统类ClassLoader
- 获取调用者的ClassLoader
注意,这里不一定非得要从一个Class文件获取
从ZIP包中读取(比如从jar包和war包中读取)
在运行时计算生成(动态代理)
由其它文件生成(比如将JSP文件转换成对应的Class类)
连接
验证(Verification)
验证时连接阶段的第一步,这个阶段的目的时确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息不被当作代码运行后不会危害虚拟机自身安全。
验证会大致完成一下四个检验动作
1. 文件格式验证
该验证阶段主要保证输入的字节流能正确地解析并存储于方法区中
一些可能包括的验证点,但是实际上不止这一些
是否已魔数 0xCAFEBABE开头
主、次版本号是否在当前Java虚拟机接受范围之内
常量池的常量中是否有补呗支持的常量类型(检查常量tag标志),等
2. 元数据验证
该阶段主要目的是对类的元数据信息进行语义校验
可能包括的验证点
这个类是否有父类(除了Object类外,所有类应当有父类)
这个类的父类是否继承了不允许被继承的累(final修饰的类)
类中的字段、方法是否与父类产生矛盾,等
3. 字节码验证
该阶段主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
【在第二阶段后,对类的方法体(Class文件中的Code属性) 进行校验分析。】
【在第二阶段后,对类的方法体(Class文件中的Code属性) 进行校验分析。】
保证不发生一些危害虚拟机安全的行为
保证任意时刻不会出现
在操作栈放置了一个int类型的数据,使用时却按long类型来加载入“本地变量表”中
在操作栈放置了一个int类型的数据,使用时却按long类型来加载入“本地变量表”中
保证任何跳转指令都不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换总是有效的
StackMapTable
为了避免过多的执行时间消耗在字节码验证阶段,JDK6之后的Javac编译器和Java虚拟机里进行了联合优化
把尽可能多的校验辅助措施挪到Javac编译器中进行。于是在方法体Code属性的属性表中新增 “StackMapTable”属性
把尽可能多的校验辅助措施挪到Javac编译器中进行。于是在方法体Code属性的属性表中新增 “StackMapTable”属性
其具体实现效果
4. 符号引用验证
该阶段发生在虚拟机将符号引用转化为直接应用的时候,这个转化动作发生在"解析"阶段中。
验证类是否缺少或被禁止访问它依赖的某些外部类、方法、方法的可访问性。
验证类是否缺少或被禁止访问它依赖的某些外部类、方法、方法的可访问性。
校验举例
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符及简单名称所描述的字段和方法
符号引用中的类、字段、方法的可访问性(private,protected,public)是否可被当前类访问
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
目的:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备(Preparation)
默认值
static会分配内存
正式为类变量分配内存并设置初始值
准备阶段正式为类中定义的变量(类变量,即:静态变量。不包括实例变量)分配内存并设置类变量初始值的阶段
初始值:通常情况下,是数据类型的零值
例如:public static int value = 123;
在经过准备阶段时 value会被赋值为int的零值,即 0
在经过准备阶段时 value会被赋值为int的零值,即 0
当类字段的字段属性表中存在ConstantValue属性时(也就是被final标志的常量),在准备阶段,将会直接指定为所指定的初始值
public static final value = 123
在准备阶段中 value 会被赋值为 123
在准备阶段中 value 会被赋值为 123
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
注意这里所说的初始值概念,
比如一个类变量定义为:
public static int v = 8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的put static指令是程序被编译后,存放于类构造器<client>方法之中。
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。
解析(Resolution)
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
解析具体类的信息
引用等
将class文件的常量池的符号引用替换为直接引用的过程(是静态链接)。
可能发生在始化阶段之前,也可能发生在初始化阶段之后,后者是为了支持 Java 的动态绑定。
符号引用就是class文件中的:等类型的常量。
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。
各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的
因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
如果有了直接引用,那引用的目标必定已经在内存中存在。
解析阶段是Java虚拟机常量池内的符号引用替换为直接引用的过程
把间接引用转换为直接引用
初始化
<clinit>()方法
该阶段就是执行类构造器<clinit>()方法的过程
<clinit>()方法并不是由程序员在Java直接编写的代码
<clinit>()方法并不是由程序员在Java直接编写的代码
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的
编译器收集顺序是由语句在源文件中出现的顺序决定的
静态语句块中只能访问到静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此,在Java虚拟机中第一个被执行的类型一定是Object类
<clinit>()方法对于类或接口来说不是必需的,如果一个类没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口也可以生成<clinit>()方法,但是于类有所不同
执行接口<clinit>()方法,不需要先执行父接口的<clinit>()方法
只有当父接口中定义的变量被使用时,父接口才会被初始化
接口的实现类在初始化时,也一样不会执行接口的<clinit>()方法
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境下 被正确的加同步锁
在准备阶段时,变量已经经过赋值一次系统要求的初始零值,在该阶段,则会更具程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
规定有六种情况必须立即对类进行“初始化”
遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果没有初始化,则需要先触发其初始化阶段。
使用了 new 关键字实例化对象的时候
读取或设置一个类型的静态字段
(被final修饰的,在编译器就把结果放入常量池的静态字段除外。)
(被final修饰的,在编译器就把结果放入常量池的静态字段除外。)
调用一个类型的静态方法的时候
调用反射包的方法对类型进行反射调用的时候
当初始化类时,如果该类的父类没有经行初始化,则需要先触发父类的初始化
虚拟机启动时,虚拟机会先初始化要执行的主类,也就是含有main()方法的那个类
当JDK7新加入动态语言支持时,如果一个java.lang.invoke.MethodHanle实例最后解析的结果为:
REF_getStatic、REF_putStatic、REFinvokeStatic、REF_newInvokeSpecial四种类型的方法句柄
且这个方法句柄对应类没有进行过初始化,则需先触发其初始化
REF_getStatic、REF_putStatic、REFinvokeStatic、REF_newInvokeSpecial四种类型的方法句柄
且这个方法句柄对应类没有进行过初始化,则需先触发其初始化
当接口中定义了JDK8新加入的默认方法(defult)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
初始化(Initialization)
执行<cliinit>(),初始化类变量、静态代码块
初始化阶段是类加载最后一个阶段,
前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。
到了初始阶段,才开始真正执行类中定义的Java程序代码。
为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序(执行<cinit>()方法)。
父类没初始化 先初始化父类
使用
使用(Using)
卸载
卸载(Unloading)
类加载子系统
类加载子系统的三个步骤:加载、链接、初始化
类加载子系统的处理过程
类加载子系统的处理过程
Java 的动态类加载功能,由类加载器子系统处理,处理过程包括加载、链接和初始化
类加载子系统的三个步骤
加载
通过三种不同的类加载器对 Class 文件进行加载
【自定义类加载器】通过复写 classLoader 方法
链接
对加载好的 Class 文件进行字节码、静态变量、方法引用等
进行验证和解析,为初始化做准备。
初始化
类加载的最后阶段
对类进行初始化
类加载子系统的三个步骤:加载
前言
启动(Bootstrap)类加载器的作用及代码验证【重点】
扩展(Extension)类加载器的作用及代码验证【重点】
系统(System Application)类加载器的作用及代码验证【重点】
扩展(Extension)类加载器的作用及代码验证【重点】
系统(System Application)类加载器的作用及代码验证【重点】
加载步骤,三个更加细粒度的模块
加载(Loading)步骤的模块划分
步骤加载(loading)里边包含了三个更加细粒度的模块
BootStrap ClassLoader
Extention ClassLoader
Application ClassLoader
这三个 Class Loader 就是加载过程中必须要使用到的三大类加载器。
类加载子系统的三个步骤:链接
前言
链接(Linking)步骤更加详细的模块划分:验证,准备和解析【基础】
掌握在链接(Linking)步骤中的第一步:验证的详细验证内容【重点】
掌握在链接(Linking)步骤中的第二步:准备的准备内容【重点】
掌握在链接(Linking)步骤中的第三步:解析的具体解析内容【重点】
掌握初始化(Init)步骤中的规则以及实例初始化顺序【重点】
掌握在链接(Linking)步骤中的第一步:验证的详细验证内容【重点】
掌握在链接(Linking)步骤中的第二步:准备的准备内容【重点】
掌握在链接(Linking)步骤中的第三步:解析的具体解析内容【重点】
掌握初始化(Init)步骤中的规则以及实例初始化顺序【重点】
链接步骤,三个更加细粒度的模块
链接步骤,三个更加细粒度的模块
链接-验证(verify)
定义
验证是连接阶段的第一步
验证过程中,主要对三种类型的数据进行验证
目的
确保 Class 文件的字节流中包含的信息,符合当前虚拟机的要求
并不会危害虚拟机的自身安全。
主要验证信息(三种类型的数据)
元数据验证
具体校验的内容
验证该类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类)
验证该类是否继承了不允许被继承的类(被 final 修饰的类)
如果该类不是抽象类,验证该类是否实现了其父类或接口之中所要求实现的所有方法
验证类中的字段、方法是否与父类产生矛盾
覆盖了父类的 final 字段
出现不符合规则的方法重载
方法参数都一致,但返回值类型却不同
字节码验证
目的
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
该阶段将对类的方法体进行校验分析,保证被校验类的方法,在运行时,不会产生危害虚拟机安全的事件
具体校验的内容
保证任意时刻,操作数栈的数据类型与指令代码序列,都能配合工作。
例如不会出现类似这样的情况:
在操作数栈,放置了一个int类型的数据
使用时,却按long类型,来加载入本地变量表中
保证跳转指令不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换是有效的
例如
可以把一个子类对象,赋值给父类数据类型
但是把父类对象,赋值给子类数据类型
甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型
则是危险,不合法的。
符号引用验证
目的
类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验
具体校验的内容
符号引用中,通过字符串描述的全限定名,是否能够找到对应的类
在指定类中,是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法的访问性是否可被当前类访问
方法的 访问性((private、default、protected、public))
链接-准备(prepare)
定义
准备阶段是正式为类变量分配内存,并设置类变量默认值的阶段
准备阶段是设置类变量的默认值
默认值(通常情况下是数据类型的零值)
这些变量所使用的内存,都将在方法区中进行分配。
此时,进行内存分配的,仅包括类变量,而不包括实例变量
类变量(被static修饰的变量)
而不包括实例变量
实例变量将会在对象实例化时,随着对象一起分配在Java堆中。
不同类型的类变量的默认值是不同的
不同类型的类变量的默认值是不同的。变量默认值的对照表请参看下表:
变量默认值的对照表
数据类型:reference
默认值:null
默认值:null
链接-解析(resolve)
定义
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用
(Symbolic References)
符号引用,以一组符号来描述所引用的目标
符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用
Direct References
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
如果有了直接引用,那么引用的目标一定是已经存在于内存中。
解析过程具体的解析内容
解析过程中,主要对如下4种类型的数据进行验证
类或接口的解析
字段解析
类方法解析
接口方法解析
类加载子系统的三个步骤:初始化
定义
【链接-准备】阶段,变量已经赋过一次系统要求的初始零值
【初始化】阶段,则会根据程序员通过程序编码制定的主观计划,去初始化类变量和其他资源。
类的【初始化】阶段是类加载过程的最后一个步骤
除了在【加载】阶段,用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由JVM来主导控制。
直到【初始化】阶段,JVM才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
实例的初始化顺序【重要】
在进行初始化时,实例变量的初始化顺序如下图所示:
实例变量的初始化顺序图示
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
双亲委派模型、委派双亲机制
前言
双亲委派模型是加载(Loading)步骤中所使用的模型
了解双亲委派模型的示意图【基础】
掌握双亲委派模型的工作原理,并结合示意图进行理解【核心】
举出三个不同类型的案例供学习者参考,更深入的了解双亲委派模型的工作原理【重点】
掌握双亲委派模型的工作原理,并结合示意图进行理解【核心】
举出三个不同类型的案例供学习者参考,更深入的了解双亲委派模型的工作原理【重点】
内容
JVM通过双亲委派模型进行类的加载
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
双亲委派原则
父类加载 不重复加载
双亲委派模型
双亲委派模型的示意图
实现
首先检查类是否被加载;
若未加载,则调用父类加载器的loadClass方法;
若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当前类加载器调用findClass加载类;
若父类加载器可以加载,则直接返回Class对象;
好处
可以避免重复加载
安全
保证java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题。
保护系统类不会被破坏
防止重复加载同一个class
比如加载位于rt.jar包中的类java.lang.Object
不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,
保证了使用不同的类加载器最终得到的都是同样一个Object对象。
破坏
基础类需要调用用户的代码
解决方式
线程上下文类加载器
也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原
实现方法
重写ClassLoader类的loadClass()
示例:
JDBC
原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类
JNDI服务需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码
重写loadClass()方法
双亲委派模型的具体实现就在loadClass()方法中
用户对程序的动态性的追求
例如OSGi(面向Java的动态模型系统)的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
代码热替换、模块热部署
典型的打破双亲委派模型的框架和中间件有tomcat与osgi
双亲委派模型原理
向上委托
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
如果父类加载器,还存在其父类加载器,则进一步向上委托
依次递归,请求最终将到达顶层的启动类加载器。
如果父类加载器,可以完成类加载任务,就成功返回;
向下委派
倘若父类加载器无法完成此加载任务
,子加载器才会尝试自己去加载
1. 如果一个类加载器收到了一个类加载请求,
它并不会自己先去加载,而是把这个请求委托给父类加载器执行
它并不会自己先去加载,而是把这个请求委托给父类加载器执行
2. 所有加载器都是如此依次递归,
因此所有的加载请求最终都应该传送到顶成的类加载器中
因此所有的加载请求最终都应该传送到顶成的类加载器中
3.只有当父类加载器反馈自己无法完成这个加载请求时,
子类加载器才会尝试自己去完成加载。
子类加载器才会尝试自己去完成加载。
图解
双亲委派机制
案例 1:加载 /jre/lib/resources.jar
前提
/jre/lib/resources.jar 是需要被启动(BootStrap)类加载器加载的核心类库
加载流程图解释与说明
根据双亲委派模型,resources.jar 的完整加载过程。
resources.jar 的完整加载过程
4 步加载核心类库 resources.jar
步骤 1
resources.jar 会先通过自定义类加载器(前提是实现了自定义类加载器)
自定义类加载器不会做处理,直接向上委托给系统加载器
步骤 2
系统类加载器,接到委托后,也不做任何处理,直接向上委托给扩展类加载器;
步骤 3
扩展类加载器接到委托后,也不做任何处理
直接向上委托给启动类加载器
步骤 4
启动类加载器接到委托后,发现 resources.jar 是自己负责加载的核心类库
于是进行加载,最后成功加载了 resources.jar。
案例 2:加载 /jre/lib/ext/cldrdata.jar
前提
/jre/lib/ext/cldrdata.jar 是需要被扩展类加载器加载的核心类库
加载流程图解释与说明
cldrdata.jar 的完整加载过程
5步加载核心类库 cldrdata.jar
步骤 1
cldrdata.jar 会先通过自定义类加载器(前提是实现了自定义类加载器)
自定义类加载器不会做处理,直接向上委托给系统类加载器
步骤 2
系统类加载器接到委托后,也不做任何处理
直接向上委托给扩展类加载器
步骤 3
扩展类加载器接到委托后,也不做任何处理
直接向上委托给启动类加载器
步骤4
启动类加载器接到委托后,发现 plugin.jar 不是自己负责加载的核心类库
于是进行向下委派,委派给扩展类加载器
步骤 5
扩展类加载器接到委派后,发现 plugin.jar 也不是自己负责加载的核心类库
于是进行向下委派,委派给系统类加载器
步骤 6
系统类加载器接到委派后,发现 plugin.jar 是自己负责加载的核心类库
于是进行加载,最后成功加载了 plugin.jar。
案例3:加载/jre/lib/plugin.jar
前提
/jre/lib/plugin.jar 是需要被系统类加载器加载的核心类库
虽然 plugin.jar 是系统类加载器负责加载的,但是要遵循向上委托的原则
因此在步骤 2 不能够实时加载,只能等待父加载器向下委派时加载。
加载流程图解释与说明
plugin.jar 的完整加载过程
6步加载核心类库 plugin.jar
步骤 1
plugin.jar 会先通过自定义类加载器(前提是实现了自定义类加载器)
自定义类加载器不会做处理,直接向上委托给系统类加载器
步骤2
系统类加载器接到委托后,也不做任何处理
直接向上委托给扩展类加载器
步骤 3
扩展类加载器接到委托后,也不做任何处理
直接向上委托给启动类加载器
步骤 4
启动类加载器接到委托后,发现 plugin.jar 不是自己负责加载的核心类库
于是进行向下委派,委派给扩展类加载器
步骤 5
扩展类加载器接到委派后,发现 plugin.jar 也不是自己负责加载的核心类库
于是进行向下委派,委派给系统类加载器;
步骤 6
系统类加载器接到委派后,发现 plugin.jar 是自己负责加载的核心类库
于是进行加载,最后成功加载了 plugin.jar。
JVM 类加载器分类
启动(Bootstrap)类加载器
定义
启动(Bootstrap)类加载器也称为引导类加载器
该加载器是用本地代码实现的类加载器
作用
它所加载的类库
绝大多数
出自 %JAVA_HOME%/lib 下面的核心类库
少部分
当然,还有其他少部分所需类库
这句话完完全全的体现出了启动(Bootstrap)类加载器存在的意义。
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
示例说明
设计思路
通过编写一个 main 函数,打印出通过启动(Bootstrap)类加载器加载的所有的类库信息
以证实启动(Bootstrap)类加载器加载的是 %JAVA_HOME%/lib 下边的核心类库。
注意事项
注意下 main 函数代码的第二行代码 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
这是通过 sun 公司提供的 Launcher 包获取 Bootstrap 类加载器下 ClassPath 下的所有的 URL。
详细代码
import java.net.URL;
public class LoaderDemo {
public static void main(String[] args) {
System.out.println("BootstrapClassLoader 的加载路径: ");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls)
System.out.println(url);
}
}
public class LoaderDemo {
public static void main(String[] args) {
System.out.println("BootstrapClassLoader 的加载路径: ");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls)
System.out.println(url);
}
}
结果验证
运行 main 函数。
此处运行结果所打印的类库的绝对路径为本人本机的安装路径,学习者应按照自己真实的JDK安装路径以及版本对号入座,此处仅为示例。
BootstrapClassLoader 的加载路径:
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/sunrsasign.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/classes
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/sunrsasign.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/classes
结果解析
运行结果中的前 7 个类库(不同JDK版本会有差异,此处JDK 1.8),都是出自lib下的核心类库。
【了解】但是对于最后一条加载信息却不是 lib 下的类库。
最后这条信息的加载 file:/D:/Programs/Java/jdk1.8.0_111/jre/classes。
这就是前文所提到的其他少部分的核心类库加载
学习者可以根据自己真实的安装位置打开 /jre 文件夹,看看是否存在 /classes 路径。
结果是 /classes 文件夹路径并不存在,除非进行特殊的参数创建才可以出现 /classes 路径。
启动类加载器(Bootstrap ClassLoader)
启动类加载器
启动类加载器(Bootstrap ClassLoader)
启动类加载器
C++ 实现,是虚拟机自身的一部分;
负责将存放在 <JRE_HOME>\lib 目录中的类库加载到虚拟机内存中
Bootstrap ClassLoader
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展(Extension)类加载器
定义
扩展类加载器是由ExtClassLoader实现的
Sun 公司提供的
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
作用
负责将两类类库加载到内存中
【重点】%JAVA_HOME%/lib/ext中的类库
对大多数的核心类库加载位置进行讨论
【了解】少数由系统变量 -Djava.ext.dir 指定位置中的类库
对于系统变量指定的类库,稍作了解即可
开发者可以直接使用标准扩展类加载器。
示例说明
详细代码
import java.net.URL;
import java.net.URLClassLoader;
public class LoaderDemo {
public static void main(String[] args) {
//取得扩展类加载器
URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
System.out.println(extClassLoader);
System.out.println("扩展类加载器 的加载路径: ");
URL[] urls = extClassLoader.getURLs();
for(URL url : urls)
System.out.println(url);
}
}
import java.net.URLClassLoader;
public class LoaderDemo {
public static void main(String[] args) {
//取得扩展类加载器
URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
System.out.println(extClassLoader);
System.out.println("扩展类加载器 的加载路径: ");
URL[] urls = extClassLoader.getURLs();
for(URL url : urls)
System.out.println(url);
}
}
结果验证
运行 main 函数。
扩展类加载器 的加载路径:
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar
结果解析
运行结果中所有的核心类库均来自 %JAVA_HOME%/lib/ext 的文件夹。
扩展类加载器
它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中
扩展类加载器(Extension ClassLoader)
Extention ClassLoader
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
扩展类加载器(Extension ClassLoader)
系统(System Application)类加载器
定义
系统类加载器是由AppClassLoader实现的
Sun 公司提供的
AppClassLoader(sun.misc.Launcher$AppClassLoader)
作用
负责将 用户类路径下的类库加载到内存中。
用户类路径
即当前类所在路径及其引用的第三方类库的路径
java -classpath或-Djava.class.path变量所指的目录
开发者可以直接使用系统类加载器。
系统类加载器加载的核心类库类型比较多
lib 下,未被(Bootstrap) 启动类加载器加载的类库
ext下,未被(Extension)扩展 类加载器加载的类库
其他类库
加载除了启动类加载器和扩展 类加载器所加载的其余的所有的核心类库。
示例说明
详细代码
import java.net.URL;
import java.net.URLClassLoader;
public class LoaderDemo {
public static void main(String[] args) {
//取得应用(系统)类加载器
URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
System.out.println("应用(系统)类加载器 的加载路径: ");
URL[] urls = appClassLoader.getURLs();
for(URL url : urls)
System.out.println(url);
}
}
import java.net.URLClassLoader;
public class LoaderDemo {
public static void main(String[] args) {
//取得应用(系统)类加载器
URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
System.out.println("应用(系统)类加载器 的加载路径: ");
URL[] urls = appClassLoader.getURLs();
for(URL url : urls)
System.out.println(url);
}
}
结果验证
运行 main 函数。
应用(系统)类加载器 的加载路径:
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/deploy.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/javaws.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfxswt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/management-agent.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/plugin.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/E:/IdeaWorkspace/LeeCode/target/classes/
file:/D:/Programs/IntelliJ%20IDEA%20Educational%20Edition%202019.3.1/lib/idea_rt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/deploy.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/javaws.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfxswt.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/management-agent.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/plugin.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/E:/IdeaWorkspace/LeeCode/target/classes/
file:/D:/Programs/IntelliJ%20IDEA%20Educational%20Edition%202019.3.1/lib/idea_rt.jar
结果解析
系统(System Application)类加载器加载的类库种类很多
除了之前两种类加载器加载的类库,其余必须的核心类库,都由系统类加载器加载。
应用程序类加载器
它负责加载用户类路径(ClassPath)上所指定的类库
应用程序类加载器(Application ClassLoader)
Appclass Loade
负责加载用户路径(classpath)上的类库。
应用程序类加载器(Application ClassLoader):
自定义类加载器
自定义类加载器(User ClassLoader)
用户根据需求自己定义的。也需要继承自ClassLoader.
图解类加载器
图解类构造器
Java类加载器
JVM提供了3种类加载器:
由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
通过继承java.lang.ClassLoader实现自定义的类加载器。
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,
类构造器<client>
初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<client>方法执行之前,父类的<client>方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。
注意以下几种情况不会执行类初始化:
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4. 通过类名获取Class对象,不会触发类的初始化。
5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
类加载方式
1、命令行启动应用时候由JVM初始化加载
main()
2、通过Class.forName()方法动态加载
class。forName
3、通过ClassLoader.loadClass()方法动态加载
ClassLoader。loadClass
类加载时机
遇到new,getStatic,putStatic,invokeStatic这四条指令
new一个对象时
调用一个类的静态方法
直接操作一个类的static 属性
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
ClassLoader加载到V1。V2不同进程如何实现?
Class 文件结构
整体结构
魔数->次版本号->主版本号->常量池计数器->常量池
类索引->父类索引->接口索引计数器->接口索引集合
字段表计数器->字段表->方法表计数器->方法表->属性表计数器->属性表
魔数、主次版本号与常量池
前言
Class 文件的数据类型,概念性的知识【基础】
Class 文件结构介绍【次重点】
魔数的定义及所占字节空间【重点】
次版本号与主版本号的定义及对照表,次版本号与主版本号为【重点】,版本号对照表【了解】
常量池计数器与常量池的定义及意义【重点】
Class 文件结构介绍【次重点】
魔数的定义及所占字节空间【重点】
次版本号与主版本号的定义及对照表,次版本号与主版本号为【重点】,版本号对照表【了解】
常量池计数器与常量池的定义及意义【重点】
Class文件两种数据类型:无符号数和表
根据JVM规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据
这种伪结构中只有两种数据类型:无符号数和表。
无符号数
无符号数属于基本的数据类型
以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节;
无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值;
魔数,次版本号,主版本号以及常量池计数器皆为无符号数类型
表
表是由多个无符号数或者其他表作为数据项,构成的复合数据类型
所有表都习惯性地,以“info”结尾。
表用于描述有层次关系的复合结构的数据
整个 Class 文件本质上就是一张表。
常量池为表类型
Class 文件结构
一组以byte 字节为基础单位的二进制流
8位bit = 1 byte 字节
Class 文件的字节码示意图
绿色框部分
Class 文件的标准模样
左侧
软件本身提供的辅助信息
记录当前行前面总共有多少个 byte (或者说多少个 u1 )
用于快速定位数据,通过数据偏移量的方式。
右侧
直接以编辑器打开 Class 文件的样子
显示为乱码
Tips:使用普通的编辑器打开 Class 文件会看到乱码
如果想要像上图一样观察 Class 文件的话,需要下载专门的编辑器。WinHex 就具备这个功能
魔数
唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
魔数
定义
(Magic Number)
每个 Class 文件的头 4 个字节(u4)
所有 Class 文件,魔数均为 0xCAFEBABE。
从Class文件结构图中,可见Class文件的开头确实是CAFEBABE。
如果使用WinHex打开任意的一个Class文件,开头也必然是CAFEBABE。
作用
确定这个文件是否为一个能被虚拟机接收的 Class 文件
无符号数结构示意图
魔数是无符号数类型的数据
对于无符号数,通常以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节。
魔数开头为 CAFEBABE,占用 4 个字节
无符号数表示为 u4,那么其中 CA,FE,BA,BE分别占用 1 个字节,无符号数表示为 u1。
魔数的无符号数结构示意图
版本号
次版本号与主版本号
为什么先次后主?
对于Class 文件结构,第一部分为 u4 的魔数,魔数后边紧跟的就是 u2 的次版本号,次版本号后边才是 u2 的主版本号
此处需要特别注意,从结构上来说,次版本号在前,主版本号在后。
定义
次版本号与主版本号共同标识了所使用的的 JDK 版本
如 JDK 1.8.0 版本
次版本号为 u2 大小,用字节码表示为 00 00
主版本号也是 u2 大小,用字节码表示为 00 34。
次版本号:JDK 版本的小版本号
主版本号:JDK 版本的大版本号
如果 Class 文件的开头 8 个字节分别为 CA FE BA BE 00 00 00 34
这是一个 JVM 可识别的 Class 文件,且使用的 JDK 1.8.0的版本进行的编译
前4个字节魔数为 CA FE BA BE 符合标准
后4 个字节 00 00 00 34 为 JDK 1.8.0的版本。
无符号数结构示意图
次版本号与主版本号也是无符号数类型的数据
次版本号与主版本号,无符号数结构示意图
次版本号与主版本号分别占用 2 个字节,无符号数表示为 u2。
版本号对照表(JDK几个版本的对照表)
JDK 版本(16进制字节码)
1.8.0(00 00 00 34)
1.7.0(00 00 00 33)
1.6.0(00 00 00 32)
常量池
字面量
符号引用
常量池计数器与常量池
按照 Class 文件的结构顺序
主版本号后边紧跟的是常量池计数器
常量池计数器后边紧跟的是常量池
常量池计数器
定义
记录常量池中的常量的数量。
由于常量池中的常数的数量是不固定的,所以在常量池的入口放置了一个 u2 类型的数据,来代表常量池容器记数值
常量池容器记数值(constant_pool_count)
常量池计数器也是无符号数类型数据。
无符号数结构示意图
无符号数结构示意图
常量池
定义
Class 文件中的资源仓库
它是 Class 文件结构中与其他项目关联最多的数据类型
它是占用Class文件空间最多的数据项目之一
它还是 Class 文件中,第一个出现的表类型数据项目。
表结构示意图
常量池表结构示意图
cp_info类型
cp_info 又可细分为 14 种结构类型。下表中第二列所说的标志,是指每一种数据类型的标记值,此处做简单了解即可。
cp_info 又可细分为 14 种结构类型
常量池中存储的数据
常量池中主要存放着两种常量,字面量(Literal)和符号引用(Synbolic References)。
字面量(Literal)
文本字符
声明为 final 的常量值
基础数据类型的值
符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
访问标志与索引
前言
Class 文件结构中的访问标志与索引,其中索引又细分为类索引、父类索引、接口索引计数器、接口索引集合四个知识点。
访问标志的定义及意义,以及结构示意图【重点】
访问标志的标记类型及标记值对应表【次重点】
类索引、父类索引、接口索引计数器、接口索引集合的定义及示意图【重点】
访问标志的标记类型及标记值对应表【次重点】
类索引、父类索引、接口索引计数器、接口索引集合的定义及示意图【重点】
访问标志
用于识别一些类或接口层次的访问信息
是否final
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
访问标志
定义
(access_flags)
在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flags)
访问标志也是无符号数类型的数据,
既然访问标志占用了 2 个字节,那么访问标志的占用空间也可用 u2 来表示。
访问标志占用了 2 个字节,类似于常量池计数器,因为常量池计数器也是占用了 2 个字节,均为 u2 大小。
作用
识别一些类或接口层次的访问信息。
无符号数结构示意图
访问标志的无符号数结构示意图
访问标志类型对应表
假设需要访问一个接口,那么此时访问标志 ACC_INTERFACE 的值为 true,标志对应的值为 0x0200。
这样 JVM 虚拟机在处理访问时,就能够做到有据可依。
访问标志的不同类型,以及不同类型的访问标志的意义
类索引,父类索引,接口索引集合
这三项数据主要用于确定这个类的继承关系。
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类父类的全限定名
接口索引
描述这个类实现了哪些接口
索引
类索引+父类索引
类索引定义
this_class
都是一个 u2 大小的数据。
确定当前类的全限定名。
父类索引定义
super_class
都是一个 u2 大小的数据。
确定当前类的父类的全限定名。
由于 Java 单继承的原则,父类只可能有一个;
由于 Object 是所有其他类的基类,所以除了 Object 类没有父类以外,其余所有类的 super_class 都不为空。
无符号数结构示意图
类索引是紧跟在访问标志之后的结构,类索引后边紧跟的结构是父类索引。
由于类索引与父类索引关系非常紧密,都是描述的当前类以及当前类的父类的全限定名,所以此处我们将二者放在一起进行讲解。
类索引+父类索引 的 无符号数结构示意图
接口索引计数器与接口索引集合
父类索引后边紧跟的是接口索引计数器,接口索引计数器后边紧跟的是接口索引集合。
接口索引计数器定义
代表了接口索引集合中接口的数量
类似于常量池计数器和常量池的关系,接口索引计数器记录的是接口索引集合中接口索引的数量。
接口索引集合定义
按照当前类 implements(或当前接口extends)的接口的顺序,从左到右依次排列在接口索引集合中
此部分集合称为接口索引集合。
无符号数结构示意图
对于常量池计数器和常量池,一个是无符号数类型,一个是表类型。
相比而言,接口索引计数器和接口索引集合,皆为无符号数类型
接口索引计数器和接口索引集合均为无符号数类型结构
从图中可以看出,接口索引计数器占用了 2 个字节,为 u2 大小,接口索引集合中的每一个接口元素占用了 2 个字节大小,也为 u2 大小。
字段表、方法表与属性表
前言
介绍 Class 文件结构中的字段表、方法表与属性表。
标题3种结构,但实际6 种结构?
3种结构
字段表、方法表与属性表
6种结构
字段表计数器和字段表
方法表计数器和方法表
属性表计数器与属性表
因为每一种表结构,都需要有计数器进行计数。
字段表计数器和字段表的定义及意义,以及结构示意图【重点】
方法表计数器和方法表的定义及意义,以及结构示意图【重点】
属性表计数器和属性表的定义及意义,以及结构示意图【次重点】由于JVM 对属性表没有特别规范的限制,此处了解其定义及结构即可。
方法表计数器和方法表的定义及意义,以及结构示意图【重点】
属性表计数器和属性表的定义及意义,以及结构示意图【次重点】由于JVM 对属性表没有特别规范的限制,此处了解其定义及结构即可。
Class 文件结构的最后部分
每一部分的结构顺序与讲解的顺序完全一样,把所有的示意图进行拼接,就能够形成一个完成的 Class 文件结构示意图。
字段表集合
表结构
访问标志
名称索引
描述符索引
属性表集合
字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
简单来说,字段表集合存储字段的修饰符+名称
变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
字段表计数器和字段表
字段表计数器
fields_count
记录字段表中字段的数量,为无符号数类型。
与字段表不可分割
一个无符号数结构类型的数据,u2 大小。
字段表计数器无符号数结构示意图
与其他计数器一样,字段表计数器(fields_count)是一个无符号数结构类型的数据,u2 大小。
字段表计数器无符号数结构示意图
字段表
fields
用于描述接口或者类中声明的变量。
字段表为表类型结构。
字段表中存储的是全局标量,不存储局部变量。
字段表是一个表结构的类型数据,同 Class 文件的表结构类型数据为常量池。
字段表-表结构类型示意图
字段表的表结构示意图
上图所示的一个 field_info 就代表了一个变量。
为了表示一个变量,需要知道这个变量的修饰符,如 public
还需要知道这个变量的变量名称,因此一个 field_info 中存储了很多特征值,所有的特征值综合起来就完整的描述了一个变量。
字段
field
包括类级变量(即静态变量)以及实例变量(即:非静态变量)
字段(field)包括类级变量(即静态变量)以及实例变量(即:非静态变量)
但不包括在方法内部声明的局部变量。
方法表集合
访问标志
名称索引
描述符索引
属性表集合
Java代码经过编译器编译为字节码之后,存储在方法属性表集合中一个名叫“Code”的属性中
方法表计数器和方法表
方法表
定义
方法表是一个表结构的类型数据,与前文所讲解的字段表结构一样。
存储了当前类或者当前接口中的 public 方法,protected 方法,default 方法,private 方法等。
方法表为表结构类型。
Class文件是通过Java文件编译而来的,如果文件中有方法,就会将方法的信息存储到方法表,并通过方法表计数器进行方法的计数。
方法表-表结构类型示意图
方法表-表结构类型示意图
方法表是一个表结构的类型数据
一个method_info中会存储很多方法的特征值
通过如下示例方法进行举例:
public String get(String name) {
return "";
}
return "";
}
对于get 方法,method_info 中会存储如下信息:
方法的修饰符 public
参考访问标记符 access_flag 吗?
access_flag 对应表中有一个 ACC_PUBLIC 就代表了 public 修饰符
他对应的值为 0x0001,此处的标记也需要使用到这个表,都是通用的哦;
access_flag 对应表中有一个 ACC_PUBLIC 就代表了 public 修饰符
他对应的值为 0x0001,此处的标记也需要使用到这个表,都是通用的哦;
方法名称
get 方法
方法的参数
String name
方法的返回值类型
String 类型的返回值
通过如上方法特征值的共同修饰,完成了一个 method_info 的存储,也就完成了一个方法的存储。
方法计数器
定义
methods_count
与方法表不可分割
记录方法表中字段的数量,为无符号数类型。
方法表计数器无符号数结构示意图
方法表计数器无符号数结构示意图
属性表集合
在 Class 文件、字段表、方法表都可以携带子集的属性表集合,以用于描述某些场景专有的信息。
属性表计数器与属性表
定义
方法表后边紧跟的是属性表计数器,属性表计数器后边紧跟的结构为属性表。至此,Class 文件的全部结构就讲解完了。
回顾之前的知识,Class 文件结构是以魔数开头,以属性表结尾的。
属性表计数器
定义
(attributes_count)
属性表计数器与属性表不可分割
记录属性表中属性的数量,为无符号数类型。
属性表计数器无符号数结构示意图
与其他计数器一样,属性表计数器(attributes_count)是一个无符号数结构类型的数据,u2 大小。
属性表计数器无符号数结构示意图
属性表
定义
(attributes)
属性表与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同
属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复
任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不能识别的属性。
属性表作为 Class 文件的一个结构,是非常灵活的,且没有明确的长度大小规定。
属性表-表结构类型示意图
属性表也是一个表结构类型,与字段表、方法表结构类似,但是属性表没有固定的长度和顺序限制
子主题
属性表的两大特点
限制宽松,无顺序长度要求
开发者可以自己向属性表中添加不重复的属性
属性表计数器无符号数结构示意图
与其他计数器一样,属性表计数器(attributes_count)是一个无符号数结构类型的数据,u2 大小。
属性表计数器无符号数结构示意图
属性表- 表结构类型示意图
属性表也是一个表结构类型,与字段表、方法表结构类似,但是属性表没有固定的长度和顺序限制,此处我们了解下其结构即可。
属性表-表结构类型示意图
字节码文件
Class文件时一组以字节为基础单位的二进制流,各个数据严格按照顺序排列紧凑的排列在文件中,中间没有任何分隔符
组成Class文件的两种数据类型
无符号数
分别以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数
无符号数用来描述
数字
索引引用
数量值
按照UTF-8编码构成的字符串值
表
是由多个无符号数或其他表作为数据项构成的复合数据类型
所有表命名都习惯性地以"_info"结尾
整个Class文件本质上也可以视作是一张表
Class文件结构
魔数
用来确定这个文件是否为一个能被虚拟机接受的Class文件
四个字节
CAFEBABE
版本号
用来确定JDK的版本
四个字节
5-6字节是次版本号,7-8字节是主版本号
常量池
常量池容量计数器
四个字节
存放着常量池中常量的个数
从1开始默认保留下标为0的空位置
主要存放两大类常量
字面量
文本字符串
被声明为final的常量值
符号引用
被模块到处或开放的包
类和接口的权限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
对象
对象的创建
根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出ClassNotFoundException;
检查符号引用对应的类是否加载过
如果没有,则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
从堆中划分一块对应大小的内存空间给该对象
指针碰撞
java堆内存空间规整的情况下使用
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
空闲列表
java堆空间不规整的情况下使用
对象中的成员变量赋上初始值
设置对象头信息
调用对象的构造函数进行初始化
对象实例化
对象创建
创建对象的方式
创建对象的步骤
Java对象内存布局
对象头(Header)
实际数据(Instance Data)
Padding
对象的内存布局
对象头
Mark Word
对象的hashCode
CG年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC标志
Class Metadata Address
指向对象实例的指针
对象的实例数据
对齐填充
对象的访问方式
指针
reference中存储的直接就是对象地址
句柄
java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息
两种方式的比较
使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot虚拟机使用的是直接指针访问的方式。
对象的生命周期
综上所述,可以通过下面的流程来对对象的生命周期做一个总结
图中用红色标明的区域表示对象处于强可达阶段。
对象被创建并初始化,对象在运行时被使用,然后离开对象的作用域,对象会变成不可达并会被垃圾收集器回收。
JDK 1.2介绍了java.lang.ref包
JDK 1.2介绍了java.lang.ref包
对象的生命周期有四个阶段
强可达(Strongly Reachable)
软可达(Soft Reachable)
弱可达(Weak Reachable)
幻象可达(Phantom Reachable)
如果只讨论符合垃圾回收条件的对象,那么只有三个阶段
软可达(Soft Reachable)
只能通过软引用才能访问的状态
对象是由Soft Reference引用的对象,并且没有强引用的对象。
用来描述一些还有用但是非必须的对象。
垃圾收集器会尽可能长时间的保留软引用的对象, 但是会在发生OutOfMemoryError之前, 回收软引用的对象。
如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出异常
弱可达(Weak Reachable)
对象是Weak Reference引用的对象。
垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象,
幻象可达(Phantom Reachable)
由Phantom Reference引用的对象
没有强、软、弱引用进行关联, 并且已经被finalize过了, 只有幻象引用指向这个对象的时候。
String Pool
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存、都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种数据类型的常量池都是系统调节的。
String类型的常量池比较特殊。它的主要使用方法有两种。
String类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接储存在常量池中
比如:String info = “hello String”;
比如:String info = “hello String”;
如果不是双引号声明的String对象,可以使用String提供的intern()方法
String Pool是一个固定大小的Hashtable
JDK6以前 StringTable的默认长度是1009。也可以无限制的自定义大小
JDK7以后StringTable的长度默认值是60013,也可以自定义设置大小,但是设置范围的限制(1009~60013)
存放地点
JDK7以前
字符串常量池存放在永久代(方法区)
JDK7时
字符串常量池调整到了Java堆当中
JDK8以后
字符串常量池依然存放在Java堆当中,此时已经取消了永久代的概念,更改为了元空间
内存原理
1.常量与常量的拼接结果在常量池,原理是编译器优化
在编译字节码时就将它自动拼接为一个常量
2. 常量池中不会存在相同内容的常量
3.如果拼接的元素其中一个为变量,则底层会创建StringBuilder的append方法对其进行拼接,最后调用toString()放回
toString()方法底层,虽然通过new String()方式放回String,但是它不会在常量池中创建对应的值
3. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
JDK6以前
intern()的工作流程
1.查看常量池中是否有这相同字符串的变量
2.如果有则将该对象的引用放回,如果没有则在常量池中创建该对象后再返回该引用(注意此时,会有两个对象,一在堆中,一个在常量池中)
JDK7以后
intern()的工作流程
1.查看常量池中是否有这相同字符串的变量
2.如果有则返回对象该引用,如果没有则将堆中的对象引用保存在字符串常量池中(此时只有一个对象,常量池中引用的是堆中的对象)
4. 通过new String("a")的方式创建对象,实际会创建2个对象,一个在堆中,一个在字符串常量池(实际上常量池也在堆中)
4.通过String a = "a";这样以字面量的方式直接赋值,实际只会创建1个在字符串常量池中的对象
6.调用 toString()虽然底层是通过new String()的方式创建对象 但是不会在字符串常量池中创建该常量
7. 如果在String变量前面添加有final关键字则编译器 在拼接时会进行优化,视为一个字符串(参考第1点)
执行引擎
概念
在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果
在JVM中的最重要两部分
解释器
即时编译器(JIT)
对“编译期”的解释
Java语言的“编译器”其实时一段“不确定”、“模糊”的操作过程,因为它可能时指前端编译器(或者叫“编译器的前端”)把 .java文件转变成 .class文件过程;
也可能是指虚拟机的“后端运行期编译器”(JITb编译器)把字节码转换成机器码的过程
还可能是指使用静态提前编译器(AOT编译器)直接把.java文件编译成本地机器码的过程
解释器
概念
解释器就像一个编译器,它不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。
通俗
把程序源代码一行一行的读懂然后执行,发生在运行时,产物是「运行结果」。
JVM中的作用
在JVM中解释器就是将Java字节码一句一句翻译成机器码去执行
解释型语言
Python
JavaScript
PHP等
优缺点
优点
1.解释型语言启动速度快,因为它无需编译可以直接解释
2.易于跨平台,在运行时可根据不同的平台进行解释执行
缺点
1.后续的执行效率不高,因为如果运行重复的代码需要再次解释
2.整体来说执行效率低
编译器
概念
编译器是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。ge)机器代码的等价程序。
把整个程序源代码翻译成另外一种代码,然后等待被执行,发生在运行之前,产物是「另一份代码」。
编译型语言
C
C++等
优缺点
优点
1.后续执行效率高,已经在一开始已经进行了编译,所以在运行到重复的代码时可以直接执行编译后的代码,无需再次编译
2.总体来说执行效率更高
缺点
1.编译型语言启动比较慢,因为启动时,需要经行一个编译的过程(编译的结果就是目标代码,就是上诉的所谓「另一份代码」)
2.启动时的编译是需要根据硬件,以及平台相关性来决定的
JVM中的编译器分类
前端编译器
JAVAC
Eclipse JDT中的增量式编译器(ECJ)
JIT编译器
HotSpot VM的C1,C2的编译器
AOT编译器
GNU Compiler for the Java(GCJ)、Excelsior JET
Java是半解释半编译的语言
这得益于在JVM中能够同时拥有,编译器(即时编译器),以及解释器
在程序启动的初期,JVM会通过解释器进行翻译执行代码,然后,如果后续有重复的代码(又称作,热点代码),JVM就会使用及时编译器进行编译,然后保存编译的结果下次再需要执行者重复的代码时,就不需要通过解释器逐句翻译执行了。
在编译的同时,JVM还能堆源代码的一些部分进行优化这样可以进一步的提高执行效率
不一定所有的JVM实现都是半解释半编译的,如:JRocket就没有解释器
(Hotspot)即时编译器(JIT)
在HotSpot VM中内嵌又两个JIT编译器,分别为Client Compiler和Server Compile,但大多数情况下我们简称为C1编译器和C2编译器。
默认情况下,我们64位处理器默认开启的时Server模式
使用
指令-client:指定Java虚拟机运行在Client模式下,并使用C1编译器
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度
指令 -servcer:指定Java虚拟机运在Server模式下,并使用C2编译器
C2进行耗时比较长的优化,可以激进优化。代码的执行效率要更高
AOT编译器
JDK9引入了AOT编译器(静态提前编译器)
它可以在程序运行过程之前,直接将Java类文件转换为机器码,并存放至生成的动态共享库之中
优缺点
优点
Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待JIT编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验。
缺点
破坏了java“一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包
降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知
编译环境的选择
在HotSpot VM中开发人员可以根据具体的引用场景,通过命令显示地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行
-Xint:完全采用解释器模式执行程序
-Xcomp:完全采用即使编译器模式执行程序。如果即使编译器出现问题,解释器还是会介入执行
-Xminxed:采用解释器+即时编译器的混合模式共同执行程序
其他知识
OSGI(动态模型系统)
OSGi(Open Service Gateway Initiative)
是面向Java的动态模型系统,是Java动态化模块化系统的一系列规范。
动态改变构造
OSGi服务平台提供在多种网络设备上无需重启的动态改变构造的功能。
为了最小化耦合度和促使这些耦合度可管理,OSGi技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
模块化编程与热插拔
OSGi旨在为实现Java程序的模块化编程提供基础条件,基于OSGi的程序很可能可以实现模块级的热插拔功能,
当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。
OSGi描绘了一个很美好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。
但并非所有的应用都适合采用OSGi作为基础架构,它在提供强大功能同时,也引入了额外的复杂度,因为它不遵守了类加载的双亲委托模型。
动态绑定
指的是在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
编译阶段,根据引用本身的类型(Father)在方法表中查找匹配的方法,如果存在则编译通过
运行阶段,根据实例变量的类型(Son)在方法表中查找匹配的方法,如果实例变量重写了方法,则调用重写的方法,否则调用父类方法
以 Father ft=new Son();ft.say();为例
表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,则对应表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面。
参数传递
值传递
引用传递
Java 在参数传递的时候,实际上是传递的当前引用的一个拷贝
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
运行过程
Java 源文件,通过编译器,能够生成相应的 .Class 文件,也就是字节码文件
① Java 源文件 编译器 字节码文件
而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。
② 字节码文件 ——-->JVM 机器码
线程
这里所说的线程指程序执行过程中的一个线程实体。
JVM 允许一个应用并发执行多个线程。
Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系
当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程 。
当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。
Java 线程结束,原生线程随之被回收。
当线程结束时,会释放原生线程和Java 线程的所有资源。
操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。
Hotspot JVM 后台运行的系统线程
虚拟机线程
这个线程等待 JVM 到达安全点操作出现。
这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。
这些操作的类型有
stop-the-world 垃圾回收、
线程栈 dump
线程暂停、
线程偏向锁(biased locking)解除。
(VM thread)
周期性任务线程
这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC 线程
这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程
这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程
这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。
内存相关
设计方案
高并发下单
订单生成
逃逸分析
STW
StackOverflowError
线程请求的栈深度要大于虚拟机所允许的深度时出现的错误。
OutOfMemoryError
oom种类
垃圾收集器无法释放足够的内存空间, 并且JVM无法扩展堆, 则会出现, 应用程序通常在这之后崩溃
内存分配
对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
最典型的大对象是那种很长的字符串以及数组。
避免在 Eden 区和 Survivor 区之间的大量内存复制。
长期存活对象进入老年区
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1,对象在Survivor区中每熬过一次 Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)_时,就会被晋升到老年代中。
对象年龄动态判定
如果在 Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
内存回收
Minor GC
特点
发生在新生代上,发生的较频繁,执行速度较快
触发条件
Eden区空间不足
空间分配担保
Full GC
特点
发生在老年代上,较少发生,执行速度较慢
触发条件
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC
内存泄漏
Java中内存泄漏是什么意思?
不再会被使用的对象内存不能被回收就是内存泄漏。
内存泄露
help dump
生产机 dump
mat
jmap
-helpdump
程序在申请内存后,无法释放已申请的内存空间
原因
长生命周期的对象持有短生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为null
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏
解决方法
将内部类定义为static
用static的变量引用匿名内部类的实例
或将匿名内部类的实例化操作放到外部类的静态方法中
Hash值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
JVM内存溢出
堆
OutOfMemoryError:Java heap space
最大堆、最小堆、新生代:-Xms512M -Xmx512M -Xmn128M
虚拟机栈/本地方法栈
StackOverflowError
每个线程可使用内存:-Xss256K
OutOfMemoryError:unable to create new native thread
方法区
OutOfMemoryError:PermGen space
JVM设置最小最大:-XX:PermSize=64M -XX:MaxPermSize=128M
直接内存
at sun.misc.Unsafe.allocateMemory(Native Method)
元空间(java1.8)
java.lang.OutOfMemoryError: Metadata space
-XX:MetaspaceSize:128M -XX:MaxMetaspaceSize=128M
GC overhead limit exceeded
内存溢出的前兆:
1、每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
2、FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
3、年老代的内存越来越大并且每次FullGC后年老代没有内存被释放之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。
程序在申请内存时,没有足够的内存空间
内存溢出的构造方式
堆溢出
OutOfMemoryError:不断创建对象
栈溢出
StackOverflowError: 增大本地变量表,例如不合理的递归
OutOfMemoryError:不断建立线程
方法区和运行时常量池溢出
OutOfMemoryError:通过String.intern()方法不断向常量池中添加常量,例如String.valueOf(i++).intern()
本机内存直接溢出
堆
OutOfMemoryError:Java heap space
最大堆、最小堆、新生代:-Xms512M -Xmx512M -Xmn128M
虚拟机栈/本地方法栈
StackOverflowError
每个线程可使用内存:-Xss256K
OutOfMemoryError:unable to create new native thread
方法区
OutOfMemoryError:PermGen space
JVM设置最小最大:-XX:PermSize=64M -XX:MaxPermSize=128M
直接内存
at sun.misc.Unsafe.allocateMemory(Native Method)
元空间(java1.8)
java.lang.OutOfMemoryError: Metadata space
-XX:MetaspaceSize:128M -XX:MaxMetaspaceSize=128M
子主题 6
GC overhead limit exceeded
CPU100%
topc -c
top -Hp pid
jstack
进制转换
cat
full gc
老年代写满
system。gc
持久代空间不足
常见问题
JVM堆和栈的区别
子主题
0 条评论
下一页