Java虚拟机——JVM简介
Java虚拟机——JVM简介
JDK(Java Development Kit)是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。JRE(Java Runtime Environment)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。JVM(Java Virtual Machine)
是整个java实现跨平台的最核心的部分,能够运行用Java语言写的程序。
参考博客:JDK,JRE,JVM的区别
虚拟机:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机有JVM、VMwave、Virtual Box。
JVM通过软件模拟Java字节码的指令集,JVM不同于VMwave、Virtual Box,JVM中只保留了PC寄存器,其他的寄存器都进行了裁剪. 现在我们用的Java虚拟机大部分指的都是HotSpot。
- 类加载器负责将类文件中的字节码和数据加载到方法区和常量池中。
- 执行引擎负责将方法区和常量池中的指令放到PC,栈区执行,如果需要用到堆区的数据,就会通过动态内存管理申请内存空间。
-
动态内存管理GC(Grabage Collect) 负责清除内存垃圾和整理内存。
注意: - 线程工作是以方法为单位的,执行方法时压入栈区,执行完后弹出栈区。线程运行过程中如果需要对象,则向动态内存管理器申请内存,动态内存管理器会在堆区分配内存空间。
- Static方法栈帧中没有this引用,而普通方法的栈帧中有this引用(指向调用该方法的对象)。
引用只是一种数据类型,它可能在堆区、方法区、栈区。看引用在哪,要看引用是局部变量还是静态变量。
Ⅰ 运行时数据区域
一般来说,JVM管理的内存将会包含以下几个运行时数据区域:
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池
(1)程序计数器(线程私有)
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
线程私有的内存:
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。
注意:
程序计数器内存区域是唯一一个在 JVM 规范中没有规定任何 OOM(Out of Memory) 情况的区域。
(2)Java虚拟机栈(线程私有)
虚拟机栈描述的是Java方法执行的内存模型: 每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表:
存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小.
此区域会产生两种异常:
- 线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出
Stack Over Flow
异常。 - 虚拟机在动态扩展时无法申请到足够的内存,会抛出
OOM(Out Of Memory)
异常
(3)本地方法栈(线程私有)
本地方法栈与虚拟机栈的作用完全一样,他俩的区别无非是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务。在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。
(4)Java堆(线程共享)
Java堆(Java Heap)是JVM所管理的最大内存区域。Java堆是所有线程共享的一块区域,在JVM启动时创建。此内存区域存放的都是对象实例。JVM规范中说到:“所有的对象实例以及数组都要在堆上分配”。
Java堆是垃圾回收器管理的主要区域,因此很多时候可以称之为"GC堆"。根据JVM规范规定的内容,Java堆可以处于物理上不连续的内存空间中。Java堆在主流的虚拟机中都是可扩展的(-Xmx设置最大值,-Xms设置最小值)。如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。
(5)方法区(线程共享)
方法区与Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
等数据。在JDK8以前的HotSpot虚拟机中,方法区也被称为"永久代"(JDK8已经被元空间取代)。
永久代并不意味着数据进入方法区就永久存在,此区域的内存回收主要是针对常量池的回收以及对类的卸载。JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。
(6)运行时常量池(方法区的一部分)
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量: 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用: 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
Ⅱ Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
内存泄漏: 泄漏对象无法被GC的情况。
内存溢出: 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大;或者检查对象的生命周期是否过长。
Ⅲ 垃圾回收器与内存分配策略
执行引擎根据用户的字节码申请内存,但不管内存的释放问题。不再使用的内存需要用GC进行回收。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭,所以这三个区域的内存分配与回收具有确定性,不用关心这三个区域的内存释放问题。方法区随着类的卸载而结束,而常量池中的内容占用内存不大,且基本上都是有用的数据,所以也不用关心。因此我们的内存分配和回收主要关注Java堆这个区域。
注意:
回收内存是以对象为单位的。
1 如何判断对象已"死"
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有引用计数法,可达性分析法等。
(1)引用计数法
引用计数描述的算法为:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题.
循环引用:对象外部没用引用了,但是出现对象内部属性相互引用的现象。
(2)可达性分析算法
Java采用"可达性分析"来判断对象是否存活(采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。
此算法的核心思想为: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
注意:GC做可达性分析时,所有的线程都会暂停。
在Java语言中,可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象(线程活着意味着栈还会被用到)
- 方法区中类静态属性引用的对象(随时可能被用到)
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
(3)引用类型
引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
1)强引用: 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被强引用引用的对象实例。
2)软引用: 软引用是用来描述一些还有用但不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
3)弱引用: 弱引用也用来描述非必需对象。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
4)虚引用: 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
生存还是死亡——finalize()方法
即使在可达性分析算法中不可达的对象,也可以有机会得到缓刑。要宣告一个对象的真正死亡,至少要经历两次标记过程: 如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将此时的对象视为真正的“死”了。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()
方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。
注意:
JVM只承诺了finalize()在对象死后一定会调用,但是没有规定在什么时机调用,因为对象啥时候死是不确定的,所以finalize()不建议使用。
2 回收方法区
方法区(永久代)的垃圾回收主要收集两部分内容: 废弃常量和无用的类。
回收废弃常量和回收Java堆中的对象十分类似。以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :
- 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。