Java虚拟机JVM

JAVA学习网 2020-07-13 14:49:03

一、内存模型及分区

  JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区。

1.1、栈(Stack-线程私有)

 1.1.1 栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放的是 8 大基础类型加上一个引用类型,所以还是一个指向地址的指针。操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式。

  栈帧(Stack Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

 1.1.2 JVM为每个线程创建一个栈,用于存储该线程执行方法的信息(实际参数、局部变量等),与线程的生命周期相同。

 1.1.3 栈的特性:后进先出;由系统自动分配速度快且栈是一个连续的存储空间。

1.2、堆(Heep-线程共享)---运行时数据区

 1.2.1 是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

 1.2.2 JVM只有一个堆,被所有线程所共享。

 1.2.3 堆是一个不连续的存储空间,分配灵活但速度慢。

1.3、方法区/永久代(线程共享):

 1.3.1 主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据 (永远不变或唯一的内容) 。

 1.3.2 我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)

 1.3.3 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字常量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

 1.3.4 JVM只有一个方法区,被所有线程共享。

 1.3.5 方法区实际上也是堆,只是用来存储类、常量相关的信息。

1.4、本地方法栈(线程私有)

 本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

1.5、程序计数器(线程私有)

 一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

二、Minor GC、Major GC、Full GC

2.1 Minor GC

 新生代 GC(Minor GC):从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

  2.1.1  当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

  2.1.2  内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。

  2.1.3 执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。

  2.1.4 所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。

  2.1.5 触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。

2.2 Major GC

 Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。

 Major GC的触发条件:当年老代空间不够用的时候,虚拟机会使用“标记—清除”或者“标记—整理”算法清理出连续的内存空间,分配对象使用。

  注意:通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

2.3 Full GC

 Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

 Full GC触发条件

  2.3.1 调用System.gc时,系统建议执行Full GC,但是不必然执行

  2.3.2 老年代空间不足

  2.3.3 方法去空间不足

  2.3.4 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  2.3.5 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

三、JVM运行时内存

   Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
 
 

3.1、新生代

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

 3.1.1 Eden 区

  Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

 3.1.2 ServivorFrom 区

  上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

 3.1.3 ServivorTo区

  保留了一次 MinorGC 过程中的幸存者。

 3.1.4 MinorGC 的过程(复制->清空->互换)

  MinorGC 采用复制算法。

  3.1.4.1 eden、servivorFrom 复制到 ServivorTo,年龄+1

    首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServivorTo 区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServivorTo 不够位置了就放到老年区);虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。

  3.1.4.2 清空 eden、servivorFrom

    然后,清空 Eden 和 ServivorFrom 中的对象;

  3.1.4.3 ServivorTo 和 ServivorFrom 互换

    最后,ServivorTo 和 ServivorFrom 互换,原 ServivorTo 成为下一次 GC 时的 ServivorFrom区。

3.2、老年代

 主要存放应用程序中生命周期长的内存对象。

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

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

 

 
 
 
 
阅读(2327) 评论(0)