浅析JVM虚拟机

1.虚拟机的组成结构

虚拟机由类装载子系统、运行时数据区和执行引擎组成

2. 类加载

    ①. 什么是类的加载

        类的加载指的是将类的class文件中的二进制数据读入到内存中并将其放在运行时数据区的方法区内,然后在堆区中创建一个class对象,这个class对象就是类加载的最终产品

        类加载是,创建对象才会去加载,当声明的变量为空或者不创建的时候,不会去加载那个类

    ②.类加载过程

        加载→[(连接)校验→准备→解析]→初始化→使用→卸载

        加载:在硬盘上查找并通过IO读入字节码文件到方法区,使用到的时候才会加载(如调用main()方法或者new对象),加载过程会执行三件事情:

        一. 通过类的权限命名来获取定义的二进制字节流

        二. 将字节流所代表的静态存储结构转化成方法区的运行时数据结构

        三. 到堆里创建类的对象(方法区数据的访问入口)

        连接:执行校验、准备、解析步骤

        校验(重要非必须):校验字节码文件的正确性(-Xverifynone关闭大部分类验证),会去做四个校验工作:

            一. 检验文件格式,是否是class文件格式

            二. 校验元数据,是否符合Java的语言规范

            三. 校验字节码,程序是否合法(符合逻辑)

            四. 校验符号引用,保证引用一定被访问,不会出现类无法访问的问题

    准备:将符号引用替换为直接引用,该阶段会把一些静态方法(符合引用,比如main())替换为指向数据所存内存的指针或句柄(直接引用),这就是所谓的静态链接的过程(类加载期间完成),动态链接是在程序运行期间完成的符合引用替换为直接引用

        符号引用:用一组符号来描述所引用的目标,这组符号可以是任意形式的字面量,只要无歧义地定义目标

        符号引用替换为直接引用:就是将符号地引用直接替换为地址地引用,在不同虚拟机地符号引用是相同的,但是直接引用地地址可能不同

    初始化:对类的静态变量初始化为指定的值,执行静态代码块

    ③. 类加载器的种类

        一. 启动类加载器

            加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、Charsets.jar等

        二. 扩展类加载器

            加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的jar类包

        三. 应用程序类加载器

            加载ClassPath路径下的类包,主要就是加载自己写的那些类

        四. 自定义加载器

            加载用户自定义路径下的类包

            自定义类加载器:继承ClassLoader类,当我们自定义类加载器时,我们需要重写findClass方法

            双亲委派机制:加载某个类时先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有的父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类

        如图:

       在程序中创建的类,如果不做特殊处理,一般是使用应用程序类加载器,这四种加载器无继承关系但是有父子关系,由上到下,由父到子,当我们加载一个类的时候,最开始时到应用程序加载器然后应用程序加载器委托给扩展类加载器,然后扩展类加载器找不到又委托给启动类加载器,启动类加载器如果找不到就又回到应用程序类加载器,如果应用程序类加载器找到就开始加载

        为什么要设计双亲委派机制:

            (1) 沙箱安全机制,自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改

            (2) 避免类的重复加载:当父亲已经加载该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

        3. Java虚拟机运行时内存区域划分

            ①. 线程私有区

                一. 程序计数器

                    记录字节码执行行号(注意:这个地方只计数,不会产生内存溢出)

                二. 虚拟机栈(线程栈)

                     当Java中的方法被执行时,会形成栈帧进入栈中,这种行为成为压栈,反之则会弹出,称为弹栈。栈内存结构,如图:

                    每一个方法会在对应的栈中开辟一块栈帧的内存空间

                    栈帧又分为局部变量表,操作数栈,方法出口等一系列区域

                            局部变量表:存放局部变量

                            操作数栈:当我们的值入栈之后,先到这个地方,然后再转到局部变量,例如:栈帧中有int a = 1,那最开始操作数栈存放1,然后再在局部变量表里存放a = 1,类似于中转站,临时存储数据

                            动态链接:在程序期间将符号引用替换为直接地址引用

                            方法出口:存储方法一些返回信息

                    三. 本地方法栈

             ②. 线程共享区

                   一. 堆内存

                        分配所有对象实例,垃圾回收工作的主要区域,一般情况下,堆内存是所有内存区域中最大的区域

                    二. 元数据区(方法区)

                            虚拟机加载的类元信息,常量,静态变量

                            方法区使用的是系统的直接内存

                            类元信息:类型信息、类型的常量池、字段信息、方法信息、类变量、指向类加载的引用、指向class实例的引用、方法表

                    三.直接内存

                            严格意义上并不属于JVM的内存,非JVM内存的堆外内存,非阻塞io会直接操作这块内存,提升读写数据的效率,如nio

      4. JVM的内存模型

   5. JVM内存分配和回收策略

        默认情况下年轻代占堆内存的1/3,老年区占2/3

        ①. 对象优先在Eden区分配

                首先new出来的对象都分配在堆内存,堆内存分为老年代,年轻代(分为Eden区、Survivor区[分为from区和to区]),多数情况下对象在Eden区分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC

