Skip to content

JVM相关

javahongxi edited this page Sep 9, 2019 · 5 revisions

本文内容

  • JVM内存划分
  • 垃圾回收
  • 类加载过程
  • 类加载器
  • JVM参数
  • JVM工具

垃圾回收

一.对象查找

    在对对象回收之前,需要首先查找出亟待回收的对象,在JVM中,采取"根检索"算法来查找"死亡"的对象;这个算法的基本思想是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径为对象引用链;如果一个对象到GC Roots上没有任何引用链可达,那么此对象就是"不可达".可以作为GC Roots的对象大概可以分为如下2种:

    1) 方法栈(包括本地方法栈)的本地变量表中的引用的对象.[表明这些对象正在被使用,不能被GC]

    2) 方法区中的类静态属性引用的对象以及常量引用的对象.[这些引用对象,几乎可以认为是不会被回收的,作为Roots非常方便]

    标记一个对象是否可以被回收,不仅和GC Roots的可达性有关(适合强引用),而且还和对象的引用类型有关,例如Soft reference将会在即将OOM时被回收,week reference在每次GC(Full GC?)都将被回收.针对一个对象覆盖了finalize方法,在被回收之前此方法将会被执行,此后对象被标记为"已执行",如果此对象在finalize方法中再次"逃逸"(重新获得对自己的引用),那么此对象很有可能在本次GC后幸存,但是此后如果对象再次被标记为GC,将直接被回收,不会再执行finalize方法(此方法只会被执行一次).

 

    对于方法区,仍然可能被回收,比如"无用的常量池"和"已经卸载的类信息"都会被GC.对于常量池,如果一个常量没有被任何对象所引用,那么它将会被回收;对于类信息比较特殊,因为Class信息可以被JVM预加载也可以在运行期间动态加载,那么类信息回收的条件需要全部具备如下情况:

    1) 如果此类没有任何对象实例存在

    2) 此类的类加载器已经被回收

    3) 此类的Class对象,没有被任何对象引用(特别是反射机制中)

    其实JVM还提供了一个可供参考的参数-Xnoclassgc,此参数用来控制是否对class进行回收;当JVM中存在大量类反射操作/动态代理/自定义类加载器/CGLIB等使用场景时,极有可能会在运行时动态生成大量的代理类,会对方法区的存储空间带来冲击,建议开启此参数(默认开启).

 

二.垃圾回收算法

  1. 标记-清除(Mark and sweep):标记"亟待清除"的对象,然后清除;这种算法很简单,不存在内存块的移动,对于被清除的对象所占空间直接被释放(NULL);但是问题也很明显,每次回收之后,内存地址将不再处于连续状态,即出现内存碎片问题,对于大对象内存申请,将可能找不到合适的连续的空间存储,尽管整体上具有较多的空闲内存.

  2. 复制算法:必须具备2个独立的内存区域(空间尺寸可以不同),每次回收,都会首先标记存活的对象,然后将这些对象复制到第二个空闲的内存区域中,然后清除先前的内存区域.此算法,完全避免了内存碎片问题,但是它的一个潜在缺点就是需要一个"冗余"的内存空间,这个思想被使用在了JVM新生代的内存模型和GC中.

  3. 标记-整理:它和1)相似,但是此算法对对象的整理上,有内存移动(对齐)的操作;此处的"整理"就是让所有的存活的对象"压缩"并对齐,最终保证所有的内存都是连续的,不存在内存碎片.JVM对旧生带的回收会采用1)算法,经过多次Full GC后,旧生带会有大量内存碎片,此时结合3)的整理方式进行“压缩”以减少内存碎片。

三.垃圾回收器

