文章目录
- 概述
- 运行时数据区
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
- 垃圾收集
- 引用计数算法
- 可达性分析算法
- 方法区的回收
- 引用类型
- 垃圾收集算法
- 标记 - 清除
- 标记 - 整理
- 复制
- 分代收集
- 内存分配策略与回收策略
- 类加载机制
概述
JVM (Java Virtual Machine)Java虚拟机,是一种用于计算机设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(百度百科)
Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
运行时数据区
对于Java运行时涉及到的存储区域主要包括程序计数器、Java虚拟机栈、本地方法栈、java堆、方法区以及直接内存等等。
程序计数器
记录正在执行的虚拟机字节码指令的地址。(如果正在执行的是本地方法则为空Undefined)。
在Java里面主要是取下一条指令的字节码文件。
(每个线程都是有一个程序计数器的,是线程私有的,相当一个指针)
Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧
(Stack Frame)用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在Java虚拟机栈
中 入栈 和 出栈的过程。
可以通过 -Xss
这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小
java -Xss2M HackTheJava
Java虚拟机栈 区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出
StackOverflowError
异常; - 栈进行动态扩展时如果无法申请到足够内存,会抛出
OutOfMemoryError
异常。
本地方法栈
本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般使用其它语言(C、C++或者汇编语言等)编写,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
堆
(heap)所有对象都在这里分配内存,是垃圾收集的主要区域(“GC堆”)。
垃圾收集器
基本都是采用 分代收集算法
,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。
可将堆分成两块:
- 新生代 (Young Generation)
- 老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError
异常
可以通过 -Xms
和 -Xmx
两个虚拟机参数 来指定一个程序的对内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出OutOfMemoryError
异常。
对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。
运行时常量池
运行时常量池是方法区的一部分。
Class文件中的常量池,用于存放编译器生成的各种字面量
和 符号引用
,这部分在类加载后进入方法区的运行时常量池中。
在运行期间,也可以向常量池中添加新的常量(动态生成),如String类的intern()方法。
每个运行时常量池都是从 JVM 的方法区中分配的。
直接内存
JDK 1.4中新引入了 NIO 类,它可以使用 Native函数库直接分配堆外内存(直接内存),然后通过一个存储在堆中的 DirectByteBuffer
对象作为这块内存的引用进行操作。
避免了Java 堆内存 和 堆外内存来回拷贝数据的时间,更高效。
垃圾收集
垃圾收集主要是针对堆
和 方法区
进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有
的 ,只存在于线程的声明周期内,线程结束后就会消失,所以不需要对这三个区域进行垃圾回收。
垃圾收集器在对 堆区 和 方法区进行回收前,首先要确定这些区域的 对象
哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法。
引用计数算法
在这种方法中,堆中的每个对象实例
都有一个 引用计数器
。当对象增加一个引用时计数器 加1
,引用失效时计数器 减1
,引用计数器为0 的对象可被回收。
在两个对象出现 循环引用
的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
class MyObject{
MyObject object;
}
public class abc_test {
public static void main(String[] args) {
MyObject object1=new MyObject();
MyObject object2=new MyObject();
//父对象有一个子对象的引用
object1.object=object2;
//子对象反过来引用父对象
object2.object=object1;
//设置为null 说明二者指向的对象已经不能再被访问
object1=null;
object2=null;
//但彼此互相引用,计数器不为0,导致GC无法回收
}
}
可达性分析算法
从一个节点 GC Root
开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots
一般包含一下内容:
- 虚拟机栈 中局部变量表中引用的对象
- 本地方法栈 中JNI (Native方法)中引用的对象
- 方法区 中类静态属性引用的对象
- 方法区 中的常量引用的对象
方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
finalize()
当一个对象可被回收时,如果执行对象的 finalize()
方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize()
方法自救,那么后面回收时不会再调用该方法。
引用类型
无论是通过 引用计数算法
判断对象的引用数量,还是通过 可达性分析算法
判断都对象是否可达,判定对象是否可被回收都与 引用
有关。
Java提供了四种强度不同的引用类型:强引用
、 软引用
、 弱引用
和虚引用
。
强引用
被强引用关联的对象不会被回收,(也就是强引用只要还存在,垃圾收集器永远不会回收掉被引用的对象)。
使用 new
一个对象的方式 创建 强引用
Object obj = new Object();
软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference
类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收之前。
使用 WeakReference
类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用
又称为 幽灵引用 或 幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的 唯一目的 是能在这个对象被回收时,收到一个系统通知。
使用 PhantomReference
来创建虚引用:
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
垃圾收集算法
标记 - 清除
标记 - 整理
复制
现在的商业虚拟机都采用 复制收集算法
回收 新生代
,但是并不是划分为大小相等的两块,而是一块较大的 Eden
空间 和 两块较小的 Survivor
空间,每次使用 Eden 和 其中一块 Survivor 。
在回收时,将Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机
的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
分代收集
现在的商业虚拟机采用 分代收集算法
,他根据对象存活周期 将内存或分为几块,不同块采用适当的收集算法。
一般将 堆
分为 新生代
和 老年代
- 新生代: 复制算法
- 老年代:标记 - 清除 或者 标记 - 整理 算法
内存分配策略与回收策略
Minor GC 和 Full GC
-
Minor GC
:回收新生代
,因为 新生代对象 存活的时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 -
Full GC
:回收老年代
和新生代
,老年代对象存活的时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
内存分配策略
-
对象
优先在Eden
上分配内存:大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 -
大对象
直接进入老年代:大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 -
长期存活的对象
进入老年代
Full GC 的触发条件
对于 Minor GC
,其触发条件非常简单,当 Eden
空间满时,就将触发一次 Minor GC。而Full GC 相对复杂,有以下条件:
- 调用
System.gc()
:只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 - 老年代空间不足
- 空间分配担保失败
- 永久代空间不足
类加载机制
类是运行期间第一次使用时动态加载的,而不是一次性加载所有的类。如果一次性加载的话,那么会占用很多的内存。
类的生命周期
类的生命周期
包括7个阶段:
加载、验证、准备、解析、初始化、使用 和 卸载。
其中,解析和初始化交换顺序,可实现 Java的动态绑定。
类的加载过程
包含了 加载
、 链接
和 初始化
阶段。
- 加载 :将
class
文件读入到内存,并为之创建一个Class
对象 - 链接 :链接阶段又包括
验证
、准备
和解析
三个阶段
- 验证 : 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:
类变量
是被static
修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 - 解析:将常量池中的
符号引用
替换为直接引用
的过程。解析过程在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java 的动态绑定。
- 初始化:初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器
<clinit>()
方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始化值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
类初始化的时机
- 创建类的实例
- 访问类的静态变量、或者为静态变量赋值
- 调用类的静态方法
- 使用反射方式来强制创建某个类或接口的java.lang.Class对象
- 初始化某个类的子类