Minor GC和Full GC有什么不同

Minor GC/Young GC:指发生在新生代的垃圾回收动作,操作比较频繁,并且速度相对较快

Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,速度比Minor GC慢十倍以上

            ②. 大对象直接进入老年代

                大对象就是需要连续内存空间的对象,如:字符串,数组

                JVM可以通过参数(-XX:PretenureSizeThreshold)设置参数的大小,如果对象超过设置值的大小就会直接进入老年代,这个参数只在Serial和ParNew两个数据期下有效

                为什么大对象直接进入老年代

                        为了避免大对象在分配内存时的赋值操作而降低效率

            ③. 长期存活的对象将进入老年代

                   虚拟机会给每一个对象一个年龄计数器,如果对象在Eden区出生并经过第一次Minor GC后能够存活并且能被Survivor容纳将被移动到Survivor空间中并且对象年龄设为1对象在Survivor中每经过一次minor GC就会把年龄加一,当年龄提升到一定程度,默认15,就会进入到老年代

                    既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器

                    如果对象在Eden区出生并经过第一次Minor GC之后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1,对象在Survivor中没经过一次Minor GC,年龄就增加1岁,当它的年龄增长到一定程度(默认时15岁),就会被晋升到老年代,对象晋升到老年代的年龄阀值,可通过参数-XX:MaxTenuringThreshold来设置

            ④. Minor GC之后存活下来的对象在Survivor中放不下,那这个对象的一部分会去老年代一部分会去Survivor区

            ⑤. Eden区和Survivor区的比例默认是8:1:1

                大量的对象被分配在Eden区,当Eden区满了之后会触发minor GC,可能有百分之九十的对象会被认为是垃圾被回收掉,剩余存活的对象会被移动到Survivor区,因为新生代的对象几乎都是朝生夕死的,所以Eden区足够大,Survivor区够用即可

            ⑥. 对象动态年龄判断

                    如果当前存放对象的Survivor区域里一批对象的总大小大于这块Survivor内存区域大小的百分之五十,那么此时大于等于这批对象年龄最大值的对象就可以直接进入老年代

                    这个规则其实是希望那些可能长期存活的对象,今早进入老年代

                    对象动态年龄判断机制一般是在Minor GC之后触发的

            ⑦. 老年代空间分配担保机制

                新生代在每一次Minor GC之前,JVM都会计算下老年代的剩余空间,如果老年代剩余可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会查看HandlePromotionFailure这个参数是否设置(jdk1.8默认设置),如果这个参数已经设置,就会查看老年代的剩余可用空间是否大于每次Minor GC后进入老年代的对象平均大小,如果是参数未设置,或者是小于这个评价大小,就会触发一次Full GC

                老年代空间担保机制整体过程如图:

                如果回收完还是没有足够空间存放新的对象,就会发生OOM

