JVM原理简介

1. JVM基础

1.1 JVM定义

JVM 是 JAVA 虚拟机(JAVA Virtual Machine)的缩写,是一个虚构出来的计算机,是通过在实际计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机有自己完善的虚拟硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 屏蔽了具体操作系统平台相关的信息,使得 java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以在多种平台上不加修改的运行。

1.2 可运行的编程语言


1.3 运行原理


1.4 JVM、JRE/JDK


2. JVM体系结构


2.1 类加载器

类加载器加载其实就是根据编译后的Class文件,将Java字节码载入JVM内存,并完成对运行数据处于的初始化工作,供执行引擎执行。

2.2 运行数据区

1. 程序计数器(Program Counter Register)
每一个 Java 线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非 Native 方法,这个区域记录的是正在执行的 VM 原语的地址,如果正在执行的是 Natvie 方法,这个区域则为空(undefined)。此内存区域是唯一一个在 VM Spec 中没有规定任何 OutOfMemoryError 情况的区域。
2. Java 虚拟机栈(Java Virtual Machine Stacks)
与程序计数器一样,VM 栈的生命周期也是与线程相同。VM 栈描述的是 Java 方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在 VM 栈中的入栈至出栈的过程。经常有人把 Java 内存简单的区分为堆内(Heap)和栈内存(Stack),实际中的区域远比这种观点复杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。“栈”就是 VM 栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型(boolean、byte、char、short、int、float、long、 double)、对象引用(不是对象本身,仅仅是一个引用指针)、方法返回地址等。其中 long 和 double 会占用 2 个本地变量空间(32bit),其余占用 1 个。本地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全确定的事情,在方法运行期间不改变本地变量表的大小。在 VM Spec 中对这个区域规定了 2 中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果 VM 栈可以动态扩展(VM Spec 中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
3. 本地方法栈(Native Method Stacks)
本地方法栈与 VM 栈所发挥作用是类似的,只不过 VM 栈为虚拟机运行 VM 原语服务,而本地方法栈是为虚拟机使用到的 Native 方法服务。和 VM 栈一样,这个区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
4. Java 堆(Java Heap)
对于绝大多数应用来说,Java 堆是虚拟机管理 大的一块内存。Java 堆是被所有线程共享的,在虚拟机启动时创建。Java 堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在 VM Spec 中的描述是:所有的实例以及数组都在堆上分配,但是在逃逸分析和标量替换优化技术出现后,VM Spec 的描述就显得并不那么准确了。 Java 堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、 to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对 Java 堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存。根据 VM Spec 的要求,Java 堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛OutOfMemoryError 异常。
5. 方法区(Method Area)
方法区中存放了每个 Class 的结构信息,包括常量池、字段描述、方法描述等等。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生 GC,这里的GC 主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
6. 运行时常量池(Runtime Constant Pool)
Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。 运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出 OutOfMemoryError 异常。

2.3 执行引擎与本地接口


3. JVM类加载机制

3.1 类加载器



3.2 类加载过程


4. JVM内存管理

4.1 线程共享内存

1. 堆内存

2. 方法区

4.2 线程独占内存


4.3 内存分配策略

Java技术体系中的自动内存管理 终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。对象的内存分配往大的方向上讲,就是在堆上分配,对象主要分配在新生代的Eden ,少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。下面是几条主要的 普遍的内存分配规则:

1)对象优先在 Eden 分配

大多数情况下,对象在新生代的 Eden区中分配。当 Eden区没有足够的空间进行分配时,虚拟将发起一次Minor GC,如果GC 后新生代中存活的对象无法全部放入Survivor 空间,则需要通过分配担保机制提前进入到老年代中。

Minor GC 与 Full GC 的区别:

a.新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java对象大多都具备朝生夕死的特性,所以 MinorGC 非常频繁,一般回收速度也比较快。

b.老年代 GC(MajorGC/Full GC):指发生在老年代的GC,出现了 MajorGC,经常会伴随至少一次 MinorGC(但非绝对,在 ParallelScavenge收集器的收集策略里就有直接进行 MajorGC 的策略选择过程)。MajorGC 的速度一般会比 MinorGC 慢 10 倍以上。

2)大对象直接进入老年代

所谓大对象是指,需要大量连续内存空间的 Java对象, 典型的大对象就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来“安置”它们。虚拟机提供了一个-XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存拷贝。