img

     JVM目前存在上述垃圾回收器,根据其所作用的Heap区域或者作用不同,分为2大类.其中SerialNew,ParNew,Parallel Scavenge适用新生代回收(minor GC),CMS/SerialOld/ParallelOld适用于旧生代.真对JVM环境,上述2部分可以有多种组合,比较常见的有:ParNew + CMS + SerialOld(失败担保);Parallel scavenge + Parallel Old;SerialNew + SerialOld[Client模式下];其中前两者适用于Server模式下,后者为client模式下默认组合.

    1) Serial收集器:单线程收集器,适用于minor GC,在垃圾回收期间,将会阻塞用户线程执行,即"Stop the world",直到收集结束.此收集器为client模式下默认收集器,当机器的CPU为单核或者内存<2G时等将会采用此收集器.采取"复制算法"。

    2) ParNew收集器:多线程并行收集器,它只不过是serial收集器的多线程版,收集效率较高,但是仍然存在"Stop the world"(暂停应用,多线程并行标记“死亡”的对象,单线程复制剩余的存活的对象);可以与CMS收集器配合使用.在多核CPU(>2)环境中,ParNew效率稍高;可以通过-XX:ParallelGCThreads来指定线程的数量."复制算法".

    3) Parallel Scavenge:它和ParNew一样为多线程收集器,也采取了复制算法;此收集器主要是用在"吞吐量优先"的环境中,"吞吐量"即为"JVM实际服务时间"/(JMV服务时间 + GC耗时);此收集器提供了多个控制GC时间的参数,例如:-XX:MaxGCPauseMillis控制GC最大停顿时间,-XX:GCTimeRatio来控制GC耗时比率(默认为99,即GC耗时为1%);在此收集器下,可以使用-XX:UseAdapterSizePolicy让JVM自适配新生代/旧生代的尺寸,以及晋升到旧生代的时机等;此参数可以让JVM帮助我们调优."复制算法".

    4) SerialOld收集器:单线程收集器,适用于旧生代,采用了"标记-整理"算法,此收集器在client模式下使用,最主要的是它作为CMS收集器在"分配担保失败"时,采取的后备方案;

    5) ParallelOld:它和Parallel Scavenge对应,是旧生代的多线程版本.采取"标记-整理"算法.

    6) CMS:Concurrent Mark Sweep,即并发"标记清除";它是一种最小停顿时间为目标的收集器.它采取多线程方式的"标记"对象(会暂停应用程序),在清理阶段是允许用户线程继续运行,这就是并发(注意和"并行"的区别);因为CMS在运行期间,允许用户线程继续进行,也就意味着标记之后,仍会有新的"垃圾对象"生成(即在回收时,仍然需要确保用户对象可以被继续创建);出于此原因,CMS设定了空间占比参数(--XX:CMSInitiatingOccupanFraction,默认为68%),即当旧生代的使用空间达到68%时,就会触发Full GC,以防止收集期间大量对象被创建而造成的空间不足问题.如果不幸预留的空间不足,将会出现一次"Concurrent Mode Failure",进而采取"失败担保"策略,即使用SerialOld重新进行Full GC.

    此外,CMS还有个缺陷就是内存碎片,因为其采用了"标记清除"算法,碎片将难免;这会给大对象的创建带来麻烦,可能旧生代剩余空间足够但是仍无法满足大对象创建时,会额外的进行一次内存碎片整理;-XX:+UseCMSCompactAtFullCollection表示在full gc之后是否进行一次内存整理;-XX:CMSFullGCsBeforeCompaction=可以设定多少次Full GC之后进行一次内存碎片整理.

 

JVM参数配置:

-XX:[+ | -]UseSerialGC:使用Serial + SerialOld组合

-XX:[+ | -]UseParNewGC:使用ParNew + SerialOld组合

-XX:[+ | -]UseConcMarkSweepGC:使用ParNew + CMS + SerialOld(失败担保)组合

-XX:[+ | -]UseParallelGC:使用Parallel scavenge + SerialOld组合

-XX:[+ |-]UseParallelOldGC:使用Parallel scavenge + Parallel Old组合.




如下资料摘抄自网络:

  1. Minor GC:适用于新生代,当新的对象在新生代中无法满足时,将触发一次Minor GC;Minor GC在整个JVM周期中,执行较为频繁.(From或者to两个交换区中alive的那个满时会触发minor GC,如果一个对象过大超过了-XX:PretenureSizeThreshold将会直接在旧生带分配。)

  2. Major GC:适用于旧生代,当旧生代使用空间达到阀值或者新生代空间不足时,将会触发Major GC,即为Full GC,触发Major GC之前往往伴随着一次Minor GC.

  3. 对象进入旧生代的时机:Full GC将新生代对象迁移到旧生代,迁移的对象可能是"对象年龄"达到设定值(-XX:MaxTenuringThreshold=),也可能是当前新生代中存在大量"长时间存活的对象"(Minor GC保留下来);对于大对象的创建,将会直接分配到旧生代(-XX:PretenureSizeThreshold=,单位byte).

  4. 空间分配担保:在发生Minor GC时,虚拟机会检测之前每次晋升到旧生代的平均大小是否大于当前旧生代的剩余空间大小;如果大于,则直接进行一次Full GC;如果小于,则查看-XX:+HandlePromationFailure是否开启,如果开启则继续Minor GC,如果关闭,则改为Full GC;