6. Java虚拟机如何判断对象是否存活

    ①.引用计数法

        如果对象A具有引用计数器,当对象B对A产生引用时,那计数器的值会+1,当引用断开时,计数器的值-1,但是会出现当AB循环引用时,引用计数器的值将无法为0

         引用计数法判断回收:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就+1;当引用失效,计数器-1;任何时候计数器为0的对象就是不可能再被使用的。当对象引用计数器为0时,即无其他对象引用,判定为死亡,可被回收,但是存在循环引用问题,导致对象一直存活,无法被回收

    ②. 可达性分析法

        从GC Root根开始一直向下查找,直接或间接找到对象,称为可达,反之则不可达,这个算法的基本思想是通过一系列称为“GC Root”的对象作为起始点,从这些节点向下搜索,找到的对象标记为非垃圾对象,其余未标记的对象都是垃圾都想

        可达性分析法判断回收:从GC Root根开始向下搜索,直接或间接可达的对象,即存活对象

        可被作为GC Root的对象:虚拟机栈引用对象,本地方法栈引用对象,静态属性引用对象,常量引用对象

        对象引用的分类:

            一.强引用

                程序中普遍存在的对象引用

            二. 软引用

                SoftReference实现,内存溢出前回收,将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现是放不出空间存放新的对象,则会把这些软引用对象回收掉。软引用可用来实现对内存敏感度不高的高速缓存

                public static SoftReference user = new SoftReference(new User());

            三. 弱引用

                WeakReference实现,下一次垃圾回收时被回收,将对象用WeakReference弱引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

                public static WeakReference user = new WeakReference(new User());

            四. 虚引用

                PhantomReference实现,形同虚设,没有具体的作用,虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

       ③. finalize()方法最终判定对象是否存活

            对象不可达、没有与GC Root根相连接的对象会调用finalize()方法

            即使再可达性分析算法中不可达的对象,也并非“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程

            标记的前提是对象再进行可达性分析后发现没有GC Root相连接的引用链

            第一次标记并进行筛选:对象没有覆盖finalize()方法,对象直接被回收

            第二次标记:如果这个对象覆盖了finalize()方法,只要重新与引用链上的任何一个对象建立关联即可

        ④. 对象怎样起死回生?

            当对象A被标记为不可达时,首先要进行筛选,是否覆盖finalize()方法,并且finalize()有没有被调用过,如果没有会放到F-Queue里面,JVM会启动进程进行重新标记,也就是说对象在finalize()方法中重新与引用链上任意一个对象建立关联即可。不建议这么做,首先是运行代价太高,其次是不确定性大,无法保证对象调用顺序

        ⑤. 如何判断对象可以被回收,判断一个类是无用的类

            类需要同时满足三个条件才能算“无用的类”:

            (1)该类所有的实例都已被回收,也就是堆中不存在该类的任何实例

            (2) 加载该类的ClassLoader已经被回收

            (3) 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

7.四种回收算法

        (1) 标记-清除算法

            首先标记出所有需要回收的对象,标记完成后统一回收,但是这种方式会产生大量不连续的内存碎片,当有大对象需要分配连续内存空间时,有可能会触发再次垃圾回收

        (2) 标记-整理算法

            首先标记出所有需要回收的对象,在回收对象之后,会集中整理内存区域,得到比较规整的内存区域,这种方式可以避免大量的内存碎片,但是整体的效率较低

        (3) 复制算法

            复制算法是将内存分为大小相等的两块,当一块内存已经用完,就将还活着的对象复制到另一块上,然后再把使用过的内存空间一次性清理,优点是:效率高,没碎片,适合朝生夕死的内存区域。缺点是:内存利用低,且不适合在对象存活率高的老年代使用

        (4) 分代回收算法(JVM使用)

            对于新生代使用的是复制算法,对内存划分为8:1:1的部分,对于老年代/元数据区的对象长期存活的区域,采用的是标记-清除/整理算法