3)长期存活对象将进入老年代

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能够识别哪些对象应当放在新生代,哪些对象应该放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden 区出生并经过第一次Minor GC 后仍然存活,并且能被Survivor 区容纳的话,将被移到Survivor 区中,并将对象年龄设置为1。对象在 Survivor区中每熬过一次 MinorGC,年龄就增加 1岁。当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 来设置。

4)动态对象年龄判定

为了更好的适应不同程序的内存状况,虚拟机并不总是要求对象年龄必须达到MaxTenuringThreshold才能晋升到老年氏,如果在 Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,那么年龄大于或等于该年龄的对象就直接进行老年代,无须等到 MaxTenuringThreshold 中要求的年龄。


5)分配担保

在发生Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,如果大于,则改为直拉进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则要改为进行一次FullGC

新生代使用复制收集算法,但为了提高内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,需要老年代进行分配担保,让Survivor空间无法容纳的对象直接进入老年代。

4.4 垃圾收集

1.对象的引用

无论是通过计数算法判断对象的引用数量,还是通过根搜索算法判断对象引用链是否可达,判定对象是否存活都与引用相关。引用主要分为:强引用、软引用、弱引用、虚引用四种,引用的强度依次减弱。

1)强引用

就是指在代码之中普遍存在的,类似:“Object objectRef = new Obejct”,这种引用,只要强引用还存在,永远不会被GC清理。

2)软引用

用来描述一些还有用,但并非必须存在的对象,当Jvm内存不足时(内存溢出之前)会被回收,如果执行GC后,还是没有足够的空间,才会抛出内存溢出异常。

通过SoftReference类来实现软引用,SoftReference很适合用于实现缓存。另,当GC认为扫描的

SoftReference不经常使用时,可会进行回收。

3)弱引用

弱引用也是用来描述一些还有用,但并非必须存在的对象,它的强度会被软引用弱些,被弱引用关联的对象,只能生存到下一次GC前,当GC 工作时,无论内存是否足够,都会回收掉弱引用关联的对象。JDK通过WeakReference类来实现。当获取时,可通过weakReference.get 方法获取,可能返回 null,可传入一个 ReferenceQueue 对象到 WeakReference构造,当引用对象被表示为可回收时,isEnqueued 返回true 

4)虚引用

虚引用称为幻影引用,它是弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对生存时间构成影响。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被GC回收时收到一个系统通知。通PhantomReference类实现。

2.垃圾判定算法
1)引用计数法

引用计数是最简单直接的一种方式,这种方式在每一个对象中增加一个引用的计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。

优点:

简单,直接,不需要暂停整个应用缺点:

1.需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作,比如每次将对象赋值给新的引用,或者者对象的引用超出了作用域等。

2.不能处理循环引用的问题 

2)根搜索法
在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC 
Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到
GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象
是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。 
在Java语言里,可作为GC Roots的对象包括下面几种: 
虚拟机栈(栈帧中的本地变量表)中引用的对象。 
方法区中的类静态属性引用的对象。 
方法区中的常量引用的对象。 
本地方法栈中JNI(即一般说的Native方法)的引用的对象。 

  
图3-1 根搜索算法判定对象是否可回收 
3.垃圾收集算法
1)标记清除算法

标记-清除(Mark-Sweep)算法是 基础的算法,就如它的名字一样,算法分为标记清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。它主要有两个缺点:

1、一个是效率问题,标记和清除过程的效率都不高;

2、另外一个是空间问题,标记清楚后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够连续的内存空间而不得不提前出发另一次垃圾收集动作。


2)复制算法

为了解决效率问题,一种称为复制(Copying)的收集算法就出现了,它将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,没存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点


现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照11的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将 Eden 和 Survivor 中还存活着的兑现个一次性地拷贝到另外一块 Survivor空间上, 后清理掉 Eden和刚才用过的 Survivor的空间。HotSpot虚拟机默认EdenSurvivor的大小比例是81,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存是会被浪费的

3)标记整理算法

复制算法在对象存活率较高时就要执行较多的复制操作,效率将会贬低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

根据老年代的特点,有人提出了另外一种标记-整理算法,标记过程仍然与标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。


4)分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象的生存周期的不同将内存划分为几块。一般是把Java堆分成新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那么就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收













  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值