JVM的初步学习:
前言:所写的.java代码都是在机器上运行的。我们写了一个.java文件,会先被编译成.class文件,jvm去读取.class文件,运行在机器(机器只认0101010)上。在软件层面,jvm屏蔽了底层硬件、指令层面的细节。
JVM如何工作?
JVM分为3个主要子系统:
- 类加载器子系统
- 运行时数据区
- 执行引擎
一、jvm的主要内存划分:方法区,虚拟机栈,本地方法栈,堆,程序计数器,如下图:
方法区:所有线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JDK 1.8 的时候,方法区被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。
可以使用参数: -XX:MetaspaceSize 来指定元数据区的大小。与永久区很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
==虚拟机栈:==线程私有的,为虚拟机执行java方法(即字节码)服务。虚拟机描述的是java方法执行的内存模型:每个方法被执行时会同时创建一个 栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。
会出现2种异常:
- StackOverFlowError:若Java虚拟机的内存大小
不允许动态扩展,那么当线程请求栈的深度超过当前java虚拟机栈的最大深度的时候,就抛出此异常 - OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法动态扩展了,就抛出此异常
思考:java中的方法/函数如何调用?
Java栈可用类比数据结构中栈,Java栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java方法有2种返回方式: - return 语句
- 抛出异常
不管哪种返回方式都会导致栈帧被弹出。
==本地方法栈:==为虚拟机使用到的native方法服务。会同Java虚拟机栈一样抛出2种异常
堆: jvm所管理内存中的最大一块,被所有的线程共享,此内存唯一目的就是 ==存放对象实例 ==,几乎所有的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,故也被称作GC堆。
堆细分:新生代和老年代,再细分:Eden空间、FromSurvivor 空间、To Survivor空间。当堆无法再扩展,会抛出OutOfMemoryError异常。JDK1.7及之后版本的 JVM 已经将运行时常量池(用于存放编译期生成的各种字面量和符号引用)从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
常量池存储的内容:
程序计数器: 是线程私有的,占用一小块内存,作用是当前线程所执行的字节码的行号指示器。主要有2个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注:程序计数器是唯一一个不会出现OutOfMemoryEror的内存区域
直接内存:
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,也可能导致OutOfMemoryError异常出现。
==本地方法库:==执行引擎所需的本地库。
本地库接口: JNI与本地方法库进行交互,并提供执行引擎所需的本地库。
==执行引擎:==分配给运行时数据区的字节码文件将由执行引擎执行。执行引擎读取字节码并逐个执行。
- 解释器:解释字节码较快,执行慢。缺点:一个方法被多次调用时,每次都会重新解析。
- JIT编译器:解决了解释器的缺点。执行引擎在转换字节码时使用解释器的帮助,但当发现重复代码时,使用JIT编译器,编译器会编译整个字节码并将其更改为本地代码。本地代码将直接用于重复的方法调用,从而来提高性能。
二、垃圾回收(GC):
能被回收的内存空间都是堆内存。
jvm的垃圾回收机制中,判断一个对象是否死亡,并不是根据是否还有对象对其有引用,而是通过可达性分析。对象间的引用可以抽象成树形结构,通过树根(GC ROOTs)作为起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则表示对象是不可以用的。
GC Roots 对象: - 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
Java 提供的软引用和弱引用可随时被jvm回收。一般将比较占内存但是有可能后面会用到的对象,可以声明为软引用活弱引用。
三、3种常见的垃圾收集算法: - 1.标记清楚算法:
首先,通过可达性分析将可回收的对象进行标记,标记后再同意回收所有被标记的对象。
弊端:效率不高,标记和清楚效率都不高;空间问题:标记清楚后会产生大量的不连续的内存碎片。 - 2复制算法:
为了解决效率问题,复制算法将内存分为大小相同的两块,每次只使用其中一块。 - 3.标记-整理算法:
先标记需要回收的对象,然后把所有存活的对象移动到内存的一端。
优点:避免了内存碎片。
四、HotSpot 虚拟机对象探秘(重点掌握):
- 对象的创建:
①类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
•CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
•TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
⑤执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
- 对象的内存布局:
在 Hotspot 虚拟机中,对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
Hostspot虚拟机的对象头包括2部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。对齐填充部分不是必然存在的,没有特殊含义,仅仅起占位作用。因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),故,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 - 对象的访问定位
建立对象就是为了使用对象,Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:
-
句柄:
如果使用句柄的话,那么Java堆中将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 -
直接指针:
如果直接指针访问,那么Java对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象的地址。使用句柄的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,节省了一次指针定位的开销。