JVM是一个虚拟机,可运行java代码的虚拟计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。 运行过程: java源文件 ——> 编译器——>字节码文件 字节码文件 ——> JVM ——>机器码
每一种平台的解释器不同,但是实现的虚拟机是相同的,因此这也就是为什么java能够跨平台的原因了。 当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
线程指的是程序执行过程中的一个线程实体,JVM允许一个应用并发执行多个线程。 HotSpot JVm后台运行的系统线程主要有下面几个
虚拟机线程 这个线程等待JVM到达安全点操作出现,这些操作必须要在独立的线程里执行周期性任务线程 这个线程负责定时器事件,用来调度周期性操作的执行。GC线程 这些线程支持JVM中不同的垃圾回收活动编译器线程 这些线程在运行时将字节码动态编译成本地平台相关的机器码信号分发线程 这个线程接收发送到JVM的信号并调用适当的JVM方法处理是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法区 和Java Stack类似,区别是虚拟机栈为执行java方法服务,而本地方法栈则为Native方法服务。堆 被线程共享。 对象 数组(数组本身也是对象) 保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。 Java堆从GC的角度可以细分成 新生代(Eden区、From Survivor区和ToSurvivor区)和老年代方法区/永久代 永久代 Permanent Generation ,用于存储被jvm加载的类信息、常量、静态变量。 使用Java堆的永久代来实现方法区,Java堆从GC的角度可以细分成 新生代(Eden区、From Survivor区和ToSurvivor区)和老年代。
Eden区:java新对象的出生地。(如果新创建的对象很大,则直接分配到老年代。)当Eden区内存不够就会触发MinorGC 对新生代区进行一次垃圾回收 SurvivorFrom: 上一次GC的幸存者 作为这一次GC的被扫描者 SurvivorTo:保留了一次MinorGC过程中的幸存者。
以下是MinorGC:过程(复制-清空-互换)
首先把Eden和FromSurvivor区域中存活的对象复制到ToSurvivor区域,(如果有对象的年龄以及达到了老年的标准,则赋值到老年代),同时把这些对象的年龄+1,如果ToSurvivor不够位置了,就放到老年代。
清空Eden和FromSurvivor中的对象
最后将ToSurvivor和FromSurvivor互换,原ToSurvivor区中对象成为下一次GC时的FromSurvivor
【
首先想的是为什么需要Survivor区? 然后再去想两个的问题。
假如没有Survivor区,那么Eden区每进行一次MinorGC,存活的对象都会被送到老年代,老年代被填满,触发MajorGC老年代内存空间远大于新生代,FullGC消耗的时间比MinorGC要长。 Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程: 刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。 上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。 】
主要存放应用程序中生命周期长的内存对象。 老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得新生代的对象晋升入到年代,导致内存空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。 MajorGC采用标记清除算法 首先扫描一次所有老年代,标记处存活的对象,然后回收没有标记的对象。MajorGC耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片。当老年代也满了装不下的时候,就会抛出OOM异常
永久代指的是内存中永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久代,GC不会在主程序运行期对永久代进行清理。所以这也导致了永久代的区域会随着Class的增多而胀满,最终OOM。
在Java1.8之后,永久代被移除了,被一个成为“元数据区”(元空间)的区域所取代,元空间的本质和永久代类似,不过元空间和永久代的区别在于,**元空间并不在虚拟机中,而是使用本地内存。**因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中。
一个对象如果没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收 对象。
如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
最基础的垃圾回收算法,分为两个阶段,标记和清除 从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可 利用空间的问题。
为了解决Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图: 这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
结合 以上两个算法,为了避免缺陷而提出,标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。
解决了前两个的优点,但是效率低。
分代收集算法是目前大部分JVM所采用的方法,核心思想是根据对象存货的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老年代和新生代。老年代的特点是每次垃圾回收时只有少量的对象需要被回收,新生代的特点是每次垃圾回收时都有大量的垃圾需要被回收,因此可以根据不同的区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代每次垃圾回收都要回收大部分对象。即要复制的操作比较少,但通常不是1:1划分新生代。