JVM原理学习

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

一、基础

1、JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
也就是如下:
• Java源文件—->编译器—->字节码文件
• 字节码文件—->JVM—->机器码
JVM原理学习
2、JVM的体系结构:
JVM原理学习
class文件被jvm装载以后,经过jvm的内存空间调配,最终是由执行引擎完成class文件的执行。

JVM内存空间包含:方法区、java堆、java栈、本地方法栈。

方法区是各个线程共享的区域,存放类信息、常量、静态变量。

java堆也是线程共享的区域,我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。

java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈,这里面有很多细节,我们以后再讲。如果java栈空间不足了,程序会抛出*Error异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。

本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。

PC寄存器,说到这里我们的类已经加载了,实例对象、方法、静态变量都去了自己改去的地方,那么问题来了,程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是PC寄存器在管,它的作用就是控制程序指令的执行顺序。
执行引擎当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。

二、垃圾回收

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
1、垃圾判断算法

**引用计数法:**通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

**可达性分析算法:**通过GC Roots的对象为起点,向下搜索,能到达的对象为不可回收对象,不能到达的对象为需要回收的对象。
GC Roots的对象:
1)方法区中常量引用的对象;
2)方法区中类静态属性(static修饰)引用的对象;
3)虚拟机栈(本地变量表)中引用的对象(正在被使用);
4)本地方法栈(native修饰方法)中引用的对象;

2、 垃圾回收算法

**标记-清除算法:**标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。但它存在一个很大的问题,那就是内存碎片,标记清除算法会产生很多内存碎片,一旦需要分配一个较大的空间给某个新对象的时候,很可能就会因为没有足够的连续内存,从而再运行一次gc机制,其实也是影响程序的运行效率。

**复制算法:**简单的说,就是把内存划分为两块(两块大小不一定要相等,事实上,也不建议相等,下面会详细说明),每次只使用其中的一块,当内存空间被占用完以后,就把尚在存活的对象存到另外的半区,再将当前半区已使用的内存空间一次清除掉。这样在分配内存时就不用考虑空间碎片,按顺序分配内存即可,有效提高了效率。但是复制算法存在另一个问题,内存始终只有一部分不可用,代价实在太高。

**标记-整理算法:**标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

**分代收集算法:**分代收集算法分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。

3、新生代老年代和永久代
JVM中的堆,一般分为三大部分:新生代、老年代、永久代:

新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代又分为 Eden、ServivorFrom、ServivorTo三个区。

Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
ServivorTo:保留了一次MinorGC过程中的幸存者。
ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。
当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。
MinorGC的过程:采用复制算法。
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)
同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)
然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

老年代:老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用
标记—清除算法

首先扫描一次所有老年代,标记出存活的对象
然后回收没有标记的对象。
MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

**永久代:**指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
Class在被加载的时候被放入永久区域。它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

4、GC触发机制
 Minor GC触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。
 Major GC的触发机制:
  Major GC又称为Full GC。当年老代空间不够用的时候,虚拟机会使用“标记—清除”或者“标记—整理”算法清理出连续的内存空间,分配对象使用。
Full GC触发机制:
  (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
  (2)老年代空间不足
  (3)方法区空间不足
  (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  (5)由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

5、内存区域与回收策略
对象优先在Eden分配、大对象直接进入老年代、长期存活的对象将进入老年代、动态对象年龄判定(虚拟机并不是永远地要求对象的年龄必须达到 MaxPretenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代)

6、内存溢出与内存泄漏

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!