jvm 内存结构,GC相关内容和调优

参考博客:https://www.tpvlog.com/article/86

1. jvm内存结构

 大致的结构如上图所示。

注意:

  • 新生代的地方,HotSpot VM(虚拟机的一种实例)对新生代采用了复制回收算法来实现gc的垃圾回收。而传统的复制算法比较浪费空间,所以它将新生代又分为了3个区域,1个Eden,和2个Survivor区。
  • 方法区只存在于JDK1.8以前的版本,从JDK1.8开始,这块区域的名字改成了元数据区(Metaspace),元数据区直接使用本地内存,本地内存指的是直接使用物理机的内存。好处就是这样方法区就不再占用堆内存,如果不设置,JVM将会根据一定的策略自动增加本地元内存空间。如果你设置的元内存空间过小,你的应用程序可能得到以下错误:java.lang.OutOfMemoryError: Metadata space。当然JDK8也提供了一个新的设置Matespace内存大小的参数                   ( -XX:MaxMetaspaceSize),通过这个参数可以设置Matespace内存大小,这样我们可以根据自己项目的实际情况,避免过度浪费本地内存,达到有效利用。
  • survivor一般也通过命名为from 和 to来区分两块区域

1.1 方法区

方法区只存在于JDK1.8以前的版本,主要是存储从”.class“文件里加载进来的类,包括类的名称方法信息字段信息静态变量常量以及编译器编译后的代码等。该区是所有线程共享的

1.2 程序计数器

程序计数器是用来记录当前执行的字节码指令的位置。

当java类被编译后会生成.class文件,当jvm加载.class文件后,会通过字节码执行引擎去执行这些字节码指令。然后程序计数器就会记录当前执行到的字节码指令的位置

程序计数器是线程私有的,也就是说每个线程都有自己的程序计数器,记录当前线程执行到了哪一条字节码指令

1.3 Java虚拟机栈

Java虚拟机栈,其实是一种表示Java方法执行的数据结构。每个方法被执行的时候,都会创建一个栈帧(Stack Frame)用于存储局部变量表操作栈动作链接方法出口等信息。每个方法从被调用到执行完成的过程,其实就是一个栈帧在虚拟机栈中从入栈到出栈的过程。Java虚拟机栈是线程私有的.

调用任何方法时,为方法创建栈帧然后入栈,栈帧里存放了这个方法对应的局部变量之类的数据(也包括方法执行的其它相关信息),而创建的对象就是存放在堆中的。方法执行完毕后就出栈。

当出现一个方法中调用另一个方法的情况时,其实就是先后创建两个栈帧,然后入栈

1.4 本地方法栈

本地方法栈,其作用和Java虚拟机栈类似,区别在于本地方法栈是为虚拟机所使用到的Native方法服务,而Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。本地方法栈也是线程私有的。

JDK中的很多底层API,比如IO、NIO、网络等,会发现很多地方是调用的native修饰的方法

1.5 堆内存

Java堆内存,这是JVM内存区域中最重要的一块区域,存放着各种Java对象,是线程共享区域。

下面代码中,new ReplicaManager()创建了一个对象实例,这个对象实例的相关信息就存放在Java堆内存中:

public class Kafka {
    public static void main(String[] args) {
        ReplicaManager manager = new ReplicaManager();
        manager.loadReplicaFromDisk();
    }
}

main线程在执行main()方法时,会为其创建一个栈帧并入栈,栈帧中的局部变量manager存放的ReplicaManager对象实例在Java堆内存中的地址(不考虑对象逃逸的情况)

说明:对象逃逸大致就是指对象的生命周期只存在于方法中,并没有指向方法以外的成员变量,静态变量等,这时,对象会很大概率直接被创建在栈内存中,而不是堆内存,一旦方法执行完毕,栈帧出栈,对象就能被一起回收。

2. 对象存活判定

上面介绍了jvm的内存结构,当一个程序执行时,会创建大量对象,如果不对这些对象进行回收,很容易就会造成内存溢出。那怎么去判断对象是否是垃圾对象,需要进行回收呢?一般有两种办法,可达性分析算法和引用计数法

2.1 可达性分析算法

可达性分析算法的基本思路就是通过一系列的名为"GC Roots"的对象作为起始点,从这些起始点开始搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots到这个对象不可达),则证明此对象是不可用的,就可以被回收。

