JVM简单介绍(内存结构、垃圾收集器,回收算法)

JVM(java虚拟机)就是帮助我们把代码加载成计算机可以识别的语言然后交给计算机去执行,JVM被JRE包裹着,而JRE被JDK包裹着,程序之所以可以运行,是因为我们在我们的电脑上配置了JDK的运行环境,JDK呢包含了我们所需要的基本的开发包、然后就是包含了我们准备说的JVM。

JVM主要由三个系统构成:类加载系统、运行时数据区(内存结构)、执行引擎

加载系统:加载实际上就是加载我们的.class文件(将我们的.class文件从磁盘加载到内存),把它加载成计算机可识别的字节码文件,虽然加载有很多的类加载机制,但我们这里只说一个比较大众的,那就是“双亲委派机制”,这个就是我们刚学的时候记得类的加载顺序,先加载静态的后加载动态的,等等....我在这里就不多说了,大家知道这个就可以了,不过双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况,例如线程上下文类加载器(Thread Context ClassLoader)。

内存结构:JVM总的分为两部分,一个是共享内存,一个是私有内存,共享内存里面又分为两部分:堆和方法区,私有内存又分为三部分:栈、本地方法栈、程序计数器,共享内存,顾名思义就是都可以使用的内存,也就是说我们的共享内存所有的线程都可以访问到的,所以也只有这么一个,而私有内存顾名思义都是自己的呗,所以每个线程都有自己独立的私有内存,下面我们就分别说一下这两个内存。

共享内存:

1、堆:我们都知道堆是存放new出来的对象的,堆主要分为两个部分,新生代和老年代,在内存占比来说新生代大概占三分之一,老年代大概占三分之二,而新生代又分为两个区域,一个是伊甸区,一个是辛存区;比如我们刚刚new出来的对象就会被放在伊甸区,如果这个对象非常大的话会被直接放到老年代中,比较小的会在伊甸区,辛存区内放的就是我们使用频率略多的对象,如果我们使用的频率再次增多的话,就会把这个对象放到老年代中,所以老年代中存放的就是我们经常使用的对象,一般不会被垃圾回收机制回收,如果我们的使用频率再次增加的话,我们会把这个对象放到永久代中,也就是方法区中。

 

这里要说一下,当我们的伊甸区满了之后,JVM会对这个区域进行一次垃圾回收,将常用的对象放入辛存区,不用或者很少用的对象进行回收,释放内存空间,这里的垃圾回收也叫做yongGC,它的特点就是快,让用户感觉不到你已经进行了一次垃圾回收,虽然概念上说在伊甸区进行垃圾回收时不会停止用户的线程,但是实际上还是停止了,只不过是因为时间太快让用户没有感觉到停顿。

在辛存区呢也有两个区域,一个是from区一个是to区,当对象从伊甸区放到辛存区的时候会先放到from区,当from区满了之后,会再次进行一次垃圾回收,然后把剩下的对象放到to区,当to区再满了之后,也会进行一次垃圾回收,然后再放回from区,如此循环15次之后,JVM会将这些对象放入老年代中。

2、方法区:是存放方法体的,还有那些静态的、final修饰的常量都是存放在我们的方法区里面的,方法区也叫永久代(jdk1.8之前,之后称之为元空间),它是一个比较大的内存空间,里面放的都是一些长期存活的对象和比较大的对象。

 

当老年代满了之后,JVM会进行一次fullGC,它是一个比较重的GC,因为数据量大,回收速度慢。

 

 私有内存:

栈:存放我们的new出来的对象的地址、局部变量、操作数栈、动态链接和方法出口

       局部变量:就是我们在方法体中声明的变量,只能在当前方法使用

       操作数栈:我们的程序进行计算的时候就是在这个操作数栈进行的,比如简单的加减乘除(JVM会将要计算的数据先放到局部变量中,然后复制一份放到操作数栈中进行计算,计算完成后会把计算结果返回,而复制到操作数栈的数据会直接丢弃掉),在程序中能操作的数据一定是在操作数栈中,局部变量中的数据是不会动的,要操作的时候就会复制一份到操作数栈中进行操作。

        动态链接:就是记录我们当前方法的地址(就是这个方法的路径)

        方法出口:比如我们当前这个方法是哪个方法调用的,当我们执行完这个方法后可以找到回去的路,这个就是所谓的方法出口

本地方法栈:我们有很多东西不是由java执行的,是计算机是CPU帮你执行的,而计算机帮你执行的部分就叫做本地方法栈,到在java中如果一个方法需要在本地方法栈执行,会用一个native关键字修饰

程序计数器:就是代码执行的时候计算机自己的一个标识,JVM计算会分为很多步,每一步都会用一个数字进行标识,而这些数字就是放在我们的程序计数器中。


