1.java虚拟机执行java程序的过程
Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),
.class 文件 由JVM中的类加载器加载各个类的字节码文件,
加载完毕之后,交由JVM执行引擎执行。
1.1 JVM生命周期
- 启动
启动一个Java程序时,一个JVM实例就产生了,
任何一个拥有public static void main(String[] args)* 函数的class都可以作为JVM实例运行的起点。
- 运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
- 消亡
当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
1.2 JVM体系结构
1.2.1 类装载器(ClassLoader)(用来装载.class文件)
a. 概述
-
JVM类加载机制:是虚拟机把描述类的数据从
Class
文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型的过程。 - 特性:运行期类加载。即在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性。
- java 动态扩展特性是依赖于运行期动态加载和动态连接实现的
注意:
a. 实际情况下,每个class 文件都可能代表一个类或一个接口
b. class 文件指一串二进制字节流
b. 类加载全过程
类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括7阶段:
注意:
- 『加载』->『验证』->『准备』->『初始化』->『卸载』这5个阶段的顺序是确定的,而『解析』可能为了支持Java语言的运行时绑定会在『初始化』后才开始。
- 上述阶段通常都是互相交叉地混合式进行的,比如会在一个阶段执行的过程中调用、激活另外一个阶段。
- 有且只有5种情况必须对类立即“初始化”:
a. 遇到new、getstatic、 putstatic和invokestatic 4条指令时,对应场景是:
(1) new 实例化对象
(2) 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
(3) 调用类的静态方法
b. 使用java.lang.reflect
包方法对类进行反射调用的时候,若类没进行初始化,先出发其初始化
c. 初始化类时,若发现父类没初始化,先对其父类初始化
d. 虚拟机启动时,初始化主类(含main()
方法的类)
e. 如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、 REF_putStatic、 REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
类加载过程:
- 加载
c. 类加载器&双亲委派模型
1.2.2 执行引擎(执行字节码,或者执行本地方法)
1.2.3 运行时数据区(方法区()、堆、java栈、PC寄存器、本地方法栈)P 3 内存模型
- 方法区(线程共享)(又称,永久代)
存储已被虚拟机加载的类相关信息、常量、静态变量、即时编译器编译后的代码等数据。(存储程序中永远不变的或唯一的内容)
方法区异常:
- OutOfMemoryError: 方法区无法满足内存需求时
- 运行时常量池 是方法区的一部分
存放编译器生成的各种字面量和符号引用,将在类加载后进入方法区的运行时常量池存放。
- 堆(线程共享)
存放对象实例和数组
是Java 虚拟机所管理的内存中最大的一块; 是垃圾回收器 管理的主要区域
在虚拟机启动时创建
可使用物理上不连续的内存空间,逻辑相连即可
扩容是通过-Xmx和-Xms控制
堆异常:
- OutOfMemoryError: 虚拟机栈扩容时不能申请到足够内存空间
- 虚拟机栈(线程私有)
存储局部变量表、操作数栈、动态链接、方法出口等信息
栈由系统自动分配,速度快,连续内存空间
描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame )。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈中两种异常:
-StackOveflowError: 线程请求栈深度大于虚拟机栈深度
- OutOfMemoryError: 虚拟机栈扩容时不能申请到足够内存空间
- 本地方法栈(线程私有)———也会抛出上面两种异常
虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。
- 程序计数器(线程私有)
是当前线程所执行的字节码的行号指示器
如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址
如果线程正在执行的是一个Native方法,那么计数器的值则为空
例子
**注
- 基本数据类型赋值,引用数据类型赋对象地址
阅读:JVM内存溢出详解
虚拟机对象
1)对象创建(new)
- 虚拟机检查new指令参数是否在常量池中定位到类的符号引用,检查符号引用代表的类是否被加载、解析、初始化过。没有, 进行类加载
- JVM为新对象分配内存。
a.大小由类加载可知
b. java堆内存是否规整,决定了java堆内存分配方式
- 指针碰撞:内存规整(空闲放一边,使用的放一边,中间放指针作为分界点指示器。)
(内存分配即指针向空闲空间挪动对象大小的距离)- 空闲列表: 内存不规整(空闲和使用的内存交错, 维护列表,记录哪些空闲)
分配是从列表中找到足够大空间
规整 由垃圾回收器是否带有压缩整理的功能决定- 分配内存空间初始化为0
- jvm对对象进行设置(对象是哪个类实例、对象哈希码等)
2)对象的内存布局(3块区域)
- 对象头(2部分信息)
a. Mark Word:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
被设计出非固定数据结构->在极小空间存储尽量多信息
b.类型指针:确定对象的所属类(对象是哪个类的实例)- 实例数据
存储真正的有效信息,是程序代码中定义的各种类型的字段内容。存储顺序会受虚拟机分配策略参数和字段在Java源码中定义顺序这两个因素影响- 对齐填充:占位符,帮助补全未对齐的对象实例数据部分(8字节的倍数),非必需。
3)对象的访问定位
java 程序通过栈上的reference数据访问堆上的具体对象(2种方式)
- 句柄
Java堆中划分出一块内存来作为句柄池 ,** reference存储的是对象的句柄地址**,在句柄中包含了对象实例数据与类型数据各自的具体地址信息。
通过句柄访问对象好处:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问:
在Java堆对象的布局中考虑如何放置访问类型数据的相关信息,reference存储的直接就是对象地址。
直接指针访问对象好处:速度更快,节省了一次指针定位的时间开销。
2. Java多线程
Java 虚拟机多线程是通过 线程轮流切换 并分配处理器执行时间 实现的
3. 垃圾回收器
JVM内存模型中的运行时数据区的5部分:
- 虚拟机栈、程序计数器、本地方法栈:随线程生死
- 方法区、堆:分配和回收都是动态的,是GC管理的部分
-
GC功能:
识别并且丢弃应用不再使用的对象来释放和重用资源
3.1 对象判活(2种方法)
3.1.1 引用计数算法:
-
方法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 -
问题:
难以解决对象间相互循环引用的问题
3.1.2 可达性分析算法:
-
方法:
通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。 - 可做GC Roots的对象:
- 虚拟机栈中引用的对象,主要是指栈帧中的本地变量
- 本地方法栈中Native方法引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
引用类型(4种)
- 强引用(StrongReference)
- "Object obj = new Object()" 这类引用
- 具有强引用的对象不会被GC;
- 即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。
- 软引用(SoftReference)
- 只具有软引用的对象,内存空间不足的时候被GC,如果回收之后内存仍不足,才会抛出OOM异常;
- 软引用常用于描述有用但并非必需的对象,比如实现内存敏感的高速缓存
- 弱引用(WeakReference)
- 只被弱引用关联的对象,GC时,无论当前内存是否足够都会被GC;
- 强度比软引用更弱,常用于描述非必需对象。**
- 虚引用(PhantomReference)
- 仅持有虚引用的对象,在任何时候都可能被GC
- 必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
对象“判刑”过程
达性分析算法中, 被判定不可达的对象还未真的判『死刑』
至少要经历两次标记过程:
- 第一次标记:若对象与GC Roots 没有相连的引用链,且进行第一次筛选
- 筛选条件: 判断对象是否有必要执行finalize()方法;若被判定为有必要执行finalize()方法,之后还会对对象再进行一次筛选,如果对象能在finalize()中重新与引用链上的任何一个对象建立关联,将被移除出“即将回收”的集合。
- 注:任何一个对象的finalize()方法只能被系统自动调用一次
回收方法区(2部分)
- 废弃常量
与回收Java堆中的对象的GC很类似,即在任何地方都未被引用的常量会被GC。- 无用的类: 满足以下三个条件才会被GC:
- 该类所有的实例都已被回收,即Java堆中不存在该类的任何实例;
- 加载该类的
ClassLoader
已经被回收- 该类对应的
java.lang.Class
对象没在任何地方被引用,即无法在任何地方通过反射访问该类的方法。
3.2 垃圾收集算法
根据对象存活周期的不同, 将java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
- 新生代: 大批对象死去,只有少量存活。使用『复制算法』,只需复制少量存活对象即可。
- 老年代: 对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。
3.2.1 标记—清理算法
- 思想:
a.标记需要回收的对象
b. 统一回收被标记的对象- 不足:
a. 效率问题: 清除和标记效率不高
b. 空间问题:标记清除后会产生大量不连续的内存碎片
3.2.2 复制算法
- 思想:
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。- 优点:
每次都是对整个半区进行内存回收,无需考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。- 不足:
a. 每次可使用的内存缩小为原来的一半,内存使用率低。
b. 存活率较高就需要进行较多复制操作,效率变低
复制算法
扩展
新生代中的对象98%是“朝生夕死”的,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
3.2.3 标记-整理算法(存活率高适用)
- 思想:
『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。- 优点:即没有浪费50%的空间,又不存在空间碎片问题,性价比较高
一般情况下,老年代会选择标记-整理算法。
标记-整理算法
补充
JVM 将堆内存划分为 Eden(年轻代)、Survivor(老年代) 和 Tenured/Old(持久代)空间。
年轻代
所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。老年代
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。持久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
堆内存划分
堆内存三个空间分别对应 三个GC: Minor GC、Major GC和Full GC
- Minor GC:
用于清理年轻代区域。Eden区满了触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中
(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)- Major GC
用于清理老年代区域。- Full GC:
用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
Old区满了,则会触发一个一次完整地垃圾回收(FullGC)
3.3 HotSpot 算法实现
3.3.1 枚举根节点(找到GC Roots)
主流Java虚拟机使用的都是准确式GC,在执行系统停顿之后无需检查所有执行上下文和全局的引用位置,而是通过一些办法直接获取到存放对象引用的地方,在HotSpot中是通过一组称为OopMap的数据结构来实现的,完成类加载后会计算出对象某偏移量上某类型数据、JIT编译时会在特定的位置记录栈和寄存器中是引用的位置。这样GC在扫描时就可直接得知这些信息,并快速准确地完成GC Roots的枚举