GC Roots包括:

  • Java虚拟机栈中的局部变量(指向着GC堆里的对象);
  • VM的一些静态数据结构里指向GC堆里的对象的引用,例如HotSpot VM里的Universe里有很多这样的引用;
  • 所有当前被加载的Java类(看情况);
  • Java类的运行时常量池里的引用类型常量;
  • String常量池(StringTable)里的引用。

上述大致就是在方法区中的常量池和栈中的变量,注意类中的普通成员变量不能作为GC ROOT,除非加上static关键字

画图说明

因为堆中的对象有可能会相互引用,所以可以看到有a指向b,b指向d这种情况。

现在来判断d对象是否需要回收?从栈帧中的变量(GC ROOT)开始看其引用链,发现a,b,c,d对象都是在该引用链上的,所以a,b,c,d对象都是不能被回收的,而e对象由于没有在任何一条引用链上,所以e对象是需要被回收的

代码说明

// 示例1:
public class Kafka {
    public static void main(String[] args) {
        loadReplicasFromDisk();
    }
    public static void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
    }
}
// 示例2
public class Kafka {
    public static ReplicaManager replicaManager = new ReplicaManager();
}

这段代码中,

示例1中由于 replicaManager 是栈中的局部变量,所以它可以当作是GC ROOT,所以ReplicaManager 是可达的,不需要回收 

示例2中由于 replicaManager是静态变量,存放在方法区中的,所以它也是可以当作是GC ROOT,也不需要回收。

那如果我在示例1中添加代码replicaManager = null,那么该变量就找不到堆中的对象了,所以堆中的a,b,c,d,e都会被回收。

 2.2 引用计数法

就是给堆中的每个对象一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是可以被回收的。那像我们2.1 中的图,b被引用了3次,那它的计数就是3。

这种方法的不足就在于不能很好的解决循环引用的问题。假设我添加代码replicaManager = null,那很显然应该是a,b,c,d,e对象都是需要回收的,但是由于他们的计数并不都是0,所以只会回收e。

所以java是没有使用引用计数法的

2.3 判断引用类型

可达性分析与Java的引用类型有关联,为了更好的管理对象的内存,更好的进行垃圾回收,JVM团队扩展了引用类型,从最早的强引用类型增加到强引用软引用弱引用虚引用四个引用类型:

  1. 强引用: 默认的对象都是强引用类型,如果JVM在对象存活判定时,通过GC Roots可达性分析结果为可达,表示引用类型仍然被引用着,这类对象始终不会被垃圾回收器回收。
  2. 软引用:在JVM内存充足的情况下,软引用是不会被GC回收的,只有在JVM内存不足的情况下,才会被GC回收。代码中实现使用SoftReference类包裹
  3. 弱引用:不论当前JVM内存是否充足,都只能存活到下一次垃圾收集之前,即只要发生GC弱引用对象就会被回收。ThreadlLocal中定义的ThreadLocalMap就使用到的弱引用。ThreadLocalMap的Entry,其Key就是一个弱引用对象。代码中使用WeakReference类包裹
  4. 虚引用:不会影响对象的生命周期,所持有的引用就跟没持有一样,随时都能被GC回收。在使用虚引用时,必须和引用队列关联使用。其使用场景是用来跟踪对象被垃圾回收器回收的活动。

弱引用示例:

public class Kafka {
    public static WeakReference<ReplicaManager> replicaManager = new WeakReference<ReplicaManager>(new ReplicaManager());
}

2.4 finalize方法

综上所述:有GC Roots引用的对象不能回收,没有GC Roots引用的对象,如果是软引用或弱引用,可能会被回收。

真正的回收环节,待被回收的对象其实还有一次机会拯救自己,那就是对象的finalize()方法。我们通过一段代码示例来看下:

public class ReplicaManager {
    public static ReplicaManager instance;

    @Override
    protected void finalize() throws Throwable {
        ReplicaManager.instance = this;
    }
}

假如有一个ReplicaManager对象马上就要被回收了(此时已经没有GC Roots到达它的链路),此时GC会首先调用下该对象的finalize()方法,看看它是否找了一个新的GC Roots来引用自己,比如上述代码中,GC发现有个静态变量instance引用了该实例,那GC就不会去回收它。