JVM调优一般有两种方式,一种是减少fullGC的次数,一种是减少它的时间,当我们多个用户同时操作程序,也就是多线程访问下,如果老年代满了,它会产生一个STW(stop the word),也就是把所有的线程都停止掉,留下一个专门进行垃圾回收的线程,这个线程会将所有的区域都清理掉,然后再去重新跑用户的线程,这样的话用户会感觉到卡顿,会让用户体验很不好,,而这个清理过程就叫fullGC,我们经常说的GC调优就是为了减少fullGC,减少卡顿。

一般我们会使用一些垃圾回收算法进行回收,但是什么样的对象可以被回收呢?这里有几种方法:

        1、引用计数法:就是给对象添加一个引用计数器,每当有一个地方引用,计数器就加一,当引用失效,计数器就减一,虽然这个方法简单高效,但是现在主流的虚拟机中没有选择这个算法来管理内存,主要原因就是它很难解决对象之间的互相引用,如果存在互相引用的对象,那么这些对象永远不会被回收。

        2、可达性分析算法:这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点往下搜索,当它发现没有和“GC Roots”相连的话,就证明这个对象是不可用的,就算是两个对象像话引用,但是如果没有被“GC Roots”引用的话也会被回收,它和引用计数法的区别就是解决了相互引用不会被回收的缺点,现在大部分用的是这个算法。

而垃圾回收算法也有几种:

        1、标记清除算法:它是最基础的收集算法,这个算法分为两个阶段“标记”和“清除”,首先标记出要回收的对象,在标记完成后统一回收标记的对象,但是它有两个不足的地方,一个是效率问题,标记和清除两个得效率都不高;还有就是空间问题,标记清除后会产生大量不连续的空间碎片,这个属于比较老的算法,现在基本已经淘了。

        2、复制算法:这个算法会把内存分为两块,当一块内存满了之后,就将还存活的对象复制到另一块内存中,然后把满了的那块内存直接清空,这样每次的回收都是对内存区间的一半进行回收,比如我们上面说到的辛存区中的from和to就是使用的这种算法,这种算法的缺点就是只能使用一半的内存空间。

        3、标记整理算法:这个和标记清除算法基本一样,不过解决了标记清除算法会产生不连续的控件碎片的问题,这个算法是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。所以缺点也就显而易见了,因为要整理所以效率要比标记清除算法的效率低。

