浅谈JVM

逼逼几句:

        jvm这个词对我们java程序员来说并不陌生,我想大家从刚开始接触java的时候应该就听说了,但是对于一些刚入行的朋友们来说可能是玄而又玄的,但是想要成为资深程序员,不可能对于jvm一点都不了解,所以这里浅浅的介绍一下,如果有没到位的地方,欢迎大家指正。

一、什么是JVM

         jvm(JavaVirtualMachine)java虚拟机,从字面意思上就可以知道这是虚构出来的机器,jvm它是java可以跨平台的核心,只要执行class文件就可以在各个平台上运行了,jvm负责将字节码编译成机器码,并提供内存管理和垃圾回收等环境的支持。而jvm它到底是怎样的呢,请允许我为大家介绍一下。

二、JVM组成

        

画得真的丑)

         JVM大致可以分为以上组件:

 1、运行时数据区:线程运行涉及到区域

          运行时数据区也叫jvm内存模型,对于现阶段来说它也是比较关键的,主要由以下几个部分组成:

                方法区/元空间:

        在jdk1.8之前,一直都是叫做方法区,它是堆的一个逻辑部分,而在1.8之后喜提改名卡叫元空间了,直接存储在本地内存中,元空间的话主要时存放类、常量、静态变量和编译后的代码等

                虚拟机栈:

        虚拟机栈它是线程私有的,每个方法都会创建一个栈帧,根据调用的先后顺序一层一层的进入虚拟机栈进行压栈,每个虚拟机栈中都有若干个栈帧,一个栈帧就对应代码中的一个方法,而从调用到结束对应了一个先入后出的过程,简单示意下:

      

而虚拟机栈中还有:

  1. 局部变量表:用来存放方法的参数和局部变量
  2. 操作数栈:用于计算的临时数据存储区
  3. 动态链接:用来转换方法的内存地址来直接使用的
  4. 返回地址
                本地方法栈

        本地方法栈和虚拟机栈的区别就是本地方法栈执行的是本地方法库的方法(为Native方法服务),而虚拟机栈执行的是java的方法。

                程序计数器

        程序计数器是线程私有的,可以看作是执行字节码的信号指示器,主要作用是用来确定指令的执行顺序。

                堆

        它是GC也就是垃圾回收的主要区域,主要用来存储对象和数组,它也是java内存管理的核心区域,在《java虚拟机规范》中提到,所有对象实例和数组都应该在运行时分配在堆上。在栈帧中有着对对象和数组的引用地址。

 2、类加载器:负责加载class文件

        负责加载字节码class文件的,类加载器是一个负责加载类的对象,而主要负责加载这两个字,每个java类都有一个引用指向加载它的类加载器,类加载器又分成一下几个部分;

        启动类加载器

                主要用于加载JDK的核心类库,在我们配置环境时,有个JAVA_HOME,而启动类加载器就负责加载它的lib下面的所有类。

        扩展类加载器

                用于加载lib/ext下面的jar包、类和背java.ext.dirs系统变量所指定的路径下的所有类。

        应用程序类加载器

                针对我们的类加载器,用来加载当前应用下的classpath下的所有jar包和类。

        自定义类加载器

                可以通过自己继承ClassLoader来自定义实现,通过传递正确的URL,就可以加载指定路径的类库或者jar包,可用于对一些加密了java代码进行解密再加载

        而类加载器主要是有一个双亲委派的机制,这种机制保护了java程序的安全性和稳定性,那什么是双亲委派呢?就是当一个java类使用时,jvm首先会到最大的类加载器也就是父类加载器去找被使用的类,找到了说明被该类加载了,可以直接返回引用,如果没找到,就将请求委派的子类加载器进行查找,如此这样递归下去,如果没找到则抛出ClassNotFoundException异常。这样做保证了安全性和稳定性同时也避免了类的重复加载(因为一旦找到了则直接返回使用),节省了内存空间。而双亲委派也分为向上委派和向下委派:

                向上委派:自下而上,一个类收到加载请求后,直接将这个类交给父类去完成,父类没找到后又交给父类的父类去执行。

                向下委派:自上而下,当父类加载器在收到请求后,发现无法加载该类,那么会向子类传递,让子类来加载这个类,一次类推

        类加载器执行流程

                大致流程分为这几个:加载—>验证—>准备—>解析—>初始化,这几个过程,流程图如下:

3、本地方法库

        本地方法指的是用native来修饰的方法,这种方法不是通过java代码写的,是其他语言编写的动态链接库

4、执行引擎

        执行引擎里面包含临时编译器和GC(也就是我们常说的垃圾回收)

        临时编译器

         临时编译器就是将字节码我文件中的命令编译成计算机机器操作命令去执行,以此来提高java代码的执行率,因为解释执行的方式就效率来说比编译执行要低很多,就是因为临时编译器的原因,它可以实时的对字节码进行编译,转换成本地机器码

        GC(垃圾回收)

        进行标记,被标记的就是垃圾,GC会对其进行回收,这也是我们常说的JVM优化的比较重要的部分,因为在垃圾回收的时候,程序是会进行暂停的,优化的目的就是为了减少垃圾回收的停顿时间。垃圾回收的主要客户就是堆。