3. 分代收集

如第一节中的图所示,堆内存中有着新生代和老生代,方法区有着永久代。分代收集就是对不同的代采用不同的收集算法,一般常见的就是新生代采用parNew GC回收器(复制算法),老生代采用CMS GC回收器(标记整理).

文中提到的Minor GC ,Full GC可查看第7节说明

3.1 新生代

当在方法中创建对象时,由于在方法中创建的对象一般生命周期很短,大多数都是在方法结束后就失去引用了,所以通常这种“朝生暮死”的小对象都会在Java堆内存区域的"新生代"进行分配。

但是,并不是每次JVM都会进行回收,默认情况下当新生代的内存空间快被占满时,会触发一次“Minor GC”,此时才会进行回收。

3.2 老生代

以下情况,对象会被分配到老年代:

  1. 如果一个实例对象在新生代中,成功的在15次垃圾回收之后,还是没有被回收到,那么就会被转移到老年代(15次是Java虚拟机的规范,也可以通过JVM参数-XX:MaxTenuring Threshold设置)
  2. 对于一些大对象,会直接在“老年代“分配
  3. 在一次”Minor GC“之后,如果新生代中的存活对象过多,即使这些对象年龄没有达到15,也会直接进入老年代
public class Kafka {
    private static ReplicaFetcher fetcher = new ReplicaFetcher();

    public static void main(String[] args) throws InterruptedException {
        ReplicaManager replicaManager = new ReplicaManager();
    }
}

如上代码,由于fetcher是一个静态变量,其实例对象ReplicaFetcher会一直被该静态变量引用,而ReplicaManager对象则一直“朝生暮死”。

最初时,ReplicaFetcher对象和ReplicaManager对象都被分配在新生代。而ReplicaManager对象,当方法执行完成后,栈帧就会出栈,所以新生代里的ReplicaManager会被垃圾回收线程清理掉。而由于fetcher是一个静态变量,其实例对象ReplicaFetcher会一直被该静态变量引用,在15次回收后就会被分配到老年代

3.3 永久代

永久代就是方法区,方法区中存储着类的信息,当满足以下三个条件时,方法区里的类会被回收:

  • 该类的所有实例对象已经从Java堆内存中被回收;
  • 加载这个类的ClassLoader已经被回收;
  • 对该类的Class对象没有任何引用。

3.4 新生代的对象什么时候会进入老年代

一共有五种情况:

  • 新生代对象的年龄超过一定阈值(默认15);
  • 动态年龄判断
  • 大对象直接分配(避免新生代中出现屡次逃过GC的大对象,大对象在新生代的Eden和Survivor区的来回复制开销比较大)
  • Survivor区空间不足(Minor GC之后发现存活对象太多,没法放入Survivor区域)

3.5 空间分配担保机制

前面讨论了新生代的存活对象何时会转移到老年代,那么问题又来了,如果老年代区域的内存空间不足了怎么办?这里就涉及了空间分配担保.

所谓空间分配担保,指在执行任何一次Minor GC之前,JVM会检查老年代的最大连续可用空间是否大于新生代所有对象的总大小.如果大于,说明这次Minor GC肯定是安全的,因为老年代可以容纳新生代中的所有对象;

如果小于,则 JVM 会查看-XX:HandlePromotionFailure参数值,这个参数值表示是否允许担保失败:

  • 如果允许(HandlePromotionFailure==true),则看下老年代的最大连续可用空间是否大于历次Minor GC后进入老年代的对象平均大小。如果大于,就进行minior GC,如果这次Minior GC失败了,就会进行FULL GC(所谓FULL GC,就是既对老年代进行垃圾回收,也对新生代进行垃圾回收);如果小于,先进行FULL GC,再Minor GC。
  • 如果不允许(HandlePromotionFailure==false),则直接触发FULL GC,然后再进行一次Minor GC。

如果经过上面的操作,老年代可用空间最后发现还是不够,就会导致所谓的OOM内存溢出了。

总之,空间分配担保机制的核心目的就是避免频繁FULL GC,能先预判就先预判,实在不行才FULL GC,因为FULL GC的开销非常大,既要对老年代进行回收,也要对新生代进行回收。