在面试中我们会经常被问到full Gc相关的问题,比如什么情况下会发生full gc,如何去排查频繁发生full Gc的问题等。要想轻松自如的回答这些问题,我们就必须充分的去理解gc的触发条件,gc回收的内容,以及gc具体的执行过程。掌握了这3个要点,full gc相关的问题就易如反掌了。

一、gc的定义

GC,即就是Java垃圾回收机制。目前主流的JVM(HotSpot)采用的是分代收集算法。与C++不同的是,Java采用的是类似于树形结构的可达性分析法来判断对象是否还存在引用。即:从gcroot开始,把所有可以搜索得到的对象标记为存活对象。

二、gc的基础知识准备

要了解GC的触发条件,就要先对 JVM的内存结构有一定的了解。我们通常所说的GC主要是针对运行的数据区的。作为程序员要关注的区域主要有5块,分别是方法区(Method Area),Java栈(Java stack),本地方法栈(Native Method Stack),堆(Heap),程序计数器(Program Counter Register)。实际jvm在管理内存的时候,比这个分的更细致,只不过做应用程序开发,我们只需要关注这5块就可以了。

img

堆(Heap),是Jvm管理的内存中最大的一块。程序的主要数据也都是存放在堆内存中的,这一块区域被所有的线程所共享,通常出现线程安全问题的一般都是这个区域的数据出现的问题。

方法区(Method Area),与Heap一样,也是各个线程共享的内存域,这块区域主要是用来存储类加载器加载的类信息,常量,静态变量,通俗的讲就是编译后的class文件信息。

Jvm栈,与程序计数器一样,它是每个线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

程序计数器,个人感觉的他就是为多线程准备的,程序计数器是每个线程独有的,所以是线程安全的。它主要用于记录每个线程的执行情况。

通常我们所说的gc主要是针对java heap这块区域的。下面来了解一下heap区。 img

从图中我们可以看出jvm heap区域是分代的,分为年轻代,老年代和持久代。 JVM的堆区对象分配的一般规则:

  1. 对象优先在Eden区分配

  2. 大对象直接进入老年代(-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代)

  3. 长期存活的对象将进入老年代(在JDK8中测试,-XX:MaxTenuringThreshold=1的阀值设定根本没用)

  4. 动态对象年龄判定(虚拟机并不会永远地要求对象的年龄都必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄的所有对象的大小总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代)

  5. 空间分配担保

  6. 只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC

三、gc的触发条件

充分了解了jvm的内存结构之后,下面我们就来说说什么情况下会触发gc。触发full gc的情况主要有这几种:

(1)System.gc()方法的调用。此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。

(2)旧生代空间不足。旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

(3)Permanet Generation空间满了。Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space 。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。

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

四、gc回收的内容

知道了gc触发的条件之后,我们就能知道gc主要回收什么了?gc的主要作用是回收堆中的对象。通过可达性分析一个对象的引用是否存在,如果不存在,就可以被回收了。

四、gc的具体过程

那么,gc的是如何实现的,这个主要看是用的哪一种回收算法以及用的什么垃圾回收集了。回收算法主要有:

标记-清除 复制算法 标记-整理(Mark-Compat)算法 分代收集(Generational Collection)算法 这里针对不同的代,可以使用一些相对合适的算法。

新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;

老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清理”算法进行回收。

常用的垃圾回收器:

Serial收集器 ParNew收集器 Parallel Scavenge收集器 CMS(Concurrent Mark Sweep)收集器 G1(Garbage First)收集器(从JDK1.7 Update 14之后的HotSpot虚拟机正式提供了商用的G1收集器) 关于垃圾回收器的使用,这里也有一个组合建议共大家参考:

img

JVM内存划分

jvm为每个新创建的线程都分配一个堆栈。堆栈以帧为单位保存线程的状态。jvm对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈 都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实 现

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧的概念结构如下图所示: img

Class实例在堆中还是方法区中

https://www.cnblogs.com/xy-nb/p/6773051.html

首页

Java核心技术

Netty

RocketMQ深入研究

kafka深入研究

Pulsar深入研究

Dubbo源码导读

微服务架构

Redis

Elasticsearch

其他

杂谈

关于我

Clone this wiki locally