8. JVM垃圾收集器

    如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

如图展示了七种作用于不同分代的收集器,如果两个收集器之间又连线,就说明他们可以搭配使用

    ①. Serial收集器(新生代使用:-XX:+UseSerialGC 老年代使用:-XX:+UseSerialOldGC)

        Serial收集器是一种单线程的收集器,单线程一般意味着他只用一个CPU或一条线程去完成垃圾手机工作,另一方面,意味着他在进行垃圾回收工作时,必须暂停其他线程的所有工作,也就是stop the word直到他收集结束为止,为了应对stop the word 产生的不良影响,在后续的迭代中,线程暂停的时间越来越短,serial垃圾回收器采用的回收算法是:新生代采用复制算法,老年代采用标记-整理算法

        优点:简单、高效,没有了多线程之间的线程交互开销,让单线程更加高效地进行收集工作

        serial收集器工作流程

            Serial Old(MSC)是serial收集器地老年代版本,同样是一个单线程收集器,主要有两大用途,一种是在JDK1.5以及以前版本中Parallel Scavenge收集器搭配使用,第二种是作为CMS收集器地后备方案

    ②.ParNew收集器(-XX:+UseParNewGC)

            ParNew收集器,其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为如回收参数,收集算法,回收策略等和Serial收集器完全一样

            ParNew垃圾回收器采用的回收算法是:新生代采用复制算法,老年代采用标记-整理算法

            ParNew收集器默认的线程数和CPU的核数相同,也可以用ParallelGCThreads指定线程收集数一般不推荐修改

            ParNew收集器一般是运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有他能与CMS收集器配合工作

        ③. Parallel Scavenge收集器(新生代使用:-XX:+UseParallelGC 老年代使用:-XX:+UseParallelOldGC)

            Parallel Scavenge收集器类似于ParNew收集器,是Server模式(内存大于2G,2个CPU)下的默认收集器

            Parallel Scavenge的特点:Parallel Scavenge的主要特点是吞吐量(高效率地利用CPU),吞吐量=运行用户代码时间/(运行云湖代码时间+垃圾手机时间),他提供了很多参数,供用户找到最合适地停顿时间或最大吞吐量

            Parallel Scavenge垃圾回收器采用的回收算法是:新生代采用复制算法,老年代采用标记-整理算法

            如果对于垃圾回收不是很了解,或者在优化比较困难地时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理地调优任务交给虚拟机去完成将会是一个不错地选择

            Parallel Old收集器,是Parallel Scavenge收集器地老年代版本,使用多线程和标记-整理算法,在注重吞吐量以及CPU资源地场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器

        ④.CMS收集器(-XX:+UseConcMarkSweepGC老年代)

            CMS:Conrrurent Mark Sweep,是指以最短回收停顿时间为目标的收集器,他非常符合在注重用户体验的应用上使用,他是HotSpot虚拟机第一款真正意义上的并发收集器,它是以标记-清除算法实现的,它的运作过程,相对于前面几种垃圾收集器更加复杂,分为四步:

            一.初始标记:暂停其他所有的线程并标记GC Root能直接关联到的对象,时间很短

            二. 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象,但当这个步骤结束,这个闭包结构并不一定包含所有可达对象,因为用户线程会不断地更新引用域,所以GC线程无法实时地分析对象的可达性,所以这个算法里会跟踪记录发生引用更新的地方