总结:每次执行Minor GC前,先看下老生代的容量是否能放下整个新生代,如果放不下,就调用一次FULL GC进行垃圾回收,如果FULL GC后还不够,那就直接报内存溢出。

4. jvm核心参数配置

通过这些参数可以设置上述提到的新生代、老年代、永久代的内存区域大小:

-Xms:Java堆内存区域的初始大小;

-Xmx:Java堆内存区域的最大大小;

-Xmn:Java堆内存区域的新生代大小,扣除新生代剩下的就是老年代的大小;

-XX:PermSize:永久代初始大小;

-XX:MaxPermSize:永久代的最大大小;

-Xss:每个线程的栈内存大小。

-XX:SurvivorRatio: 用来设置新生代中eden空间和from/to空间的比例。默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10

-XX:NewRatio:配置新生代与老年代占比,如配置2,表示新生代和老年代占比为1:2,新生代占了堆内存的1/3

总结:

  • 在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
  • JDK1.8以后,方法区变成了“元数据区”,-XX:PermSize和-XX:MaxPermSize这两个参数,也相应的变成了-XX:MetaspaceSize和-XX:MaxMetaspaceSize。

5. 垃圾回收算法

5.1 复制算法

复制算法,主要用于新生代中对象的回收。其基本思路就是:将新生代内存按划分为大小相等的两块,每次只使用其中的一块,当一块内存用完了,将存活的对象移动到另外一块上面,然后在把已使用过的内存空间一次清理掉。

如上图所示,新生代内存被分为了A,B两个区域,红色的表示不需要回收的对象,黄色表示需要回收的对象。

创建对象时,只会使用一块区域A,当GC回收时会将红色对象全部转移至区域B,保证没有内存碎片(顺序占用区域B的内存),然后清空区域A。

优化:

上述复制算法的缺点很明显:即对内存的使用效率太低。比如我们给新生代分配了1G内存,那其实只有512MB是实际使用的,很浪费内存空间。那么如何来优化呢?

HotSpot VM 采用了一种做法,把新生代区域划分成了三块:1个Eden区(80%),2个Survivor区(各占10%),最开始,对象只在Eden进行分配,

如果Eden区快满了,此时触发GC会将Eden区中的存活对象转移到其中一块Survivor中,同时清空Eden。

下一次再分配空间时,依然在Eden区分配,然后触发GC,将Eden的存活对象和上一次使用的Survivor中的存活对象转移到另一块空白Survivor中,然后清空Eden和使用过的Survivor,循环往复。

这种内存划分方式的最大好处就是只有10%的空间是闲置的,无论是垃圾回收的性能、内存碎片的控制、内存使用率,都非常好。

5.2 标记整理算法

一般用于老年代的垃圾回收算法

标记整理算法,其实就是先标记存活对象,然后将存活对象都向内存端边界移动,然后清理掉端边界以外的内存,这样就可以避免出现大量内存碎片。如下图所示,其中的整理算法有很多种实现。

 标记整理算法的好处是:

  • 内存的完全使用,不需要像复制算法,分割内存
  • 清理后的内存空间是连续的

缺点就是整理算法一般都是挺耗时间的。

对于老年代来说,一般存放在其中的对象都是很少需要回收的,所以使用算法比较好

5.3 标记清除算法

如上图所示,标记清除算法就是先标记,后清除

标记-清除算法的比较大的缺点就是垃圾收集后有可能会造成大量的内存碎片,所以java一般不使用这种算法进行垃圾回收

6. 垃圾回收器

在新生代和老年代进行垃圾回收的时候,都需要使用回收器进行回收,不同的JVM 垃圾回收器会有所不同,不同区域一般也采用不同的垃圾回收器。JVM常见的垃圾回收器有以下几种。

6.1 Serial/Serial Old

Serial/Serial Old收集器是最基本也是最古老的垃圾收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程,也就是发生“Stop the World”。一般JVM都不再使用该收集器。一般用于PC端的java应用,如百度云盘的Windows客户端、印象笔记的Windows客户端等等。

Stop The World图解说明

如上图所示,当gc在回收垃圾时,其他的工作线程必须先停止,等到GC回收完成后,工作线程才能继续执行。