三、JVM的垃圾回收

        我们经常听说jvm优化这个名词,为什么需要优化呢,这是因为在jvm垃圾回收的时候,我们的程序会进行停顿,所以这也是我们优化的一个方向——减少垃圾回收引起的停顿时间。而JVM垃圾回收就是让我们不用担心内存管理,让GC来帮我们进行清理

        主要的垃圾标记算法

      1、引用计数法

        给每个对象添加一个引用计数器,记录该对象的引用数量,当计数器为0时,表示该对象没有被引用,就会被标记成垃圾,再用垃圾回收算法进行清除,引用计数法有一下优缺点:

        优点:

        1、实时性比较好,当一个对象的计数器为0时直接标记

        2、简单高效,容易理解

        缺点:

        1、当两个垃圾对象互相引用时,他们的计数器是不为0的,所以也不会被标记成垃圾

        2、有额外的开销,因为每个对象都需要去维护一个计数器

        2、可达性分析算法

        在对象的根上面再添加一个根,作为起始根,这样每个对象对于起始根都有直接或者间接引用的关系,如果没有就进行第一次标记,用官方话术说的话就是判断此对象是否有必要执行finalize()方法(如果覆盖或者被调用过那么对象就会被回收),后面GC会进行第二次标记,只需要对该链中的任何一个对象关联上就可以不被标记,如果还是不行则会被回收,示意图如下:

        如果所示,B和C对于起始根来说都直接或者间接有联系,这种就不会被标记

        如图所示,A、B和C都没有直接关联上起始根,会被进行第一次标记

 主要的垃圾回收算法

        1、标记清除算法

        

如图所示,插了小旗子的地方就会被标记成垃圾,然后进行清除,但是这样容易造成内存碎片化的风险,并且效率也不高 

             2、复制算法

如图所示,是将内存等分为两块,每次存储只是用其中的一块,用完了之后就把存活的对象全部复制到另一边,然后把左边的全部清理掉, 这中算法的缺点是实际的使用内存只有原来的一半

        3、 标记整理算法

如图所示,先对可用的对象进行标记,然后将它们向一边移动,最后将除了可用对象的全部清除 

        4、分代收集算法

        这里就不得不提一下JAVA自带的监控工具了,分别是jconsole.exe和jvisualvm.exe,他们在目录jdk下面的bin中可以找到:

                                        ​​​​​​​        

我们在这里就借助jvisualvm.exe来进行演示,打开它可以看到:

       

我们所谓的分代收集算法就是将堆内存分为老年代(Old)和新生代,新生代又分为Eden(伊甸区)和两个Survivor(幸存者区),因为新生代的对象生命周期都很短暂,只需要将那些存活的对象复制下来即可,所以采用复制算法来进行清除;老年代中的对象都是存活率比较高或者比较大的对象或者数组,所以采用标记整理算法和标记清除算法来进行。从图中可以看出,老年代的清除时间比新生代的时间要多很多。上面我们说到了,垃圾回收的时候会进行暂停,新生代的暂停时间很短,而老年代的暂停时间很长,一般来说是新生代的10倍以上

        GC的流程

        1、大多数情况下,新对象都在Eden区,当Eden区没有内存进行分配时,会进行一次新生代回收(Minor GC ),清理掉Eden中没有用的对象

        2、清理后如果存活对象小于Survivor的可用空间的话则会进入,不然直接进入老年代中

        3、我们的虚拟机为每个对象都定义了一个计数器,每执行一次新生代回收那么计数器就加1,当存活对象的计数器到达15(默认)此之后,进入老年代中

        4、如果进行了新生代回收之后,Eden还是没有足够的空间给一个对象时,表示这个对象很大,直接进入老年代中

 常见的垃圾回收器

        1、新生代Serial

        这是针对新生代的单线程收集器,采用的是复制算法进行垃圾收集,它在垃圾收集时,所有用户线程必须停止,如图所示:

       

        2、 新生代ParNew

        它其实就是Serial的多线程版本,没有其他的区别,如图所示:

         3、新生代Parallel Scavenge

        它也是用于新生代的多线程收集器,它是通过控制垃圾收集的的停顿时间来达到控制一个吞吐量的收集器,它采用标记整理算法进行垃圾回收,如图所示:

         4、老年代Serial Old

        它其实是新生代Serial的老年代版本,也是单线程,采用标记整理算法,适用于单核服务器等

        5、老年代CMS

        它可以看作是垃圾回收线程和用户线程并发执行的,整个过程主要分成四个步骤:

                1、初始标记,标记可以被根直接关联到的对象

                2、并发标记,标记出全部的垃圾对象

                3、重新标记,修改因并发标记的时候因用户程序运行而导致变化的对象标记记录

                4、并发清除,采用标记清除算法清除垃圾对象

                如图所示:

当然它的缺点也是比较明显的:    

                1、在并发清理阶段,因为用户线程也在运行,所以会不断的产生垃圾,不能及时的进行回收

                2、因为它也是基于标记清除算法实现的,所以也会有内存碎片化的风险

                3、对CPU比较敏感,占用CPU资源越多,吞吐量越小

        6、老年代Parallel Old

它是新生代Parallel Scavenge的老年代版本,也是一个多线程的垃圾收集器,采用标记整理算法

        7、G1收集器

它是jdk1.7之后正式引用的商用的收集器,现在是1.9默认的收集器了,它的回收返回是整个堆内存,主要是将整个堆内存划分成多个同等大小的独立空间,每个空间有一个Remembered Set来实时的记录该区域的数据和数据之间的引用关系,标记时直接查看这些数据之间的引用关系就可以知道是否应该被清除,主要分成以下几个步骤:

                1、初始标记,标记可以被根直接关联到的对象,单线程执行

                2、并发标记,从根开始对堆中的对象进行可达性分析,找出存活对象,可以和用户线程并发执行

                3、最终标记,修改因并发标记的时候因用户程序运行而导致变化的对象标记记录

                4、筛选回收,堆各个独立空间的回收价值和成本进行排序,根据期望的GC停顿时间来指定回收计划

如图所示:

这些主要都是对JVM比较浅的介绍,肯定会有很多解释不充分的地方,如果有不正确或者不足的地方请大佬们指正,请多多支持谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值