并发标记过程占用整个GC70%-80%的时间

            三. 重新标记:修正并发标记期间因用户程序继续运作而导致标记长生变动的那一部分对象的标记记录,这个阶段停顿时间比初始阶段稍长

            四. 并发清除:开启用户线程,同时GC线程开始对未标记的区域作清扫,时间很长

            优点: 并发收集、低停顿

            缺点:

                一. 对CPU资源敏感

                二. 无法处理浮动垃圾

                三. 它使用的回收算法是标记-清除算法,会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让JVM在执行完标记-清除后再做一次整理

                四. 执行过程中的不确定性,会存在上一次垃圾回收还没有执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理时出现,一边回收,系统一边运行,也许没回收完就再次触发Full GC,也就是‘concurrent mode failure’,此时进入Stop The Word(停止所有用户线程),用Serial Old垃圾收集器来回收

        相关参数:

            (1)-XX:+UseConcMarkSweepGC:启用CMS

            (2) -XX:ConcGCThread:并发的GC线程数

            (3) -XX:+UseCMSCompactAtFullCollection:Full GC之后做压缩整理(减少碎片)

            (4) -XX:CMSFullGCBeforeCompaction:多少次Full GC之后压缩一次,默认是0,代表每次Full GC之后都会压缩一次

            (5) -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发Full GC(默认是92,这是百分比)

            (6) -XX:+UseCMSInitiatingOccupancyOnly:只是用设定的回收阀值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM在第一次使用设定值,后续会自动调整

            (7) -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor GC,目的在于减少老年代对新生代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC好事80%都在标记阶段

    ⑤. G1收集器(-XX:+UseG1GC)

            G1:Garbage-Frist,是一款面向服务器的收集器,主要针对配备多颗处理器及大容量内存的机器以及高概率满足GC挺短时间要求的同时,还具备高吞吐量性能特征

基本特性:

    一. 将堆划分为多个大小相等的独立区域(Region,JVM最多含有2048个Region)

    二. 一般Region大小等于堆大小除以2048

    三. 保留了年轻代和老年代的概念,但不再物理隔阂了,他们都是(可以不连续)Region的集合

    四. 默认年轻代对堆内存的占比是5%

    五. Region的区域功能可能会动态变化,一个Region可能之前是年轻代,垃圾回收之后又可能变成了老年代

    G1对大对象的处理:G1有专门分配大对象的Region叫Humongous区,在G1中,大对象的判定规则是一个大对象超过了一个Region大小的50%,Full GC回收该区对象

    G1垃圾收集过程

        一. 初始标记(STW):暂停其他所有的线程,并记录下GC Root直接引用的对象,速度很快

        二.并发标记:用一个闭包结构去记录可达对象,跟踪记录发生引用的地方

        三. 最终标记(STW):修正并标记期间因用户程序继续运作而导致标记长生变动的那一部分对象的标记记录

        四. 筛选回收(STW):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数-XX:MaxGCPauseMillis来指定)来制定回收计划

    G1垃圾收集分类:

        (1) Young GC:Young GC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMillis设置的值,那么增加年轻代的Region,继续给新对象存放,不会马上做Young GC,知道下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMillis设置的值,那么就会触发Young GC

        (2)Mixed GC:不是Full GC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设置的值则会触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及大对象区

        (3) Full GC:停止系统程序,然后采用单线程进行标记,清理和压缩整理,空闲出来一批Region来供下一次Mixed GC使用,这个过程非常耗时

  9. 逃逸分析

    JVM三种运行模式

        ①.解释模式(Interpreted Model):只使用解释器(-Xint强制JVM使用解释模式),执行一行JVM字节码编译一行为机器码

        ②. 编译模式(Compiled Model):只是用编译器(-Xcomp JVM使用编译模式),先将所有JVM字节码一次性编译为机器码,然后一次性执行所有机器码

        ③.混合模式(Mixed Model):依然使用解释模式执行代码,但是对于一些“热点”代码采用编译模式执行,JVM一般采用混合模式执行代码

对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中

(设置参数:开启:-XX:+DoEscapeAnalysis 关闭:-XX:-DoEscapeAnalysis)

你可能感兴趣的:(浅析JVM虚拟机)