形象点讲就是, 妈妈在扫地时,你不能嗑瓜子了,要先停一下,她扫完你再继续嗑瓜子,如果边嗑瓜子边打扫,那就太费神了。

6.2 ParNew

ParNew收集器是Serial收集器的多线程版本。新生代并行回收,采用复制算法,老年代串行回收,采用标记整理算法。所以,该收集器一般只用于新生代。通过JVM的参数设置,可以显式指定使用ParNew作为新生代的垃圾回收器。-XX:+UseParNewGC,只要加入这个选项,JVM启动之后就会使用ParNew进行新生代的垃圾对象回收。在使用ParNew作为生产环境的垃圾回收器时,记得使用-server指定为服务端模式

6.3 CMS

CMS(Current Mark Sweep)收集器,目标是使回收停顿时间最短,也是多线程机制,采用标记整理算法,该回收器一般用于老年代,生产环境上也经常会使用该垃圾回收器与其它GC搭配使用。

CMS采取的策略是:垃圾回收线程和系统工作线程尽量并行执行

CMS在执行一次垃圾回收的过程一共为4个阶段:

  • 初始标记:就是标记“GC Roots”能够引用到的对象,会进入“Stop the World”状态
  • 并发标记:通过初始标记的对象,找到引用了该对象的其他所有对象,不会进入“Stop the World”状态。比较耗时
  • 重新标记:由于在并发标记时,可能会存在已标记的对象又失去了引用的情况,所以这一步是停止掉工作线程,进入“Stop the World”,对并发标记后的对象重新标记。虽然进入“Stop the World”,但速度是很快的,因为只是对第二阶段中因为并行而变动过的少数对象进行标记
  • 并发清理,清理标记的对象,该阶段垃圾回收线程和工作线程是并行运行的,由于并发清理需要将垃圾对象从各种随机的内存位置清理掉,所以也比较耗时。

性能分析:其中初始标记和重新标记阶段虽然会”Stop the World“,但是耗时很短,所以影响不大;并发标记和并发清理阶段虽然耗时较长,但是可以跟工作线程并行执行,所以影响也不大。

缺点:CPU消耗比较大

我们到现在还没看到CMS 的整理部分,那它是怎么处理内存碎片的呢?

如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续空间,触发Full GC。

CMS有一个参数-XX:+UseCMSCompactAtFullCollection(默认打开),表示是否要在Full GC之后进行Stop the World,停止工作线程,然后进行老年代的内存碎片整理。

还有另外一个参数-XX:CMSFullGCsBeforeCompaction,意思是执行多少次Full GC之后再执行一次内存碎片整理工作,默认是0,即每次Full GC之后都会进行一次内存碎片整理。

6.4 G1

G1 提供比“ParNew+CMS”组合更好的垃圾回收性能。

G1垃圾回收器是Jdk1.7的新特性之一,在Jdk1.7+版本都可以自主配置G1作为JVM GC选项。G1垃圾回收器可以同时回收新生代和老年代的对象,它一个人就可以搞定所有的垃圾回收。

G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然G1还保留着新生代和老年代的概念,但它们只是逻辑上的,新生代和老年代不再是物理上隔阂的,而只是一部分Region的集合,每一个Region既可能属于新生代,也可能属于老年代.

刚开始时Region谁都不属于,然后会先分配给新生代,当对象越来越多后,可能触发G1对这个Region进行垃圾回收,然后下一次,这个Region可能又被分配给了老年代,用来存放长期存活对象.

垃圾回收的预期停顿时间

G1最大的特点就是,可以让我们设置一个垃圾回收的预期停顿时间。比如我们可以指定:G1进行垃圾回收时,保证“Stop the World”的时间不超过1分钟。

之前,我们采用ParNew+CMS时,为了尽量减少GC次数,需要对JVM内存空间合理划分,还要配置各种JVM参数。但是现在,我们可以直接给G1指定一个预期停顿时间,告诉它一段时间内因垃圾回收导致的系统停顿时间不能超过多久,剩下的全部交给G1全权负责,这样就相当于我们可以直接控制GC对系统性能的影响

通过-XX:MaxGCPauseMills参数可以设定预期停顿时间,表示G1执行GC时最多让系统停顿多长时间,默认200ms。

回收价值

