当前位置: 首页>编程语言>正文

java JVM java jvm详解

使用Java的同学都知道,Java语言有两个特点:一次编译,到处运行;不需要手动释放内存。为什么能做到这一点呢?这一切都归功于Java的虚拟机JVM。接下来,我们就聊聊JVM。

一、啥是JVM

JVM,又称java虚拟机,其作用是充当操作系统的翻译官,能够将.class文件传递给操作系统运行,是JRE的组成部分。JVM的生命周期起始与main方法,这个方法必须是public,接受一个字符串数组参数,返回void。main方法启动的时候,会初始化一个守护线程,程序中的其它线程,都由守护线程启动,所以只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。

JVM由一个加载器子系统和一个执行引擎组成,加载器系统负责加载程序中的类型,并赋予唯一的名字,执行引擎负责执行被加载类中的指令,还有内存空间。

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示:

java JVM java jvm详解,java JVM java jvm详解_Java虚拟机,第1张

 

1.1 程序计数器

程序计数器是JVM中较小的一块内存区域,它的作用是当前线程所执行的字节码行号指示器,字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令。**线程正在执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;**如果执行的是Native方法,这个计数器则为空。** 为了确保多线程切换后能够恢复到正确的执行位置,每一条线程都有一个独立的程序计数器。此内存区域也是唯一 一个在Java虚拟机规范中没有规定任何OOM情况的区域。

1.2 Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法在调用的时候,都会创建一个栈桢,用于存储方法的局部变量表,操作数栈,方法的入口出口信息和动态链接等,每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。对象引用直接或者间接指向堆中对象的地址。由于此过程是在编译时期完成的,所以局部变量内存分配大小是固定的,不会在运行时改变大小。

Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

1.3 本地方法栈

本地方法栈和虚拟机栈发挥的作用是非常类似的,只是本地方法栈为虚拟机使用到的Native方法服务。本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

1.4 堆

堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存,所有的对象实例和数组都在堆上分配。

1.5 方法区

在jdk1.7以前,方法区被堆内存托管,jdk1.8后,这部分变成了元空间,成为了堆外内存,不受JVM直接控制。

方法区存储内容:加载的类信息,常量、静态变量,即时编译器编译后的代码等数据。类信息包括:类的全限定名,类的访问修饰等;字段信息:类中所有字段声明的描述,比如字段名字,类型和修饰符等。方法信息:方法名称,方法返回类型,方法修饰符,操作数栈和该方法在战桢中局部变量区的大小等;类变量:指向类加载器的引用,指向class实例的引用,方法表等。

运行时常量池:存放了该类所用到的常量有序集合,存放编译期生成的各种字面量和符号引用,字面量相当于Java层面的常量,如文本字符串、final修饰的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

1.6 直接内存

直接内存并不是虚拟机运行时数据区的一部分,不是虚拟机运行时数据区的一部分。在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。可能导致OOM。

二、Hotspot虚拟机对象

2.1 对象的创建

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

接下来为对象分配内存:就是把一块确定大小的内存从Java堆中划分出来。有‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式。假设Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”。如果Java堆中的内存并不规整,已使用的内存和空闲的内存相互交错,那就没办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。

执行Init方法,进行初始化,这个时候对象才算创建完成。

2.2 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)。

对象头包含两部分内容:第一部分用于存储对象自身的运行时数据Markword,包含对象的Hash,GC年龄,锁状态,线程持有的锁等;第二部分是指向类的元数据指针,标明是哪一个类的实例;如果是Java数组,对象头中还包含记录数组长度的数据;

实例数据:程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。。

对齐填充:不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

2.3 对象的访问定位

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。Java中包含两种对象的访问:句柄访问和直接内存访问。使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。
 

三、JVM的垃圾回收

线程私有的程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是堆内存和方法区。

3.1 对象是否死亡

Java判断对象的存活主要有两种方法:引用计数法和根搜索法。

  • 引用计数法

创建对象的时候,为这个对象产生一个引用计数器。当有新的引用的时候,引用计数器+1,而当其中一个引用销毁的时候,引用计数器-1;当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了。这种算法有一个致命的缺点是:不能解决循环引用。

  • 根搜索算法  

 算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 

3.2 哪些可以作为GC root, 引用有哪些

Java语言中的GC ROOT,包含:虚拟机栈中的引用对象;方法区中的静态引用对象;方法区中的常量引用对象;本地方法栈中的引用对象。