这是几种垃圾回收算法,接下来我们说一下垃圾收集器,垃圾收集器就是相当于我们点餐的时候的一种套餐,当我们选择了一种垃圾收集器的时候,它就帮我们把每个内存空间会使用什么算法都准备好了,我们只管用就行了,下面我们来了解一下几种垃圾收集器:

      1、Serial收集器:它是一个单线程收集器,是最古老的一个收集器,这个收集器进行垃圾回收的时候,会把所有线程全部暂停,直到它收集完成,所以会给用户带来不良的用户体验,但是它的优点也一样突出,就是简单、高效,因为它没有线程交互的开销,所以可以获得很高的单线程收集效率,这个算法它在我们的新生代使用的是复制算法,老年代使用的是整理算法。

        2、ParNew收集器:这个收集器实际上就是Serial收集器的多线程版本,除了是使用多线程进行垃圾收集之外,其余和Serial收集器是一样的,

        3、Parallel Scavengel收集器:(JVM默认的收集器)它和ParNew收集器基本上一致,就是由单个执行变成了并行,但是它比ParNew收集器增加了吞吐量的计算,吞吐量就是JVM在处理高并发时的效率问题,吞吐量越高效率就越高,吞吐量越低效率也就越低,ParallelScavenge收集器多了一个功能,它会利用电脑的CPU自动分配收集的效率;Parallel Scavengel收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的“MaxGCPauseMillis”参数以及直接设置吞吐量大小的“GCTimeRatio”参数。可以通过在JDK配置这两种参数:

  •         MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存回收花费的时间不超过设定值,不过这个时间的缩短是以牺牲吞吐量和新生代的空间来换取的,(可以这么理解,新生代的空间越小可以存储的对象也就越少,所以垃圾回收的时间也就会变快,但是回收的频率就会增多,当我们的垃圾收集器占用更多的CPU它的运行速度也就越快,但是相对的,它会占用其他线程的CPU,所以吞吐量会变小);
  •         GCTimeRatio参数 的值应当是大于0且小于100的整数,也就是垃圾收集时间占总时间的比例,也相当于吞吐量的倒数,假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集,它的默认值是99,也就是说允许最大1%的垃圾收集时间。
  •         因为设置这些参数很麻烦,很难根据需求来更改,所以收集器哈提供了另外一个很强大的参数:UseAdaptiveSizePolicy,这是一个开关参数,这个参数打开之后,就不需要手工指定新生代大小,伊甸区、辛存区的比例以及晋升老年代的条件等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(详细信息科参考:http://ifeve.com/useful-jvm-flags-part-6-throughput-collector/)。

        4、Serial Old收集器:是Serial收集器的老年代版本,它同样是一个单线程收集器,它主要有两大用途:一种是在JDK1.5之前的版本中与Parallel Scavengel收集器搭配使用。另一种是作为CMS(下面会介绍)收集器的后备方案。

        5、Parallel  Old收集器:它是Parallel Scavengel收集器的老年代版本,使用多线程和“标记整理”算法,在注重吞吐量和CPU资源的场合,都可以优先考虑使用Parallel Scavengel收集器和Parallel Ol收集器。

        6、CMS收集器:它的主要适合场景是对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用,CMS是为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿,它管理新生代的方式与Parallel收集器和Serial收集器相同,而在老年代则是尽可能得并发执行,每个垃圾收集器周期只有2次短停顿,整个并发收集通常包括以下几个步骤:

  • 初始化标记:这个阶段是标记从GCRoot直接可达的老年代对象、新生代引用的老年代对象,这个操作时并行执行的(可以通过参数CMSParallelInitialMarkEnabled调整)。
  • 并发标记:由上一个标记过的对象,开始tracing过程,用一个闭包结构去标记所有可达对象,但是在这个阶段结束,这个结构并不能保证包含当前所有的可达对象,因为该阶段是并发执行的,所以在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。
  • 预处理:这个可以通过CMSPrecleaningEnabled选择关闭该阶段,默认启用,这个阶段主要做两件事,一个是处理新声嗲已经发现的引用,就是将并发标记阶段本来没有被引用的对象,在并发标记的同时被引用而没有标记的对象进行标记。
  • 可中断预处理:这个阶段发生的前提是,新生代伊甸区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold(默认是2M),若果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段,(预处理和可中断预处理都是为了减少重新标记的STW时间)
  • 重新标记:这个阶段就是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
  • 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。
  • 并发重置:CMS清除内部状态,为下次回收做准备。

如果项目对于停顿时间要求比较高,可以此阿勇这种收集器,它的实现比较麻烦,它的流程是先初始标记出需要被清理的对象,速度很快,当然标记时会STW(stop the word)一下,接着使用并发标记GC Roots Tracing验证是都还在使用标记的这些对象,重新标记可以修正因为用户程序操作导致标记发生变化,最总并发清除到那些不用的对象。

缺点:

  • 对CPU资源很敏感,因为使用的是并发,并发代表着多线程抢占CPU资源,所以这可能会造成用户线程执行效率下降。CMS默认的回收线程数是(CPU个数+3)/4.,这个公式意思就是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,剩下的75%用户线程占用,这个还是可以接受的,但是,如果CPU资源很少,只有两个的话,按照公式CMS会启用一个GC线程,相当于GC占用了50%的CPU资源,这个可能会使用户线程的执行速度明显降低。
  • 浮动垃圾:并发清理阶段用户还在运行,这段时间就可能产生心的垃圾,新的垃圾在这次的GC无法清除,只能等到下次清理,这个就叫做浮动垃圾。
  • 空间碎片:这个上面有说过,这个是因为使用标记清除算法引起的,这里就不在细说了。

        7、G1垃圾收集器:这个收集器是在JDK1.7之后发布的垃圾收集器,并在JDK1.9中成为了默认的垃圾收集器。它是一款面向服务器的垃圾回收器,主要针对配备多颗处理器及大容量内存的机器,极高的满足了GC的停顿时间的要求,还具备了很高的吞吐量,这种垃圾回收器适用于大型的高并发项目。

G1回收器和CMS有三点不同:

  • G1垃圾回收器是compacting的,因此其回收得到的空间是连续的。这避免了CMS回收器因为不连续空间所造成的问题。如需要更大的堆空间,更多的floating garbage。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • G1回收器的内存与CMS回收器要求的内存模型有极大的不同。G1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的;
  • G1还有一个及其重要的特性:软实时(soft real-time)。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。

具体的G1垃圾回收器算法和数据结构在这里我就不说了,可以去网上找找相关资料。

最后我们简单说一下JVM调优,JVM调优主要就是调整两个指标,一个是减少STW的时间,也就是垃圾收集器做垃圾回收时中断用户线程的时间,再一个就是增加吞吐量,就是垃圾收集的时间和总时间的占比。

调优主要从两个方面考虑:

  • 分析日志查看是否存在问题
  • 分析GC原因,调优JVM参数

不同情况使用不同的调优策略

 

以上就是我对JVM的一些理解,如果有什么不对的地方请多指教,让我们一同进步!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值