G1之所以能够做到控制停顿时间,是因为它会追踪每个Region里的回收价值。所谓回收价值,是指每个Region里有多少垃圾对象,如果进行回收,耗时多长,能够回收掉多少。然后在垃圾回收的时候,G1就会判断哪个Region更有回收价值.

根据回收价值进行GC,这个就是G1的核心设计思路

Region大小设置

G1的堆内存中,各个Region的大小是相同的,那么要分配多少个Region呢?每个Region的大小为多少?

JVM启动时发现如果采用了G1作为垃圾回收器(通过参数-XX:UseG1GC指定),那么就会自动设置,默认2048个Regio,然后通过堆内存的大小(-Xms-Xmx)除以2048就能得到Region的大小了。Region的大小必须为2的整数倍,如2MB、4MB、6MB等,可以通过-XX:G1HeapRegionSize参数手动指定,设置后,就会根据你设定的size大小计算region个数了。

动态Region

初始情况下,堆内存的5%空间为新生代的大小,以4G堆内存来算,就是200MB的新生代,约100个Region。但是在系统运行期间,Region的数量是动态变化的,不过新生代最多占比也不会超过60%。另外,一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。

可以通过参数-XX:G1NewSizePercent来设置新生代的初始占比,默认5%;通过参数-XX:G1MaxNewSizePercent来设置新生代的最大占比,默认60%。

Eden和Survivor

G1垃圾回收器的新生代也有Eden和Survivor的划分,同样通过-XX:SurvivorRatio=8设置比例。比如说,新生代最初有100个Region,那Eden就占80个,两个Survivor各占10个。

随着对象不停的在新生代分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

垃圾回收原理

和CMS相同,采用的标记整理算法,也是分4步。但是它是对新生代和老生代进行共同回收,而CMS只是针对老生代

停止回收

由于在执行回收阶段,基于复制算法,那就会不断的空出一些Region,一旦空闲的Region数据量达到了堆内存的5%,就会立即停止回收,那么本轮混合回收(Mixed GC)就结束了。可以通过参数-XX:G1HeapWastePercent配置这个空闲Region的占比,默认为5%。

回收失败

由于在执行回收时,需要将存活对象拷贝到其他Region中,如果万一在次过程中没有空闲的Region可以承载存活对象,就会触发Full GC。此时,JVM会立即停止程序,然后采用Serial Old收集器进行单线程标记、清除、压缩整理,空出一批Region,这个过程是非常缓慢的。

优点:

  1. 把每次执行回收的时间控制在我们设置的预期停顿时间范围内。
  2. 适合在大内存的机器上运行,可以完美解决大内存垃圾回收时间过长的问题

7. 各种GC分类

7.1 Minor GC/Young GC

当新生代的Eden区域被占满后,实际就需要触发新生代的GC,这就是所谓的”Minor GC“,也可以称之为”Young GC“。

触发时机:新生代的Eden区域被占满后。

7.2 Old GC

Old GC是仅仅针对老年代区域进行垃圾回收。

7.3 Full GC

Full GC则是针对新生代、老年代、永久代的全体内存空间进行垃圾回收.回收比较耗时间

触发时机:老年代空间不够。具体时机可细分为以下几种:

  1. 进行Young GC之前:如果老年代的连续可用内存空间 < 新生代历次晋升的平均大小,此时先触发一次Old GC清理老年代,然后再执行Young GC。
  2. 进行Young GC之后:如果存活对象要进入老年代,但是老年代的连续可用内存空间 < 存放对象的大小,此时必须触发一次Old GC。
  3. 老年代的内存使用率超过了92%,此时也会触发Old GC

8.4 Mixed GC

Mixed GC是G1垃圾回收器中特有的概念,在G1中,一旦老年代占据了Java堆内存的45%,就会触发Mixed GC,此时对新生代和老年代都进行垃圾回收。

触发时机:G1特有,老年代空间占据到Java堆内存的45%。G1新生代内存满了会触发Young GC

8.5 永久代GC

永久代一般存放着类信息、常量池等等。在进行Full GC的时候,会顺带对永久代进行GC,一般来说永久代里的东西是不需要回收的,如果永久代真的满了,回收之后也没腾出足够的空间来,就会抛出OOM异常。

9 调优