Java对引用分为四大类:

强引用:类似于new创建的,只要强引用在就不回收。

软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3.3 Finalize()方法

在对象不可达的时候,也并非是“非死不可”的。真正宣告一个对象死亡,至少要经历两次标记过程:如果对象再进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机吊用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。

任何一个对象的finalize()方法都至多被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

3.4 垃圾回收算法

   常见的垃圾回收的算法

  • 标记清除算法       

标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!

  • 复制算法

复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。

  • 标记整理算法

采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

  • 分代算法

 根据Java虚拟机规范,jvm将内存划分为:

  • 年轻代(Young):年轻代用来存放JVM刚分配的Java对象
  • 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
  • 持久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够

 其中Young和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:xxx)指定的内存中分配,Perm不属于堆内存,虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。

对Young进一步细分,可有:

  • Eden:伊旬园区,用来存放JVM刚分配的对象。
  • Survivor1:
  • Survivro2:当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足条件时,就会被Copy到Tenured。Survivor增加了对象在年轻代中的逗留时间,也就增加了被垃圾回收的可能性。

   3.5 垃圾回收器

  • Serial器:串行收集器,并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,直到它回收结束为止,因此又号称“Stop The World” 的垃圾回收器。新生代采用复制算法,年老代采用标记整理算法。
  • ParNew:Serial 收集器的多线程版本
  • 并行回收器Parallel:并行收集器其实就是多线程版本的Serial收集器
  • 并发回收器:CMS,又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法。整个过程分为4个步骤:1)初始标记 2)并发标记 3)重新标记 4)并发清除。1、3会stop the word。
  • G1收集器:可以在基本不牺牲吞吐量的前提下,完成低停顿内存回收。G1将内存分为一个个Region,一部分 region 是作为 Eden,一部分作为 Survivor,一部分作为Old region。在新生代,G1 采用的仍然是并行的复制算法,在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。整个过程分为1)初始标记 2)并发标记 3)最终标记 4)筛选回收。

 3.6 内存分配和回收策略

大多数情况下,新生的对象直接分配到Eden区,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。对于大对象(需要大量连续内存空间的Java对象,如很长的字符串以及数组),为了避免Eden区及两个Servivor之间发生大量的内存复制,直接进入老年代。

如果对象在Eden区出生并经历过一次Minor GC后仍然能存活,并且能够被Servivor容纳,将被转移到Servivor空间中;对象在Servivor区每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋级到老年代中。

空间分配担保:

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于晋级到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的,如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

Minor GC: 年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC

Full GC: 清理整个堆空间—包括年轻代和老年代

  • 当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
  • Full GC 的触发条件:调用System.gc时,系统建议执行Full GC,但是不必然执行;当年老代满时会引发Full GC;当持久代满时也会引发Full GC,会导致Class、Method元信息的卸载。通过Minor GC后进入老年代的平均大小大于老年代的可用内存。

GC的触发条件有两种:1)程序调用System.gc时可以触发,但并不保证一定执行;2)系统自身来决定GC触发的时机。4、

3.6 G1和CMS的区别

1)CMS将jvm内存分为年轻代、年老代,G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。

2)CMS只能回收老年代,需要配合一个年轻代收集器。它的运作过程包括初始标记、并发标记、重新标记和并发清除。

与用户线程一起与运行进行垃圾清除。

3)G1收集器的四部曲:

    I、初始标记:标记GC Root能直接关联的对象,并且修改TAMS的值,让下一阶段的用户进行并发运行时,能够正确运用Region创建新对象,这阶段需要停顿,但停顿时间很短

    II、并发标记:从GC Root开始对堆进行可达性分析,找出存活的对象,这段耗时较长,但可以与用户线程并发执行。

    III、最终标记是为了修正在并发标记阶段因用户程序继续运作导致标记产生变动的那一部分的标记记录,虚拟机将这部分标记记录在线程Remembered Set中,这阶段需要停顿线程,但是可并行执行。

   IV、筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期待的GC停顿时间来制定回收计划,这个阶段也可以与用户线程并行执行,但由于只回收一部分的Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

4)CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)

5)CMS收集器并发收集、低停顿,但是对CPU资源非常敏感,并发阶段可能会抢占资源而降低吞吐量,此外,无法处理浮动垃圾,还会存在内存碎片;

6)与CMS不同,G1是基于标记整理实现,可预测停顿;


https://www.xamrdz.com/lan/5cj1967432.html

相关文章: