JVM思维导图-待整理版本
2020-07-15 10:38:20 1 举报
AI智能生成
JVM
作者其他创作
大纲/内容
JVM在什么情况下会加载一个类 <br>
加载
你的代码中用到这个类的时候
验证
根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容,是否符合指定的规范。
准备
给类分配一定的内存空间<br>然后给他里面的类变量(也就是static修饰的变量)分配内存空间,<br>并且给一个默认的初始值<br>
解析
把将常量池中的符号引用转为直接引用,<br>可以认为是一些静态绑定的会被解析,<br>动态绑定则只会在运行是进行解析;<br>静态绑定包括一些final方法(不可以重写),<br>static方法(只会属于当前类),<br>构造器(不会被重写)。<br>
初始化
执行类的初始化代码
什么时候会初始化一个类?
比如“new ReplicaManager()”来实例化类的对象了,此时就会触发类的加载到初始化的全过程,<br>把这个类准备好,然后再实例化一个对象出来;<br>
包含“main()”方法的主类,必须是立马初始化的
初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类
使用
卸载
类加载器
Bootstrap ClassLoader<br>[启动类加载器]
他主要是负责加载我们在机器上安装的Java目录下的核心类的
加载Java安装目录下的“lib”目录中的核心类库。
Extension ClassLoader<br>[扩展类加载器]
加载 " lib\ext " 目录
Application ClassLoader<br>[应用程序类加载器]
加载“ClassPath”环境变量所指定的路径中的类
自定义类加载器
根据你自己的需求加载你的类。
双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说<br>1.启动类加载器是最上层的,<br>2.扩展类加载器在第二层,<br>3.第三层是应用程序类加载器,<br>4.最后一层是自定义类加载器。<br>
假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载<br><br>但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
先找父亲去加载,不行的话再由儿子来加载。
什么是JVM的内存区域划分?
JVM在运行我们写好的代码时,他是必须使用多块内存空间的,不同的内存空间用来放不同的数据,<br><br>然后配合我们写的代码流程,才能让我们的系统运行起来
存放类的方法区
方法区是在JDK 1.8以前的版本里,代表JVM中的一块区域。
主要是放从“.class”文件里加载进来的类,还会有一些类似常量池的东西放在这个区域里
但是在JDK 1.8以后,这块区域的名字改了,叫做“Metaspace”,可以认为是“元数据空间”这样的意思。当然这里主要还<br><br>是存放我们自己写的各种类相关的信息
执行代码指令用的程序计数器
用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令
首先Java代码被编译成字节码指令,然后字节码指令一定会被一条一<br><br>条执行,这样才能实现我们写好的代码执行的效果
所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令
虚拟机栈
JVM必须有一块区域是来保存每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈
Java代码在执行的时候,一定是线程来执行某个方法中的代码
线程执行方法的代码指令的时候,就会通过线程对应的程序计数器记录自己执行的指令位置。
每个线程都有自己的Java虚拟机栈,比如这里的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量
线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧
栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等
JVM中的“Java虚拟机栈”这个组件的作用:调用执行任何方法时,都会给方法创建栈帧然后入栈<br>在栈帧里存放了这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关的信息,方法执行完毕之后就出栈。<br>
堆内存
存放我们在代码中创建的各种对象的
[方法]对应的[栈帧]的[局部变量表]里,会有一个[引用类型]的[局部变量]存放对象的[地址]
本地方法栈
native方法去调用本地操作系统里面的一些方法,<br><br>可能调用的都是c语言写的方法,或者一些底层类库
在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种native方<br><br>法的局部变量表之类的信息
堆外内存空间
不属于JVM的,通过NIO中的allocateDirect这种API,可以在Java堆外分配内存空间。<br>然后,通过Java虚拟机里的DirectByteBuffer来引用和操作堆外内存空间<br>
一个方法执行完毕之后会怎么样?
方法执行完毕,此时就会把方法对应的栈帧从线程的Java虚拟机栈里出栈<br>出栈后 栈帧里的 [ 局部变量 ] 消失<br>
面试题
如何对“.class”文件处理保证不被人拿到以后反编译获取公司源代码?
采用一些小工具对字节码加密,或者做混淆等处理
Tomcat这种Web容器中的类加载器应该如何设计实现?
Tomcat是打破了双亲委派机制的
Tomcat自定义了Common、Catalina、Shared等类加载器,其实就是用来加载Tomcat自己的一些核心基础类库的。
然后Tomcat为每个部署在里面的Web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类
至于Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器。
每个WebApp负责加载自己对应的那个Web应用的class文件,<br>也就是我们写好的某个系统打包好的war包中的所有class文件,不会传导给上层类加载器去加载。<br>
我们创建的那些对象,到底在Java堆内存里会占用多少内存空间呢?
一个是对象自己本身的一些信息
比如对象头,如果在64位的linux操作系统上,会占用16字节,<br>然后如果你的实例对象内部有个int类型的实例变量,他会占用4个字节,<br>如果是long类型的实例变量,会占用8个字节。<br>如果是数组、Map之类的,那么就会占用更多的内存了<br><br>优化: 补齐机制、指针压缩机制<br>
一个是对象的实例变量作为数据占用的空间
方法区里的类会被回收<br>[需要满足后面三个条件]
首先该类的所有实例对象都已经从Java堆内存里被回收
其次加载这个类的ClassLoader已经被回收
最后,对该类的Class对象没有任何引用
说每个线程执行方法的时候,那些方法对应的栈帧出栈了,那么那里的局部变量需要垃圾<br>回收吗?
JVM里垃圾回收针对的是新生代,老年代,还有方法区(永久代),不会针对方法的栈帧。
方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉了。
子主题
子主题
子主题
子主题
子主题
子主题
子主题
子主题
子主题
疑惑点
Java支持多线程,每个线程有自己的Java虚拟机栈和本地方法栈
新建的实例在堆内存,实例变量也是在堆内存
除非用到子类才会加载子类;但是加载子类要初始化之前,必须先加载父类,初始化父类
必须先加载类,再实例化对象
Object Header(4字节) + Class Pointer(4字节)+ Fields(看存放类型),但是jvm内存占用是8的倍数,所以结果要向上取整到8的倍数
静态成员变量,他在内存里,只有一份,就是属于类的。多个线程并发修改,一定会有并发问题,可能导致数据出错。
默认的类加载机制,那么是你的代码运行过程中,遇到什么类加载什么类。如果你要自己加载类,那么需要写自己的类加载器
类是在准备阶段分配内存空间的
实例变量得在你创建类的实例对象时才会初始化
类的初始化阶段,仅仅是初始化类而已,跟对象无关,用new关键字才会构造一个对象出来
为什么类的初始化需要执行静态代码块,给静态成员变量赋值,是因为这些数据是在方法区吗?
类在方法区,他在内存里,所以你必须给他初始化,赋值
类加载双亲委派机制 为什么要先找父加载 而不是自己找?这种设计的好处是?
好处就在于,每个层级的类加载器各司其职,而且不会重复加载一个类。
还有一些安全性的问题,比如只有 启动类加载器才能加载String类等受保护的资源.
-XX:+TraceClassLoading 可以看加载了哪些类,动手实验了一下,jre\lib\rt.jar下的类全部加载了,其他都是用到时候加载
静态变量,是在类初始化的时候来执行,但是不是JVM执行代码的流程来执行,他是类初始化,自成一套体系
Java对象其实都是占用内存资源的
我们在Java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限
启动的Java系统本质就是一个JVM进程,他负责运行我们的系统的代码
我们在JVM的Java堆内存中创建的对象,其实本质也是会占用JVM的内存资源的
这个JVM进程本身也是会占用机器上的部分内存资源
JVM的垃圾回收机制
JVM本身是有垃圾回收机制的,他是一个后台自动运行的线程
你只要启动一个JVM进程,他就会自带这么一个垃圾回收的后台线程。<br>这个线程会在后台不断检查JVM堆内存中的各个实例对象<br>如果某个实例对象没有任何一个方法的局部变量指向他,也没有任何一个类的静态变量,包括常量等地方在指向他。<br>那么这个垃圾回收线程,就会把这个没人指向的实例对象给回收掉,从内存里清除掉,让他不再占用任何内存资源。<br>
什么情况下JVM内存中的一个对象会被垃圾回收??
什么时候会触发垃圾回收?
一旦新生代快满了,那么垃圾回收的时候
哪些变量引用的对象是不能回收的
[ 可达性分析 ] 算法来判定哪些对象是可以被回收的,哪些对象是不可以被回收的。
局部变量就是可以作为GC Roots的
只要一个对象被局部变量引用了,那么就说明他有一个GC Roots,此时就不能被回收了。
静态变量也可以看做是一种GC Roots
方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots。
只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收他们。
Java中对象不同的引用类型
强引用
只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象的
弱引用
正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,<br>内存都快溢出了此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为他是软引用,<br>所以还是要回收。<br>
虚引用
如果发生垃圾回收,就会把这个对象回收掉。
总结:
有GC Roots引用的对象不能回收,没有GC Roots引用的对象可以回收,如果有GC Roots引用,但是如果是软引用或者弱引用的,也有可能被回收掉。
finalize()方法的作用
假设有一个对象要被垃圾回收了,那么假如这个对象重写了Object类中的finialize()方法<br>此时会先尝试调用一下他的finalize()方法,看是否把自己这个实例对象给了某个GC Roots变量,<br>比如说代码中就给了某个类的静态变量。<br>如果重新让某个GC Roots变量引用了自己,那么就不用被垃圾回收了。<br>
JVM内存的一个分代模型
大部分对象都是存活周期极短的
少数对象是长期存活的
JVM分代模型
年轻代
短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收
老年代
长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用
内存分配
先理解对象优先分配在新生代
新生代如果对象满了,会触发Minor GC回收掉没有人引用的垃圾对象
如果有对象躲过了十多次垃圾回收,就会放入老年代里
如果老年代也满了,那么也会触发垃圾回收,把老年代里没人引用的垃圾对象清理掉
跟JVM内存相关的几个核心参数图解
-Xms:Java堆内存的大小<br>-Xmx:Java堆内存的最大大小<br>
限定Java堆内存的总大小的
-Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize:永久代大小<br>-XX:MaxPermSize:永久代最大大小<br>
jdk8 过时 改为元空间
-Xss:每个线程的栈内存大小
案例
百万交易的支付系统
一个每日百万交易的支付系统的压力到底集中在哪里呢?
每天JVM内存里会频繁的创建和销毁100万个支付订单
我们的支付系统需要部署多少台机器?
每台机器需要多大的内存空间?
每台机器上启动的JVM需要分配多大的堆内存空间?
给JVM多大的内存空间才能保证可以支撑这么多的支付订单在内存里的创建,而不会导致内存不够直接崩溃?
支付系统每秒钟需要处理多少笔支付订单 ?
假设每天100万个支付订单,那么一般用户交易行为都会发生在每天的高峰期,比如中午或者晚上。
假设每天高峰期大概是几个小时,用100万平均分配到几个小时里,那么大概是每秒100笔订单左右,咱们就以每秒100笔订单来计算一下好了
假设我们的支付系统部署了3台机器,每台机器实际上每秒大概处理30笔订单。
每个支付订单处理要耗时多久?
如果用户发起一次支付请求,那么支付需要在JVM中创建一个支付订单对象,填充进去数据,然后把这个支付订单写入数据<br><br>库,还可能会处理一些其他的事情咱们就假设一次支付请求的处理,包含一个支付订单的创建,大概需要1秒钟的时间
那么大体上你的脑子里可以出现的一个流动的模型,应该是每台机器一秒钟接收到30笔支付订单的请求,然后在JVM的新生代<br><br>里创建了30个支付订单的对象,做了写入数据库等处理
接着1秒之后,这30个支付订单就处理完毕,然后对这些支付订单对象的引用就回收了,这些订单在JVM的新生代里就是没人<br><br>引用的垃圾对象了。
接着再是下一秒来30个支付订单,重复这个步骤。
每个支付订单大概需要多大的内存空间?
直接根据支付订单类中的实例变量的类型来计算就可以了。
比如说支付订单类如下所示,<br>你只要记住一个Integer类型的变量数据是4个字节,<br>Long类型的变量数据是8个字节,<br>还有别的类型的变量数据占据多少字节<br>
比如支付订单这种核心类,你就按20个实例变量来计算,然后一般大概一个对象也就在几百字节的样子
我们算他大一点好了,就算一个支付订单对象占据500字节的内存空间,不到1kb。
每秒发起的支付请求对内存的占用
假设有3台机器,每秒钟处理30笔支付订单的请求,那么在这1秒内,大家都知道,肯定是有方法里的局部变量在<br>引用这些支付订单的
那么30个支付订单,大概占据的内存空间是30 * 500字节 = 15000字节,大概其实也就15kb而已。其实是非常非常小的。
让支付系统运行起来分析一下
每秒30个支付请求,创建30个支付订单对象,也就占据kb级别的内存空间而已
然后接着1秒过后,这30个对象就没有人引用了,就成了新生代里的垃圾了。
下一秒请求过来,我们的系统持续的创建支付订单对象,不停在新生代里放入30个支付订单,然后新生代里的对象会持续的累积和增加。
直到有一刻,发现可能新生代里都有几十万个对象了,此时占据了几百MB的空间了,可能新生代空间就快满了
然后就会触发Minor GC,就把新生代里的垃圾对象都给回收掉了,腾出内存空间,然后继续来在内存里分配新的对象。
对完整的支付系统内存占用需要进行预估
真实的支付系统线上运行,肯定每秒会创建大量其他的对象,但是我们结合这个访问压力以及核心对象的内存占据,<br>大致可以来估算一下整个支付系统每秒钟大致会占据多少内存空间<br>
其实如果你要估算的话,可以把之前的计算结果扩大10倍~20倍。<br>也就是说,每秒钟除了在内存里创建支付订单对象,还会创建其他数十种对象<br>
那么每秒钟创建出来的被栈内存的局部变量引用的对象大致占据的内存空间就在几百KB~1MB之间。
然后下一秒继续来新的请求创建大概1MB的对象放在新生代里,接着变成垃圾,再来下一秒
循环多次之后,新生代里垃圾太多,就会触发Minor GC回收掉这些垃圾。这就是一个完整系统的大致JVM层面的内存使用模型。
支付系统的JVM堆内存应该怎么设置
常见的机器配置是2核4G,或者是4核8G。
机器采用4核8G,然后-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,-Xmn设置为2G,给新生代2G内存空间。
假设你的业务量如果更大,你可以考虑不只部署3台机器,可以横向扩展部署5台机器,或者10台机器,这样每台机器处理的请求更少,对JVM的压力更小。
不合理设置内存的反面示例
内存不足,满足不了瞬时创建对象的需求, 导致 OOM
大促期间,瞬时访问量增加十倍
少数请求需要几十秒处理,导致老年代内存占用变大
老年代对象越来越多导致频繁垃圾回收
FULL GC
如何合理设置栈内存大小
一般也不会特别的去预估和设置的,一般默认就是比如512KB到1MB
每日上亿请求量的电商系统
年轻代垃圾回收参数优化
案例背景引入
如何在特定场景下,预估系统的内存使用模型然后合理优化新生代、老年代、Eden和Survivor各个区域的内存大小,接着再尽量优化参数避免新生代的对象进入老<br><br>年代,尽量让对象留在新生代里被回收掉。
案例背景是每日上亿请求量的电商系统,<br>推算一下每日上亿请求量的电商系统,<br>他会每日有多少活跃用户<br>
一般按每个用户平均访问20次来计算,那么上亿请求量,大致需要有500万日活用户。
这里可以按照10%的付费转化率来计算,每天大概有50万人会下订单,那么大致就是每天会有50万订单。
这50万订单算他集中在每天4小时的高峰期内,那么其实平均下来每秒钟大概也就几十个订单
因为几十个订单的压力下,根本就不需要对JVM多关注,基本上就是每秒钟占用一些新生代内存,隔很久新生代才会满,<br>然后一次Minor GC后垃圾对象清理掉,内存就空出来了,几乎无压力。<br>
特殊的电商大促场景
可能在大促开始的短短10分钟内,瞬间就会有50万订单。<br>每秒就会有接近1000的下单请求,我们就针对这种大促场景来对订单系统的内存使用模型分析一下<br>
抗住大促的瞬时压力需要几台机器?
基本上可以按3台来算,就是每台机器每秒需要抗300个下单请求。这个也是非常合理的,而且需要假设订单系统部署的就是最普通的标配4核8G机器
从机器本身的CPU资源和内存资源角度,抗住每秒300个下单请求是没问题的
但是问题就在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次<br>数尽可能最少,而且尽量避免Full GC,这样可以尽可能减少JVM的GC对高峰期的系统新更难的影响。
大促高峰期订单系统的内存使用模型估算
按照每秒钟处理300个下单请求来估算,其实无论是订单处理性能还是并发情况,都跟生产很接近
因为处理下单请求是比较耗时的,涉及很多接口的调用,基本上每秒处理100~300个下单请求是差不多的。
那么每个订单咱们就按1kb的大小来估算,单单是300个订单就会有300kb的内存开销
然后算上订单对象连带的订单条目对象、库存、促销、优惠券等等一系列的其他业务对象,一般需要对单个对象开销放大10倍~20倍。
此外,除了下单之外,这个订单系统还会有很多订单相关的其他操作,比如订单查询之类的,所以连带算起来,可以往大了估算,再扩大10倍的量。
那么每秒钟会有大概300kb * 20 * 10 = 60mb的内存开销。但是一秒过后,可以认为这60mb的对象就是垃圾了,因<br>为300个订单处理完了,所有相关对象都失去了引用,可以回收的状态
内存到底该如何分配?
假设我们有4核8G的机器,那么给JVM的内存一般会到4G,剩下几个G会留点空余给操作系统之类的来使用,<br>不要想着把机器内存一下子都耗尽,其中堆内存我们可以给3G,新生代我们可以给到1.5G,老年代也是1.5G<br>
然后每个线程的Java虚拟机栈有1M,那么JVM里如果有几百个线程大概会有几百M
然后再给永久代256M内存,基本上这4G内存就差不多了。
设置: -XX:HandlePromotionFailure
HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
该参数已经废弃掉了
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M
MaxPermSize JDK1.8 已经废弃了
MaxMetaspaceSize JDK1.8 设置
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
订单系统的系统程序在大促期间不停的运行,每秒处理300个订单,都会占据新生代60MB的内存空间
但是1秒过后这60MB对象都会变成垃圾,那么新生代1.5G的内存空间大概需要25秒就会占满
25秒过后就会要进行Minor GC了,此时因为有“-XX:HandlePromotionFailure”选项,所以你可以认为需要进行的<br><br>检查,主要就是比较 “老年代可用空间大小”和“历次Minor GC后进入老年代对象的平均大小”,刚开始肯定这个<br><br>检查是可以通过的 [ HandlePromotionFailure 已废弃 ]
所以Minor GC直接运行,一下子可以回收掉99%的新生代对象,因为除了最近一秒的订单请求还在处理,大部分订单<br><br>早就处理完了,所以此时可能存活对象就100MB左右
但是这里问题来了,如果“-XX:SurvivorRatio”参数默认值为8,那么此时新生代里Eden区大概占据了1.2GB内存,<br><br>每个Survivor区是150MB的内存.
所以Eden区1.2GB满了就要进行Minor GC了,因此大概只需要20秒,就会把Eden区塞满,就要进行Minor GC了。
然后GC后存活对象在100MB左右,会放入S1区域内。
然后再次运行20秒,把Eden区占满,再次垃圾回收Eden和S1中的对象,存活对象可能还是在100MB左右会进入S2区.
新生代垃圾回收优化之一:Survivor空间够不够
第一个要考虑的问题,就是你通过估算,你的新生代的Survivor区到底够不够
首先每次新生代垃圾回收在100MB左右,有可能会突破150MB,那么岂不是经常会出现Minor GC过<br><br>后的对象无法放入Survivor中?然后岂不是频繁会让对象进入老年代?
还有,即使Minor GC后的对象少于150MB,但是即使是100MB的对象进入Survivor区,因为这是一批同龄对象,直<br><br>接超过了Survivor区空间的50%,此时也可能会导致对象进入老年代。
建议的是调整新生代和老年代的大小
考虑把新生代调整为2G,老年代为1G,那么此时Eden为1.6G,每个Survivor为200MB
这个时候,Survivor区域变大,就大大降低了新生代GC过后存活对象在Survivor里放不下的问题,或者是同龄对象超<br>过Survivor 50%的问题
其实对任何系统,首先类似上文的内存使用模型预估以及合理的分配内存,尽量让每次Minor GC后的对象都留在<br><br>Survivor里,不要进入老年代,这是你首先要进行优化的一个地方
新生代对象躲过多少次垃圾回收后进入老年代?
除了Minor GC后对象无法放入Survivor会导致一批对象进入老年代之外,还有就是有些对象连续躲过15<br><br>次垃圾回收后会自动升入老年代。
基本上20多秒触发一次Minor GC,那么如果按照“-XX:MaxTenuringThreshold”参数的默认值15次来说,<br>你要是连续躲过15次GC,就是一个对象在新生代停留超过了几分钟了,此时他进入老年代也是应该的<br>
如果躲过15次GC都几分钟了,一个对象几分钟都不能被回收,<br><br>说明肯定是系统里类似用@Service、@Controller之类的注解标注的那种需要长期存活的核心业务逻辑组件。<br>那么他就应该进入老年代,何况这种对象一般很少,一个系统累计起来最多也就几十MB而已。<br>
所以你说你提高“-XX:MaxTenuringThreshold”参数的值,有啥用呢?让这些对象在新生代里多停留几分钟?
要考虑,考虑实际情况<br>如果是需要长期进入老年代的核心逻辑组件, 这个应该不用管.
其实这个参数甚至你都可以降低他的值,比如降低到5次,也就是说一个对象如果躲过5次Minor GC,在新生代里停留<br>超过1分钟了,尽快就让他进入老年代,别在新生代里占着内存了。
对于这个参数务必是结合你的系统具体运行的模型来考虑!!!!
-XX:MaxTenuringThreshold=5
然后根据你的系统运行模型,合理设置“-XX:MaxTenuringThreshold”,让那些长期存活的对象,抓紧尽快进入老年代,别在新生代里一直待着
多大的对象直接进入老年代?
大对象可以直接进入老年代 ,因为大对象说明是要长期存活和使用的
但是一般来说,给他设置个1MB足以,因为一般很少有超过1MB的大对象。如果有,可能是你提前分配了一个大数组、大List之类的东西用来放缓存的数据。
-XX:PretenureSizeThreshold=1M
别忘了指定垃圾回收器
新生代使用ParNew,老年代使用CMS
-XX:+UseParNewGC <br>-XX:+UseConcMarkSweepGC<br>
ParNew垃圾回收器的核心参数,其实就是配套的新生代内存大小、Eden和Survivor的比例,只要你设置合理,避免Minor GC后对象放不下Survivor进入老年代,或者是动态年龄判定之后进入老年代,给新生代里的Survivor充足的空间,那么Minor GC一般就没什么问题
老年代垃圾回收参数优化
什么时候对象会进入老年代?
第一种情况,那绝对就是“-XX:MaxTenuringThreshold=5”这个参数会让在一两分钟内连续躲过5次Minor GC的对象迅速进入老年代中
这种对象一般就是一些@Service、@Controller之类的注解标注的那种系统业务逻辑组件,这种对象实例一般全局就有一个实例就可以了,要一直使用的
一般会长期被GC Roots引用,这种对象一般不会太多,大概最多一个系统就几十MB这种对象。
第二种情况, 如果分配一个超过1MB的大对象,比如说你创建一个大数组或者是大List之类的,就会直接进入老年代。
第三种情况, Minor GC过后可能存活的对象超过200MB放不下Survivor了,或者是一下子占到超过Surviovr的50%,此时会有一些对象进入老年代中。
大促期间多久会触发一次Full GC?
假设 每隔5分钟会在Minor GC之后有一小批对象进入老年代,大概200MB左右的大小
触发FULL GC 条件
每次Minor GC之前,都检查一下“老年代可用内存空间” < “历次Minor GC后升入老年代的平均对象大小”
其实按照我们目前设定的背景,要很多次Minor GC之后才可能有一两次碰巧会有200MB对象升入老年代,所以这个<br><br>“历次Minor GC后升入老年代的平均对象大小”,基本是很小的。
可能某次Minor GC后要升入老年代的对象有几百MB,但是老年代可用空间不足了
设置了“-XX:CMSInitiatingOccupancyFraction”参数,比如设定值为92%,那么此时可能前面几个条件都没满<br>足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Full GC
其实在真正的系统运行期间,可能会慢慢的有对象进入老年代,但是因为新生代我们优化过了内存分配,所以对象进<br><br>入老年代的速度是很慢的。
所以很可能是在系统运行半小时~1小时之后,才会有接近 1GB的对象进入老年代。
这个推论很重要,因为按照大促开始10分钟就有50万订单来计算,其实大促开始后一堆用户等着下单剁手购物
那么1小时候就可能有两三百万订单了,这是一年难得罕见的节日大促才会有的,然后这个高峰期过后,基本订单系统<br>访问压力就很小了,那么GC的问题几乎就更不算什么了。
所以经过新生代的优化,可以推算出,基本上大促高峰期内,也就可能1小时才1次Full GC,然后高峰期一过,随着订<br>单系统慢慢运行,可能就要几个小时才有一次Full GC。
老年代GC的时候会发生“Concurrent Mode Failure”吗?
假设就是订单系统运行1小时之后,老年代大概有900MB的对象了,剩余可用空<br>间仅仅只有100MB了,此时就会触发一次Full GC
但是有一个很大的问题,就是CMS在垃圾回收的时候,尤其是并发清理期间,系统程序是可以并发运行的,所以此时<br><br>老年代空闲空间仅剩100MB了
然后此时系统程序还在不停的创建对象,万一这个时候系统运行触发了某个条件,比如说有200MB对象要进入老年<br><br>代,此时会如何?
这个时候就会触发“Concurrent Mode Failure”问题,因为此时老年代没有足够内存来放这200MB对象,此时就<br><br>会导致立马进入Stop the World,然后切换CMS为Serial Old,直接禁止程序运行,然后单线程进行老年代垃圾回<br><br>收,回收掉900MB对象过后,再让系统继续运行,
概率是挺小的,因为必须是CMS触发Full GC的时候,系统运行期间还让200MB对象进入老年代,这个概率其实本身<br><br>就很小,但是理论上是有可能的
最终参数
-Xms3072M <br>-Xmx3072M <br>-Xmn2048M <br>-Xss1M <br>-XX:PermSize=256M ===> [ 替换 MetaspaceSize 元空间 ] <br>-XX:MaxPermSize=256M ===> [ 替换 MaxMetaspaceSize 元空间 ] <br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5 <br>-XX:PretenureSizeThreshold=1M <br>-XX:+UseParNewGC <br>-XX:+UseConcMarkSweepGC <br>-XX:CMSInitiatingOccupancyFaction=92<br>-XX:+UseCMSCompactAtFullCollection<br>-XX:CMSFullGCsBeforeCompaction=0<br><br><br>
CMS垃圾回收之后进行内存碎片整理的频率应该多高?
默认
其实没必要,因为通过前面的分析,在大促高峰期,<br>Full GC可能也就1小时执行一次,然后大促高峰期过去之后,<br>就没那么多的订单了,此时可能几个小时才会有一次Full GC<br>
Full GC优化的前提是Minor GC的优化,<br>Minor GC的优化的前提是合理分配内存空间,<br>合理分配内存空间的前提是对系统运行期间的内存使用模型进行预估<br>
百万级用户在线的教育平台
系统的运行压力
核心点就是搞明白在晚上两三小时高峰期内,每秒钟会有多少请求,每个请求会连带产生多少对象,占用多少内存,每个请求要处理多长时间。
比如说晚上3小时高峰期内有总共60万活跃用户,平均每个用户大概会使用1小时左右来上课,那么每小时大概会有20万活跃用户同时在线学习。
这20万活跃用户因为需要进行大量的互动操作,所以大致可以认为是每分钟进行1次互动操作,一小时内会进行60次互动操作
那么20万用户在1小时内会进行1200万次互动操作,平均到每秒钟大概是3000次左右的互动操作,这是一个很合理的数字。
那么每秒钟要承载3000并发请求,<br>根据经验来看,一般系统的核心服务需要部署5台4核8G的机器来抗住是差不多的,<br>每台机器每秒钟抗个600请求,这个压力可以接受,一般不会导致宕机的问题。<br>
那么每个请求会产生多少个对象呢?
一次互动请求大致会连带创建几个对象,占据几KB的内存,比如我们就认为是5KB吧那么一秒600请求会占用3MB左右的内存。
G1垃圾回收器的默认内存布局
接着我们来看看G1垃圾回收器的默认内存布局,之前说过我们采用的是4核8G的机器来部署系统,然后每台机器每秒会有600个请求会占用3MB左右的内存空间
那么假设我们对机器上的JVM,分配4G给堆内存,其中新生代默认初始占比为5%,最大占比为60%,每个Java线程的栈内存为1MB,<br>元数据区域(永久代)的内存为256M,此时JVM参数如下:<br>“-Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC“<br>
-XX:G1NewSizePercent”参数是用来设置新生代初始占比的,不用设置,维持默认值为5%即可。
“-XX:G1MaxNewSizePercent”参数是用来设置新生代最大占比的,也不用设置,维持默认值为60%即可
此时堆内存共4G,那么此时会除以2048,计算出每个Region的大小,此时每个Region的大小就是2MB,<br>刚开始新生代就占5%的Region,可以认为新生代就是只有100个Region,有200MB的内存空间,<br>
GC停顿时间如何设置?
“-XX:MaxGCPauseMills”,他的默认值是200毫秒
到底多长时间会触发新生代GC?
有一个问题,就是系统运行起来之后,会不停的在新生代的Eden区域内分配对象,按照之前的推算是每秒分配3MB的对象
“-XX:G1MaxNewSizePercent”参数限定了新生代最多就是占用堆内存60%的空间
他会根据你预设的gc停顿时间,给新生代分配一些Region,然后到一定程度就触发gc,<br>并且把gc时间控制在预设范围内,尽量避免一次性回收过多的Region导致gc停顿时间超出预期。<br>
新生代gc如何优化?
对于G1而言,我们首先应该给整个JVM的堆区域足够的内存,比如我们在这里就给了JVM超过5G的内存,其中堆内存有4G的内存。
应该合理设置“-XX:MaxGCPauseMills”参数
如果这个参数设置的小了,<br>那么说明每次gc停顿时间可能特别短,<br>此时G1一旦发现你对几十个Region占满了就立即触发新生代gc,然后gc频率特别频繁,虽然每次gc时间很短。<br>比如说30秒触发一次新生代gc,每次就停顿30毫秒<br>
如果这个参数设置大了呢?<br>那么可能G1会允许你不停的在新生代理分配新的对象,然后积累了很多对象了,再一次性回收几百个Region<br>此时可能一次GC停顿时间就会达到几百毫秒,但是GC的频率很低。比如说30分钟才触发一次新生代GC,但是每次停顿500毫秒。<br>
尽量让系统的gc频率别太高,同时每次gc停顿时间也别太长,达到一个理想的合理值。
mixed gc如何优化?
老年代在堆内存里占比超过45%就会触发。
年轻代的对象进入老年代的几个条件
1.新生代gc过后存活对象太多没法放入Survivor区域
2.对象年龄太大
3.动态年龄判定规则
一旦老年代频繁达到占用堆内存45%的阈值,那么就会频繁触发mixed gc。
尽量避免对象过快进入老年代,尽量避免频繁触发mixed gc,就可以做到根本上优化mixed gc了
核心: “-XX:MaxGCPauseMills”
假设你“-XX:MaxGCPauseMills”参数设置的值很大,导致系统运行很久,新生代可能都占用了堆内存的60%了,此时才触发新生代gc<br>那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。<br>
或者是你新生代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,<br>达到了Survivor区域的50%,也会快速导致一些对象进入老年代中<br>
所以这里核心还是在于调节“-XX:MaxGCPauseMills”这个参数的值,在保证他的新生代gc别太频繁的同时,<br>还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc<br>
每日百亿数据量的实时分析引擎,为啥频繁发生Full GC ?
子主题
GC回收算法
复制算法
把新生代内存划分为两块内存区域,然后只使用其中一块内存待那块内存快满的时候,<br>就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片接着一次性回收原来那块内存区域的垃圾对象,<br>再次空出来一块内存区域。<br>
两块内存区域就可以重复着循环使用
优点
Minor GC : 无STW
无内存碎片
缺点
对内存的使用效率太低了
复制算法的优化:Eden区和Survivor区
1个Eden区,2个Survivor区,其中Eden区占80%内存空间,<br>每一块Survivor区各占10%内存空间,<br><br>比如说Eden区有800MB内存,每一块Survivor区就100MB内存<br>
最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了
如何进入老年代
躲过15次GC之后进入老年代
-XX:MaxTenuringThreshold”来设置,默认是15岁
动态对象年龄判断
假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大<br><br>于等于这批对象年龄的对象,就可以直接进入老年代了
年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。
大对象直接进入老年代
“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。
Minor GC后的对象太多无法放入Survivor区
这个时候就必须得把这些对象直接转移到老年代去
老年代空间分配担保规则
首先,在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。
如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,<br>因为即使Minor GC之后所有对象都存活,Survivor区放不下了,也可以转移到老年代去<br>
但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了<br>那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?<br>
发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:-HandlePromotionFailure”的参数是否设置了
该参数已经废弃掉了 HandlePromotionFailure
如果有这个参数,那么就会继续尝试进行下一步判断。
下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小
此时就会直接触发一次“FullGC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。
进行Minor GC有几种可能
第一种可能,Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可
第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小<br>的,此时就直接进入老年代即可。
第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内<br><br>存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面
如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的<br>“OOM”内存溢出了
老年代垃圾回收算法
老年代触发垃圾回收的时机 ??
要不然是在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC
要不然是在Minor GC之后,发现剩余对象太多放入老年代都放不下了
回收算法
标记整理算法 ???
老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。
JVM中都有哪些常见的垃圾回收器,各自的特点是什么
这个系统到底多块会塞满新生代?
每条数据包含了平均20个字段,可以认为平均每条数据在1KB左右的大小。那么每次计算任务的1万条数据就对应了10MB的大小。
1.G堆内存 新生代是按照8:1:1的比例来分配Eden和两块Survivor的区域,那么大体上来说,Eden区就是<br>1.2GB,每块Survivor区域在100MB左右
每次执行一个计算任务,就会在Eden区里分配10MB左右的对象,那么一分钟大概对应100次计算任务
触发Minor GC的时候会有多少对象进入老年代??
正在运行的任务对象会进入老年代.
系统运行多久,老年代大概就会填满?
这个系统运行多久,老年代会触发1次Full GC?
优化: 调整 Survivor 区域的内存大小, 到无阻塞处理 , 尽量保持任务不进入老年代.
回收器
Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象
工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然<br><br>后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。
ParNew和CMS垃圾回收器:
ParNew现在一般都是用在新生代的垃圾回收器,<br>CMS是用在老年代的垃圾回收器,他们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。<br>
G1垃圾回收器
统一收集新生代 和老年代,采用了更加优秀的算法和设计机制
Stop the World
会直接停止我们写的Java系统的所有工作线程,让我们写的代码不再运行!!!!!!!!!!!!
新生代的回收
Serial垃圾回收器就是用一个线程进行垃圾回收,然后此时暂停系统工作线程,所以一般我们在服务器程序中很<br><br>少用这种方式。
ParNew 他针对服务器一般都是多核CPU做了优化,他是支持多线程个垃圾回收的,可以大<br><br>幅度提升回收的性能,缩短回收的时间
老年代回收
CMS垃圾回收器,专门负责老年代的垃圾回收
parnew+cms的gc,如何保证只做ygc,jvm参数如何配置?
首先上线系统之后,要借助一些工具(后面会讲)观察每秒钟会新增多少对象在新生代里,然后多长时间触发一次Minor GC,平均每<br><br>次MInor GC之后会有多少对象存活,Survivor区是否可以放的下。
这里的关键点就是必须让Survivor区放下,而且不能因为动态年龄判定规则直接升入老年代。然后只要Survivor区可以放下,那么下次<br><br>Minor GC后还是存活这么多对象,依然可以在另外一块Survivor区放下,基本就不会有对象升入老年代里去
加大分代年龄,比如默认15加到30; 增加程序的处理时长
演算过程和公式
每台机器可以提供给JVM的最大内存: each_m,比如2核4G机器,可提供JVM最大内存2G 栈占用:<br>stack_m = QPS 估值 * 1M * 20倍数,估值30QPS,栈约为600M<br>
新生代以30分钟一次GC计算总内存:30(Monitor GC间隔) * 60 * QPS估值 * 接口内存估值,young_m 所需机器数量,假设等于N
方法区:200M,一般够用,method_m 老年代:500M,一般不大,300M也行,像我们结算服务,100M都够用
old_m 演算公式: JVM最大内存*N = stack_m + young_m + old_m + method_m * N 机器数N,也同时估算出来,是这样吗
每个计算计算任务1万条数据需要计算10秒钟,假设此时计算出80个计算任务都执行成功了
假设1分钟100个计算任务,每个计算任务是10秒钟,在60秒到来的那一刻,只有50-60秒开始执行的<br><br>计算任务是没有执行完毕的,占比1/6,100 / 6 = 16.66 。 所以可以大致估算还有20个计算任务还在计算中。
GC回收器
年轻代
ParNew
ParNew垃圾回收器如果一旦在合适的时机执行Minor GC的时候,就会把系统程序的工作线程全部停掉,<br>禁止程序继续运行创建新的对象,然后自己就用多个垃圾回收线程去进行垃圾回收,回收的机制和算法就跟之前说的是一样的<br>
-XX:+UseParNewGC
ParNew垃圾回收器默认情况下的线程数量
默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的。
-XX:ParallelGCThreads
老年代
CMS
"标记清理" 算法
会造成很多内存碎片
垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。
CMS在执行一次垃圾回收的过程一共分为4个阶段:
初始标记 [STW]
标记出来所有GC Roots直接引用的对象
方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots。
并发标记
系统线程可以随意创建各种新对象,继续运行
在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。<br>在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪<br>
但是这个过程中,在进行并发标记的时候,系统程序会不停的工作,他可能会各种创建出来新的对象,部分对象可能成为垃圾
重新标记 [STW]
对老年代所有对象进行GC Roots追踪,其实是最耗时的
他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的.
并发清理
需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行
对CMS的垃圾回收机制进行性能分析
第一个问题就是会消耗CPU资源。
因为最耗时的,其实就是对老年代全部对相关进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉,这是最耗时的。
在并发标记和并发清理两个最耗时的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分
CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。
用最普通的2核4G机器和4核8G机器来计算一下,<br>假设是2核CPU,本来CPU资源就有限,<br>结果此时CMS还会有个“(2 + 3) / 4”= 1个垃圾回收线程,去占用宝贵的一个CPU。<br>
第二个问题"浮动垃圾"
在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象
但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”
但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。
所以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。
CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。
“-XX:CMSInitiatingOccupancyFraction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。
那么如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时会如何?
Concurrent Mode Failure
此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追<br>踪,标记出来全部垃圾对象,不允许新的对象产生
然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
第三个问题 "内存碎片"
就是老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样<br>会导致大量的内存碎片产生。
-XX:+UseCMSCompactAtFullCollection 默认打开
他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片
-XX:CMSFullGCsBeforeCompaction
这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。
几个触发老年代GC的时机
第一是 新生代空间不足, Minor GC 前 . 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开
第二是 新生代空间不足, Minor GC 前 . 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
第三是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。
第四 大对象直接进入老年代
第五 CMSInitiatingOccupancyFraction
在使用CMS收集器的情况下,老年代使用了指定阈值的内存时,出发FullGC.。
如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的<br><br>内存空间超过了这个参数指定的比例,也会自动触发Full GC。
频繁FULL GC 现象
机器CPU负载过高
频繁FULL GC报警
系统无法处理请求或者处理过慢
频繁FULL GC的常见原因 [五种]
系统承载高并发请求或者处理数据量过大,导致Young GC频繁, <br>而且每次Young GC过后存活对象过多,内存分配不合理.Survivor区域过小,导致对象频繁进入老年代,频繁触发FULL GC<br>
合理分配内存,调整Survivor区域
系统一次性加载过多的数据进内存,产生很多大对象,导致频繁有大对象进入老年代,导致FULL GC
dump内存快照,用MAT等工具分析
发生内存泄漏,莫名创建大量对象,始终无法回收,一直占用老年代内存, 导致FULL GC
dump内存快照,用MAT等工具分析
jstat 发现内存使用并不多,频繁触发GC
Metaspace加载的类过多导致FULL GC
调用System.gc()方法触发Full GC , 最好在启动的时候设置参数禁用 -XX:DisableExplicitGC
为啥老年代的Full GC要比新生代的Minor GC慢很多倍,一般在10倍以上?
Minor GC
新生代执行速度其实很快,因为直接从GC Roots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的,<br><br>不需要追踪多少对象。
然后直接把存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。
但是CMS的Full GC呢?
在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢;
其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;
最后完事儿了,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得“Stop theWorld”,那就更慢了。
万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得<br><br>立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。
Metaspace
Metaspace一般是放一些加载到JVM中的类
如果你在代码汇总大量用了类似反射的代码,那么JVM就会动态的生成一些类放入到Metaspace区域
系统在运行的过程中,不断的有新的类产生被加载到Metaspace区域. 然后不停的把Metaspace区域占满<br>接着会触发Full GC 回收掉Metaspace区域中的部分类
不要乱设置: SoftRefLRUPolicyMSPerMB
因为反射的对象会创建软引用
如果这个参数设置为0的话,会导致metaspace可能会被迅速填满,造成Full GC
总结GC
Young GC的触发时机
在新生代的Eden区域满了之后就会触发,采用复制算法来回收新生代的垃圾
Old GC和Full GC的触发时机
(1)发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象<br><br>总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间<br><br>此时必须先触发一次Old GC给老年代腾出更多的空间,然后再执行Young GC
(2)执行Young GC之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必<br><br>须立即触发一次Old GC
(3)老年代内存使用率超过了92%,也要直接触发Old GC,当然这个比例是可以通过参数调整的
三个条件你概括成一句话,就是老年代空间也不够了,没法放入更多对象了,这个时候务必执行Old<br>GC对老年代进行垃圾回收
(4) Metaspace 空间占满
Old GC执行的时候一般都会带上一次Young GC
一般Old GC很可能就是在Young GC之前触发或者在Young GC之后触发的,所以自然Old GC一般都会跟一次Young GC连带关联在一起了。
在上述几种条件达到的时候,他触发的实际上就是Full GC,这个Full GC会包含Young GC、Old GC和永久代的GC<br>说触发Full GC的时候,可能就会去回收年轻代、老年代和永久代三个区域的垃圾对象<br>
在上述几种条件达到的时候,他触发的实际上就是Full GC,这个Full GC会包含Young GC、Old GC和永久代的GC<br>说触发Full GC的时候,可能就会去回收年轻代、老年代和永久代三个区域的垃圾对象<br>
FULL GC 性能优化
CMSParallelRemarkEnabled
CMS垃圾回收器的"初始标记"阶段开启多线程并发执行
"初始标记"阶段,会进行STW,导致系统停顿,所以这个阶段开启多线程并发之后,可以提升这个阶段性能,减少STW时间
CMSScavengeBeforeRemark
开启在CMS重新标记阶段之前的清除尝试
CMS重新标记阶段也会STW , 在重新标记之前,先执行一个YGC,就会回收掉一些年轻代里没有引用的对象.<br>所以如果先提前回收掉一些对象,那么CMS的重新标记阶段就可以少扫描一些对象,这样就可以提升CMS在重新标记阶段的性能
可能发生OOM的区域
1.Metaspace 空间
Metaspace区域用来存放类信息
参数
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
回收条件
1.类加载器先要被回收
2.类的所有对象实例要被回收.
3. 等等....
OMM原因
1.设置的Metaspace空间比较小,运行较大系统,空间不够用,导致OOM
默认 : 20.79M
最大值: 无限大
2. 虚拟机栈内存
每一个线程都有一个自己的虚拟机栈[栈内存]. 然后线程只要执行一个方法,就会为这个方法创建一个栈帧,将栈帧放到自己的虚拟机栈中,<br>然后再这个栈帧里放入方法中定义的各种局部变量. 每个线程的虚拟机栈内存大小是固定的.<br>
一般都是程序bug : 比如递归
3.堆内存空间
JVM分配给堆内存的空间其实一般都是固定的,所以堆内存可能发生OOM
1. 系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,导致OOM
2.系统内存泄漏,就是莫名其妙弄了很多对象,结果对象都是存活的,没有及时取消对他们的引用,<br>导致触发GC还是无法回收,此时只能引发内存溢出,因为实在是放不下更多的对象了.<br>
有限的内存中放了过多的对象,而且大多数都是存活的,此时即使是GC之后还是大部分都存活,<br>所以要继续放入更多对象已经不可能的,此时只能引发内存溢出问题.<br>
要么是系统负载搞,要么是内存泄漏
通用模板
-server<br>-Xms4096M <br>-Xmx4096M <br>-Xmn3072M <br>-Xss1M <br>-XX:PermSize=256M ===> [ 替换 MetaspaceSize 元空间 ] <br>-XX:MaxPermSize=256M ===> [ 替换 MaxMetaspaceSize 元空间 ] <br>-XX:+UseParNewGC <br>-XX:+UseConcMarkSweepGC<br>-XX: CMSInitiatingOccupancyFraction =92<br>-XX:UseCMSInitiatingOccupancyOnly<br>-XX:+UseCMSCompactAtFullCollection<br>-XX:CMSFullGCsBeforeCompaction=0<br>-XX:+CMSParallelInitialMarkEnabled<br>-XX:+CMSScavengeBeforeRemark<br>-XX:+CMSParallelRemarkEnabled<br>-XX:+PrintHeapAtGC<br>-XX:DisableExplicitGC<br>-XX:PringGCDetails<br>-Xloggc:gc.log<br>-XX:+HeapDumpOnOutOfMemoryError <br>-XX:HeapDumpPath=/app/logs/log_hprof/gc.hprof <br><br>----------下面的参数看情况----------<br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5 <br>-XX:PretenureSizeThreshold=1M <br><br>-XX:LargePageSizeInBytes=128m <br>-XX:+UseFastAccessorMethods <br>-XX:MaxDirectMemorySize=40M<br>
查找默认值
java -XX:+PrintFlagsFinal -version | grep UseFastAccessorMethods<br>
-XX:DisableExplicitGC
禁止手动回收GC
DirectByteBuffer 使用的时候垃圾回收会出问题,可以会导致OOM : Direct buffer memory
由于内存部分的不合理,young gc 之后, 一些活下来的对象(包括一些DirectByteBuffer在内), 会因为超过survivor的大小或者因为动态年龄判断直接进入老年代. <br><br> 随着young gc次数的增多, 老年代里面的DirectByteBuffer对象越来越多, 而且这些DirectByteBuffer都是关联了很多对外内存的,这些老年代的DirectByteBuffer其实很多都是可以回收的状态了,但是因为老年代没有到触发Full GC 的条件. 所以 老年代里这些没有回收的DirectByteBuffer就一直关联占据大量的堆外内存空间. <br><br>直到最后,当你继续要使用堆外内存的时候,结果所有的堆外内存都被老年代里大量的DirectByteBuffer占用了,虽然他们可以回收,<br>但是无奈没有触发Full GC的条件,所以堆外内存也始终无法被回收掉.<br><br>最终导致OOM : Direct buffer memory<br>
Java NIO 的源码会做如下处理: 每次分配新的堆外内存的时候,都会调用System.gc() <br>去提醒JVM主动执行GC去回收掉一些没有引用的DirectByteBuffer对象,释放堆外内存空间<br><br>
G1
概念
把Java堆内存拆分为多个大小相等的Region
G1也会有新生代和老年代的概念,但是只不过是逻辑上的概念
G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间
希望G1在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的“Stop the World”时间,<br>也就是系统停顿的时间,不能超过1分钟
其实我们对内存合理分配,优化一些参数,就是为<br>了尽可能减少Minor GC和Full GC,尽量减少GC带来的系统停顿,避免影响系统处理请求。
直接可以给G1指定,在一个时间内,垃圾回收导致的系统停顿时间不能超过多久,G1全权给你负责,保证达到这个目标
相当于我们就可以直接控制垃圾回收对系统性能的影响了
核心设计思路
G1可以做到让你来设定垃圾回收对系统的影响,<br>他自己通过把内存拆分为大量小Region,<br>以及追踪每个Region中可以回收的对象大小和预估时间,<br>最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,<br>同时在有限的时 间内尽量回收尽可能多的垃圾对象。<br>
Region可能属于新生代也可能属于老年代
Region随时会属于新生代也会属于老年代,<br>所以没有所谓新生代给多少内存,老年代给多少内存这一说了<br>
如何设定G1对应的内存大小
默认情况下自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。
然后JVM启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048
因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的。
堆大小是4G,那么就是4096MB,此时除以2048个Region,每个Region的大小就是2MB。<br>大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以<br>
手动方式来指定,则是“-XX:G1HeapRegionSize”
默认
刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,<br>对应大概是100个Region,这个是可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可<br>
因为在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,<br>可以通过“-XX:G1MaxNewSizePercent”。<br>
而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。
新生代还有Eden和Survivor的概念吗?
没错,其实在G1中虽然把内存划分为了很多的 Region,但是其实还是有新生代、老年代的区分
而且新生代里还是有Eden和Survivor的划分的
新生代的参数,“-XX:SurvivorRatio=8”,所以这里还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor。
比如新生代之前说刚开始初始的时候,有100个Region,那么可能80个Region就是Eden,两个Survivor各自占10个Region
有Eden和Survivor的概念的,他们会各自占据不同的Region。<br>只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。<br>
G1的新生代垃圾回收
随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。
一旦新生代达到了设定的占据堆内存的最大大小60%,<br>比如都有1200个Region了,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区还占满了对象<br>
这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“Stop the World”状态
然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象
但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,<br>可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms<br>
什么时候触发新生代+老年代的混合垃圾回收?
G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%<br>如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段<br>
G1垃圾回收的过程
初始标记
首先会触发一个“初始标记”的操作,这个过程是需要进入“Stop the World”的,仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。<br>
先停止系统程序的运行,<br>然后对各个线程栈内存中的局部变量代表的GC Roots,<br>以及方法区中的类静态变量代表的GC Roots,进行扫描,标记出来他们直接引用的那些对象。<br>
并发标记
接着会进入“并发标记”的阶段,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象
这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。
但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大。
而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。
最终标记阶段
这个阶段会进入“Stop the World”,系统程序是禁止运行的,<br>但是会根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象<br>
混合回收
最后一个阶段,就是“混合回收“阶段,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。
接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。
比如说老年代此时有1000个Region都满了,但是因为根据预定目标,本次垃圾回收可能只能停顿200毫秒,那么通过之前的计算得知,<br>可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,把GC导致的停顿时间控制在我们指定的范围内<br>
其实老年代对堆内存占比达到45%的时候,触发的是“混合回收
也就是说,此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象
到底是回收这些区域的哪些Region呢?
那就要看情况了,因为我们设定了对GC停顿时间的目标,所以说他会从新生代、老年代、大对象里各自挑选一些Region,<br>保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收<br>
G1垃圾回收器的一些参数
-XX:G1MixedGCCountTarget
一般在老年代的Region占据了堆内存的Region的45%之后,会触发一个混合回收的过程,也就是Mixed GC
在这里最后一个环节,其实就是执行混合回收,从新生代和老年代里都回收一些Region。
但是最后一个阶段混合回收的时候,其实会停止所有程序运行,所以说G1是允许执行多次混合回收
比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。
有一些参数可以控制这个,比如“-XX:G1MixedGCCountTarget”参数,就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次
意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次
假设一次混合回收预期要回收掉一共有160个Region,那么此时第一次混合回收,会回收掉一些Region,比如就是 20个Region(年轻代10个,老年代10个)。<br>接着恢复系统运行一会儿,然后再执行一次“混合回收”(年轻代10个,老年代10个)<br>如此反复执行8次混合回收阶段之后 ,不就把预订的160个Region都回收掉了?而且还把系统停顿时间控制在指定范围内。<br><br>
那么为什么要反复回收多次呢?
因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽<br><br>可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下
-XX:G1HeapWastePercent
“-XX:G1HeapWastePercent”,默认值是5%
在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他<br>Region,然后这个Region中的垃圾对象全部清理掉
这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意味着本次混合回收就结束了。
而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。
-XX:G1MixedGCLiveThresholdPercent
默认值是85%
确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收
否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。
回收失败时的Full GC
如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region去
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的
程序假死
先用top查看一下机器负载
使用了大量内存,并且内存无法释放,导致GC
1. 内存使用率高,导致频繁full gc, gc带来的STW影响业务
2.内存使用率过多.导致JVM发生OOM
3.内存使用率过高,也许有的时候,导致这个进程因为申请内存不足,直接被操作系统把这个进程杀掉了
机器CPU负载过高,也许是某个进程耗尽CPU,导致你的服务线程无法获取到cpu资源
收藏
收藏
0 条评论
下一页