9.1 新生代调优

在系统内存不是很大的情况下,可以通过提升Eden和Survivor的空间,来容纳更多的新生代对象。但是,当新生代的内存空间太大时,需要考虑每次Young GC的时间成本,传统的ParNew回收器不太适合这种大内存场景,所以针对大内存机器建议使用G1进行垃圾回收

9.2 Full GC调优

当内存过小,导致新生代和老生代内存都不大,而数据又比较多的情况,很容易导致老生代内存溢出,而触发Full GC,这种情况下首先要考虑的显然就是扩内存。增大新生代和老生代内存,避免触发Full GC

9.3 System.gc()

禁止在代码中显式调用System.gc()方法,因为显示调用后,很有可能会触发Full GC。在访问量很高的情况下,System.gc()方法被频繁调用,会频繁触发Full GC。 所以GC需要完全交由JVM自己去处理

9.4 内存溢出

一般内存溢出就三个地方,方法区(元数据区),栈内存,堆内存

  • 方法区溢出:一般元数据区中的对象回收的条件是相当苛刻的,所以可以不考虑回收。JVM启动时,元数据区默认情况只会分配几十MB空间,所以生产环境一定要显式指定该区域的大小,一般512MB就足够了:--XX:MetaspaceSize=512m --XX:MaxMetaspaceSize=512m
  • 栈溢出:一般来说栈内存存放栈帧和方法中的参数,当栈帧出栈后内存就会即时的回收,但是如果出现像递归这种只入不出的情况,就会导致栈溢出.所以一般要注意代码写法
  • 堆溢出:Young GC过后的存活对象首先会先尝试进行一块Survivor区,如果Survivor区无法容纳,则尝试进入老年代,如果此时老年代也满了就会触发Full GC。但是,如果Full GC之后,老年代的空间还是不够, 这时只能抛出内存溢出异常了。所以,堆内存溢出的原因,总结起来就是一句话:有限的内存中放了过多的对象,而且大多数对象是存活的,此时要继续放入更多对象已经不可能了,只能抛出内存溢出异常。所以解决的话就是针对代码的优化,内存大小的设置等。基本的分析思路就是dump出事发现场的内存快照,然后通过MAT进行查看,分析出内存占用最多的对象,然后分析线程调用栈,找到代码位置,最后进行优化即可

9.5 内存溢出和内存泄漏

内存泄漏表示:对象已经没有被应用程序使用,但是垃圾回收器没办法移除他们,因为还在被引用着,比如我在类中定义了一个静态成员变量且同时还给这个变量new了一个对象。这种解决办法,比如可以使用懒加载模式,或者使用上面说过的弱引用或软引用。

内存溢出就表示,我需要2G内存存放对象,但是jvm一共就1个G内存,内存不够放,解决办法上面说过了

10 打印GC日志

需要在系统的JVM参数中加入GC日志的打印选型:

  • -XX:+PrintGCDetails:打印详细的GC日志
  • -XX:+PrintGCTimeStamps:打印每次GC发生的时间
  • -Xloggc:gc.log:设置将GC日志写入一个磁盘文件

以idea为例:

然后就可以分析堆帧情况了

11.  JVM调优工具

我们常用的调优/检测工具有:

jps: 可以查看jvm各个运行程序的端口,通过端口才可以使用jstat,jmap等命令操作指定的jvm运行程序

jstat:用于监视JVM运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。一般用来查看内存的大致情况。

jmap:用于生成heap dump文件,jmap可以查询当前Java堆内存的详细信息(什么对象占据了大量的内存),比如当前各个区域使用率(总容量、已使用、未使用)、当前使用的是哪种收集器等。

jhat:一般与jmap搭配使用,用来分析jmap生成的dump文件,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。但是一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程。

MAT: MAT分析dump文件所需要的额外内存比jhat要小的多的多,所以建议使用MAT来进行分析。且一般分析时,都是讲dump文件复制到其他电脑上去分析

jconsole: java自带的可视化工具,可实时查看内存,线程等详细情况

VisualVm:  和jconsole相同,都是可视化工具,但是功能比jconsole略多一点,比如说垃圾回收次数等

12. 压力测试

使用jmeter,通过该工具可以模拟并发的http请求,用来测试jvm性能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章