分类: jvm

  • 面试官:一台服务器最大能支持多少条 TCP 连接?问倒一大片。。。

    快乐分享,Java干货及时送达👇

    来源:juejin.cn/post/7162824884597293086

    • 一台服务器最大能打开的文件数
    • 调整服务器能打开的最大文件数示例
    • 一台服务器最大能支持多少连接
    • 一台客户端机器最多能发起多少条连接
    • 其他
    • 相关实际问题
    图片

    之前有一位读者诉苦,有次面试,好不容易(今年行情大家都懂的)熬到到技术终面,谁知道面试官突然放个大招问他:一台服务器最大能支持多少条 TCP 连接,把他直接给问懵逼了 。。。。(请自行脑补那尴尬的场面与气氛)。

    所以,今天就来讨论一下这个问题。

    一台服务器最大能打开的文件数

    限制参数

    我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:

    • fs.file-max (系统级别参数) :该参数描述了整个系统可以打开的最大文件数量。但是root用户不会受该参数限制(比如:现在整个系统打开的文件描述符数量已达到fs.file-max ,此时root用户仍然可以使用ps、kill等命令或打开其他文件描述符)。
    • soft nofile(进程级别参数) :限制单个进程上可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同的值。
    • fs.nr_open(进程级别参数) :限制单个进程上可以打开的最大文件数。可以针对不同用户配置不同的值。

    这三个参数之间还有耦合关系,所以配置值的时候还需要注意以下三点:

    1. 如果想加大soft nofile,那么hard nofile参数值也需要一起调整。如果因为hard nofile参数值设置的低,那么soft nofile参数的值设置的再高也没有用,实际生效的值会按照二者最低的来。
    2. 如果增大了hard nofile,那么fs.nr_open也都需要跟着一起调整(fs.nr_open参数值一定要大于hard nofile参数值)。如果不小心把hard nofile的值设置的比fs.nr_open还大,那么后果比较严重。会导致该用户无法登录,如果设置的是*,那么所有用户都无法登录。
    3. 如果加大了fs.nr_open,但是是用的echo “xxx” > ../fs/nr_open命令来修改的fs.nr_open的值,那么刚改完可能不会有问题,但是只要机器一重启,那么之前通过echo命令设置的fs.nr_open值便会失效,用户还是无法登录。所以非常不建议使用echo的方式修改内核参数!!!

    调整服务器能打开的最大文件数示例

    假设想让进程可以打开100万个文件描述符,这里用修改conf文件的方式给出一个建议。如果日后工作里有类似的需求可以作为参考。

    vim /etc/sysctl.conf

    fs.file-max=1100000 // 系统级别设置成110万,多留点buffer  
    fs.nr_open=1100000 // 进程级别也设置成110万,因为要保证比 hard nofile大

    使上面的配置生效sysctl -p

    vim /etc/security/limits.conf
        
    // 用户进程级别都设置成100完  
    soft nofile 1000000  
    hard nofile 1000000

    一台服务器最大能支持多少连接

    我们知道TCP连接,从根本上看其实就是client和server端在内存中维护的一组【socket内核对象】(这里也对应着TCP四元组:源IP、源端口、目标IP、目标端口),他们只要能够找到对方,那么就算是一条连接。那么一台服务器最大能建立多少条连接呢?

    • 由于TCP连接本质上可以理解为是client-server端的一对socket内核对象,那么从理论上将应该是【2^32 (ip数) * 2^16 (端口数)】条连接(约等于两百多万亿)。
    • 但是实际上由于受其他软硬件的影响,我们一台服务器不可能能建立这么多连接(主要是受CPU和内存限制)。

    如果只以ESTABLISH状态的连接来算(这些连接只是建立,但是不收发数据也不处理相关的业务逻辑)那么一台服务器最大能建立多少连接呢?以一台4GB内存的服务器为例!

    • 这种情况下,那么能建立的连接数量主要取决于【内存的大小】(因为如果是)ESTABLISH状态的空闲连接,不会消耗CPU(虽然有TCP保活包传输,但这个影响非常小,可以忽略不计)。
    • 我们知道一条ESTABLISH状态的连接大约消耗【3.3KB内存】,那么通过计算得知一台4GB内存的服务器,【可以建立100w+的TCP连接】(当然这里只是计算所有的连接都只建立连接但不发送和处理数据的情况,如果真实场景中有数据往来和处理(数据接收和发送都需要申请内存,数据处理便需要CPU),那便会消耗更高的内存以及占用更多的CPU,并发不可能达到100w+)。

    上面讨论的都是进建立连接的理想情况,在现实中如果有频繁的数据收发和处理(比如:压缩、加密等),那么一台服务器能支撑1000连接都算好的了,所以一台服务器能支撑多少连接还要结合具体的场景去分析,不能光靠理论值去算。抛开业务逻辑单纯的谈并发没有太大的实际意义。

    服务器的开销大头往往并不是连接本身,而是每条连接上的数据收发,以及请求业务逻辑处理!!!

    一台客户端机器最多能发起多少条连接

    我们知道客户端每和服务端建立一个连接便会消耗掉client端一个端口。一台机器的端口范围是【0 ~ 65535】,那么是不是说一台client机器最多和一台服务端机器建立65535个连接呢(这65535个端口里还有很多保留端口,可用端口可能只有64000个左右)?

    由TCP连接的四元组特性可知,只要四元组里某一个元素不同,那么就认为这是不同的TCP连接。所以需要分情况讨论:

    情况一 】、如果一台client仅有一个IP,server端也仅有一个IP并且仅启动一个程序,监听一个端口的情况下,client端和这台server端最大可建立的连接条数就是 65535 个。

    因为源IP固定,目标IP和端口固定,四元组中唯一可变化的就是【源端口】,【源端口】的可用范围又是【0 ~ 65535】,所以一台client机器最大能建立65535个连接。

    情况二 】、如果一台client有多个IP(假设客户端有 n 个IP),server端仅有一个IP并且仅启动一个程序,监听一个端口的情况下,一台client机器最大能建立的连接条数是:n * 65535 个。

    因为目标IP和端口固定,有 n 个源IP,四元组中可变化的就是【源端口】+ 【源IP】,【源端口】的可用范围又是【0 ~ 65535】,所以一个IP最大能建立65535个连接,那么n个IP最大就能建立 n * 65535个连接了。

    以现在的技术,给一个client分配多个IP是非常容易的事情,只需要去联系你们网管就可以做到。

    情况三 】、如果一台client仅有一个IP,server端也仅有一个IP但是server端启动多个程序,每个程序监听一个端口的情况下(比如server端启动了m个程序,监听了m个不同端口),一台client机器最大能建立的连接数量为:65535 * m。

    源IP固定,目标IP固定,目标端口数量为m个,可变化的是源端口,而源端口变化范围是【0 ~ 65535】,所以一台client机器最大能建立的TCP连接数量是 65535 * m个。

    • 其余情况类推,但是客户端的可用端口范围一般达不到65535个,受内核参数net.ipv4.ip_local_port_range限制,如果要修改client所能使用的端口范围,可以修改这个内核参数的值。
    • 所以,不光是一台server端可以接收100w+个TCP连接,一台client照样能发出100w+个连接。

    其他

    • 三次握手里socket的全连接队列长度由参数net.core.somaxconn来控制,默认大小是128,当两台机器离的非常近,但是建立连接的并发又非常高时,可能会导致半连接队列或全连接队列溢出,进而导致server端丢弃握手包。然后造成client超时重传握手包(至少1s以后才会重传),导致三次握手连接建立耗时过长。我们可以调整参数net.core.somaxconn来增加去按连接队列的长度,进而减小丢包的影响
    • 有时候我们通过 ctrl + c方式来终止了某个进程,但是当重启该进程的时候发现报错端口被占用,这种问题是因为【操作系统还没有来得及回收该端口,等一会儿重启应用就好了】
    • client程序在和server端建立连接时,如果client没有调用bind方法传入指定的端口,那么client在和server端建立连接的时候便会自己随机选择一个端口来建立连接。一旦我们client程序调用了bind方法传入了指定的端口,那么client将会使用我们bind里指定的端口来和server建立连接。所以不建议client调用bind方法,bind函数会改变内核选择端口的策略
    public static void main(String[] args) throws IOException {  
        SocketChannel sc = SocketChannel.open();  
       // 客户端还可以调用bind方法  
        sc.bind(new InetSocketAddress("localhost", 9999));  
        sc.connect(new InetSocketAddress("localhost", 8080));  
        System.out.println("waiting..........");  
    }
    • 在Linux一切皆文件,当然也包括之前TCP连接中说的socket。进程打开一个socket的时候需要创建好几个内核对象,换一句直白的话说就是打开文件对象吃内存,所以Linux系统基于安全角度考虑(比如:有用户进程恶意的打开无数的文件描述符,那不得把系统搞奔溃了),在多个位置都限制了可打开的文件描述符的数量。
    • 内核是通过【hash表】的方式来管理所有已经建立好连接的socket,以便于有请求到达时快速的通过【TCP四元组】查找到内核中对应的socket对象。

    在epoll模型中,通过红黑树来管理epoll对象所管理的所有socket,用红黑树结构来平衡快速删除、插入、查找socket的效率。

    相关实际问题

    在网络开发中,很多人对一个基础问题始终没有彻底搞明白,那就是一台机器最多能支撑多少条TCP连接。不过由于客户端和服务端对端口使用方式不同,这个问题拆开来理解要容易一些。

    注意,这里说的是客户端和服务端都只是角色,并不是指某一台具体的机器。例如对于我们自己开发的应用程序来说,当他响应客户端请求的时候,他就是服务端。当他向MySQL请求数据的时候,他又变成了客户端。

    “too many open files” 报错是怎么回事,该如何解决

    你在线上可能遇到过too many open files这个错误,那么你理解这个报错发生的原理吗?如果让你修复这个错误,应该如何处理呢?

    • 因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制的打开了过多文件而让整个服务器奔溃,Linux对打开的文件描述符数量有限制。如果你的进程触发到内核的限制,那么”too many open files” 报错就产生了。
    • 可以通过修改fs.file-max 、soft nofile、fs.nr_open这三个参数的值来修改进程能打开的最大文件描述符数量。

    需要注意这三个参数之间的耦合关系!

    一台服务端机器最大究竟能支持多少条连接

    因为这里要考虑的是最大数,因此先不考虑连接上的数据收发和处理,仅考虑ESTABLISH状态的空连接。那么一台服务端机器上最大可以支持多少条TCP连接?这个连接数会受哪些因素的影响?

    • 在不考虑连接上数据的收发和处理的情况下,仅考虑ESTABLISH状态下的空连接情况下,一台服务器上最大可支持的TCP连接数量基本上可以说是由内存大小来决定的。
    • 四元组唯一确定一条连接,但服务端可以接收来自任意客户端的请求,所以根据这个理论计算出来的数字太大,没有实际意义。另外文件描述符限制其实也是内核为了防止某些应用程序不受限制的打开【文件句柄】而添加的限制。这个限制只要修改几个内核参数就可以加大。
    • 一个socket大约消耗3kb左右的内存,这样真正制约服务端机器最大并发数的就是内存,拿一台4GB内存的服务器来说,可以支持的TCP连接数量大约是100w+。
    一条客户端机器最大究竟能支持多少条连接

    和服务端不同的是,客户端每次建立一条连接都需要消耗一个端口。在TCP协议中,端口是一个2字节的整数,因此范围只能是0~65535。那么客户单最大只能支持65535条连接吗?有没有办法突破这个限制,有的话有哪些办法?

    • 客户度每次建立一条连接都需要消耗一个端口。从数字上来看,似乎最多只能建立65535条连接。但实际上我们有两种办法破除65535这个限制。

    方式一,为客户端配置多IP 方式二,分别连接不同的服务端

    • 所以一台client发起百万条连接是没有任何问题的。
    做一个长连接推送产品,支持1亿用户需要多少台机器

    假设你是系统架构师,现在老板给你一个需求,让你做一个类似友盟upush这样的产品。要在服务端机器上保持一个和客户端的长连接,绝大部分情况下连接都是空闲的,每天也就顶多推送两三次左右。总用户规模预计是1亿。那么现在请你来评估一下需要多少台服务器可以支撑这1亿条长连接。

    • 对于长连接推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多一两次。绝大部分情况下TCP连接都是空闲的,CPU开销可以忽略。
    • 再基于内存来考虑,假设服务器内存是128G的,那么一台服务器可以考虑支持500w条并发。这样会消耗掉大约不到20GB内存用来保存这500w条连接对应的socket。还剩下100GB以上的内存来应对接收、发送缓冲区等其他的开销足够了。所以,一亿用户,仅仅需要20台服务器就差不多够用了!


  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    快乐分享,Java干货及时送达👇

    文章来源:http://t.csdn.cn/g8GN1


    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。

    我看网上出现了很多不靠谱的答案。

    这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    由于题目中给出的OOM,java中OOM又分很多类型;比如:

    • 堆溢出(“java.lang.OutOfMemoryError: Java heap space”)

    • 永久带溢出(“java.lang.OutOfMemoryError:Permgen space”)

    • 不能创建线程(“java.lang.OutOfMemoryError:Unable to create new native thread”)

    等很多种情况。

    本文主要是分析堆溢出对应用带来的影响。

    先说一下答案,答案是还能运行 。

    代码如下:

    public class JvmThread {


        public static void main(String[] args) {
            new Thread(() -> {
                Listbyte[]> list = new ArrayListbyte[]>();
                while (true) {
                    System.out.println(new Date().toString() + Thread.currentThread() + "==");
                    byte[] b = new byte[1024 * 1024 * 1];
                    list.add(b);
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            // 线程二
            new Thread(() -> {
                while (true) {
                    System.out.println(new Date().toString() + Thread.currentThread() + "==");
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    结果展示:

    Wed Nov 07 14:42:18 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:18 CST 2018Thread[Thread-0,5,main]==
    Wed Nov 07 14:42:19 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:19 CST 2018Thread[Thread-0,5,main]==
    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
     at com.gosaint.util.JvmThread.lambda$main$0(JvmThread.java:21)
     at com.gosaint.util.JvmThread$$Lambda$1/521645586.run(Unknown Source)
     at java.lang.Thread.run(Thread.java:748)
    Wed Nov 07 14:42:20 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:21 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:22 CST 2018Thread[Thread-1,5,main]==

    JVM启动参数设置:

    上图是JVM堆空间的变化。我们仔细观察一下在14:42:05~14:42:25之间曲线变化,你会发现使用堆的数量,突然间急剧下滑!

    这代表这一点,当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行!

    讲到这里大家应该懂了,此题的答案为:一个线程溢出后,进程里的其他线程还能照常运行

    注意了,这个例子我只演示了堆溢出的情况。如果是栈溢出,结论也是一样的,大家可自行通过代码测试。

    总结:

    其实发生OOM的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。

  • 阿里终面:每天100w次登陆请求, 8G 内存该如何设置JVM参数?

    快乐分享,Java干货及时送达👇

    就在上周,一个同学在阿里云技术面终面的时候被问到这么一个问题:假设一个每天100w次登陆请求的平台,一个服务节点 8G 内存,该如何设置JVM参数? 。
    下面以面试题的形式给大家梳理出来,做到一箭双雕: 
    • 既供大家实操参考
    • 又供大家面试参考
    大家要学习的,除了 JVM 配置方案 之外,是其 分析问题的思路、思考问题的视角。这些思路和视角,能帮助大家走更远、更远。
    接下来,进入正题。

    每天100w次登陆请求, 8G 内存该如何设置JVM参数?

    每天100w次登陆请求, 8G 内存该如何设置JVM参数,大概可以分为以下8个步骤

    Step1:新系统上线如何规划容量?

    1.套路总结

    任何新的业务系统在上线以前都需要去估算服务器配置和JVM的内存参数,这个容量与资源规划并不仅仅是系统架构师的随意估算的,需要根据系统所在业务场景去估算,推断出来一个系统运行模型,评估JVM性能和GC频率等等指标。以下是我结合大牛经验以及自身实践来总结出来的一个建模步骤:
    • 计算业务系统每秒钟创建的对象会佔用多大的内存空间,然后计算集群下的每个系统每秒的内存佔用空间(对象创建速度)
    • 设置一个机器配置,估算新生代的空间,比较不同新生代大小之下,多久触发一次MinorGC。
    • 为了避免频繁GC,就可以重新估算需要多少机器配置,部署多少台机器,给JVM多大内存空间,新生代多大空间。
    • 根据这套配置,基本可以推算出整个系统的运行模型,每秒创建多少对象,1s以后成为垃圾,系统运行多久新生代会触发一次GC,频率多高。
    2.套路实战——以登录系统为例
    有些同学看到这些步骤还是发憷,说的好像是那么回事,一到实际项目中到底怎麽做我还是不知道!
    光说不练假把式,以登录系统为例模拟一下推演过程:
    • 假设每天100w次登陆请求,登陆峰值在早上,预估峰值时期每秒100次登陆请求。
    • 假设部署3台服务器,每台机器每秒处理30次登陆请求,假设一个登陆请求需要处理1秒钟,JVM新生代里每秒就要生成30个登陆对象,1s之后请求完毕这些对象成为了垃圾。
    • 一个登陆请求对象假设20个字段,一个对象估算500字节,30个登陆佔用大约15kb,考虑到RPC和DB操作,网络通信、写库、写缓存一顿操作下来,可以扩大到20-50倍,大约1s产生几百k-1M数据。
    • 假设2C4G机器部署,分配2G堆内存,新生代则只有几百M,按照1s1M的垃圾产生速度,几百秒就会触发一次MinorGC了。
    • 假设4C8G机器部署,分配4G堆内存,新生代分配2G,如此需要几个小时才会触发一次MinorGC。
    所以,可以粗略的推断出来一个每天100w次请求的登录系统,按照4C8G的3实例集群配置,分配4G堆内存、2G新生代的JVM,可以保障系统的一个正常负载。
    基本上把一个新系统的资源评估了出来,所以搭建新系统要每个实例需要多少容量多少配置,集群配置多少个实例等等这些,并不是拍拍脑袋和胸脯就可以决定的下来的。

    Step2:该如何进行垃圾回收器的选择?

    吞吐量还是响应时间

    首先引入两个概念:吞吐量和低延迟
    吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
    响应时间 = 平均每次的GC的耗时
    通常,吞吐优先还是响应优先这个在JVM中是一个两难之选。
    堆内存增大,gc一次能处理的数量变大,吞吐量大;但是gc一次的时间会变长,导致后面排队的线程等待时间变长;相反,如果堆内存小,gc一次时间短,排队等待的线程等待时间变短,延迟减少,但一次请求的数量变小(并不绝对符合)。
    无法同时兼顾,是吞吐优先还是响应优先,这是一个需要权衡的问题。

    垃圾回收器设计上的考量

    • JVM在GC时不允许一边垃圾回收,一边还创建新对象(就像不能一边打扫卫生,还在一边扔垃圾)。
    • JVM需要一段Stop the world的暂停时间,而STW会造成系统短暂停顿不能处理任何请求;
    • 新生代收集频率高,性能优先,常用复制算法;老年代频次低,空间敏感,避免复制方式。
    • 所有垃圾回收器的涉及目标都是要让GC频率更少,时间更短,减少GC对系统影响!

    CMS和G1

    目前主流的垃圾回收器配置是新生代采用ParNew,老年代采用CMS组合的方式,或者是完全采用G1回收器,
    从未来的趋势来看,G1是官方维护和更为推崇的垃圾回收器。

    业务系统:

    • 延迟敏感的推荐CMS;
    • 大内存服务,要求高吞吐的,采用G1回收器!

    CMS垃圾回收器的工作机制

    CMS主要是针对老年代的回收器,老年代是标记-清除,默认会在一次FullGC算法后做整理算法,清理内存碎片。

    CMS GC 描述 Stop the world 速度
    1.开始标记 初始标记仅标记GCRoots能直接关联到的对象,速度很快 Yes 很快
    2.并发标记 并发标记阶段就是进行GCRoots Tracing的过程 No
    3.重新标记 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。 Yes 很快
    4.垃圾回收 并发清理垃圾对象(标记清除算法) No
    • 优点:并发收集、主打“低延时” 。在最耗时的两个阶段都没有发生STW,而需要STW的阶段都以很快速度完成。
    • 缺点:1、消耗CPU;2、浮动垃圾;3、内存碎片
    • 适用场景:重视服务器响应速度,要求系统停顿时间最短。

    总之:

    业务系统,延迟敏感的推荐CMS;

    大内存服务,要求高吞吐的,采用G1回收器!

    Step3:如何对各个分区的比例、大小进行规划

    一般的思路为:
    首先,JVM最重要最核心的参数是去评估内存和分配,第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。
    其次,需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定,针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。
    最后,是设置-Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。
    JVM参数 描述 默认 推荐
    -Xms Java堆内存的大小 OS内存64/1 OS内存一半
    -Xmx Java堆内存的最大大小 OS内存4/1 OS内存一半
    -Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了 跌认堆的1/3 sun推荐3/8
    -Xss 每个线程的栈内存大小 和idk有关 sun
    对于8G内存,一般分配一半的最大内存就可以了,因为机器本上还要占用一定内存,一般是分配4G内存给JVM,
    引入性能压测环节,测试同学对登录接口压至1s内60M的对象生成速度,采用ParNew+CMS的组合回收器,
    正常的JVM参数配置如下:
    -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 
    这样设置可能会由于动态对象年龄判断原则导致频繁full gc。为啥呢?
    压测过程中,短时间(比如20S后)Eden区就满了,此时再运行的时候对象已经无法分配,会触发MinorGC,
    假设在这次GC后S1装入100M,马上过20S又会触发一次MinorGC,多出来的100M存活对象+S1区的100M已经无法顺利放入到S2区,此时就会触发JVM的动态年龄机制,将一批100M左右的对象推到老年代保存,持续运行一段时间,系统可能一个小时候内就会触发一次FullGC。
    按照默认8:1:1的比例来分配时,  survivor区只有 1G的 10%左右,也就是几十到100M,
    如果  每次minor GC垃圾回收过后进入survivor对象很多,并且survivor对象大小很快超过 Survivor 的 50% ,  那么会触发动态年龄判定规则,让部分对象进入老年代.
    而一个GC过程中,可能部分WEB请求未处理完毕,  几十兆对象,进入survivor的概率,是非常大的,甚至是一定会发生的.

    如何解决这个问题呢?为了让对象尽可能的在新生代的eden区和survivor区, 尽可能的让survivor区内存多一点,达到200兆左右,

    于是我们可以更新下JVM参数设置:

    -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8  

    说明:
    ‐Xmn2048M ‐XX:SurvivorRatio=8 
    年轻代大小2g,eden与survivor的比例为8:1:1,也就是1.6g:0.2g:0.2g

    survivor达到200m,如果几十兆对象到底survivor, survivor 也不一定超过 50%

    这样可以防止每次垃圾回收过后,survivor对象太早超过 50% ,

    这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,

    什么是JVM动态年龄判断规则呢?

    对象进入老年代的动态年龄判断规则(动态晋升年龄计算阈值):Minor GC 时,Survivor 中年龄 1 到 N 的对象大小超过 Survivor 的 50% 时,则将大于等于年龄 N 的对象放入老年代。

    核心的优化策略是:是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc

    应该如何去评估新生代内存和分配合适?

    这里特别说一下,JVM最重要最核心的参数是去评估内存和分配,

    第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,

    后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。

    其次需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定:

    • 针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;
    • 而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。

    服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。

    step4:栈内存大小多少比较合适?

    -Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。

    step5:对象年龄应该为多少才移动到老年代比较合适?

    假设一次minor gc要间隔二三十秒,并且,大多数对象一般在几秒内就会变为垃圾,

    如果对象这么长时间都没被回收,比如2分钟没有回收,可以认为这些对象是会存活的比较长的对象,从而移动到老年代,而不是继续一直占用survivor区空间。

    所以,可以将默认的15岁改小一点,比如改为5,

    那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了(5*30s= 150s),和几秒的时间相比,对象已经存活了足够长时间了。

    所以:可以适当调整JVM参数如下:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 

    step6:多大的对象,可以直接到老年代比较合适?

    对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),一般可以结合自己系统看下有没有什么大对象 生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,

    所以:可以适当调整JVM参数如下:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M

    step7:垃圾回收器CMS老年代的参数优化

    JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),

    如果内存较大(超过4个G,只是经验 值),还是建议使用G1.

    这里是4G以内,又是主打“低延时” 的业务系统,可以使用下面的组合:

    ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)

    新生代的采用ParNew回收器,工作流程就是经典复制算法,在三块区中进行流转回收,只不过采用多线程并行的方式加快了MinorGC速度。

    老生代的采用CMS。再去优化老年代参数:比如老年代默认在标记清除以后会做整理,还可以在CMS的增加GC频次还是增加GC时长上做些取舍,

    如下是响应优先的参数调优:

    XX:CMSInitiatingOccupancyFraction=70

    设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)

    XX:+UseCMSInitiatinpOccupancyOnly

    和上面搭配使用,否则只生效一次

    -XX:+AlwaysPreTouch

    强制操作系统把内存真正分配给IVM,而不是用时才分配。

    综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=70 ‐XX:+UseCMSInitiatingOccupancyOnly ‐XX:+AlwaysPreTouch

    参数解释

    1.‐Xms3072M ‐Xmx3072M 最小最大堆设置为3g,最大最小设置为一致防止内存抖动

    2.‐Xss1M 线程栈1m

    3.‐Xmn2048M ‐XX:SurvivorRatio=8 年轻代大小2g,eden与survivor的比例为8:1:1,也就是1.6g:0.2g:0.2g

    4.-XX:MaxTenuringThreshold=5 年龄为5进入老年代 5.‐XX:PretenureSizeThreshold=1M 大于1m的大对象直接在老年代生成

    6.‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC 使用ParNew+cms垃圾回收器组合

    7.‐XX:CMSInitiatingOccupancyFraction=70 老年代中对象达到这个比例后触发fullgc

    8.‐XX:+UseCMSInitiatinpOccupancyOnly  老年代中对象达到这个比例后触发fullgc,每次

    9.‐XX:+AlwaysPreTouch 强制操作系统把内存真正分配给IVM,而不是用时才分配。

    step8:配置OOM时候的内存dump文件和GC日志

    额外增加了GC日志打印、OOM自动dump等配置内容,帮助进行问题排查

    -XX:+HeapDumpOnOutOfMemoryError

    在Out Of Memory,JVM快死掉的时候,输出Heap Dump到指定文件。

    不然开发很多时候还真不知道怎么重现错误。

    路径只指向目录,JVM会保持文件名的唯一性,叫java_pid${pid}.hprof。

    -XX:+HeapDumpOnOutOfMemoryError 
    -XX:HeapDumpPath=${LOGDIR}/

    因为如果指向特定的文件,而文件已存在,反而不能写入。

    输出4G的HeapDump,会导致IO性能问题,在普通硬盘上,会造成20秒以上的硬盘IO跑满,

    需要注意一下,但在容器环境下,这个也会影响同一宿主机上的其他容器。

    GC的日志的输出也很重要:

    -Xloggc:/dev/xxx/gc.log 
    -XX:+PrintGCDateStamps 
    -XX:+PrintGCDetails

    GC的日志实际上对系统性能影响不大,打日志对排查GC问题很重要。

    一份通用的JVM参数模板

    一般来说,大企业或者架构师团队,都会为项目的业务系统定制一份较为通用的JVM参数模板,但是许多小企业和团队可能就疏于这一块的设计,如果老板某一天突然让你负责定制一个新系统的JVM参数,你上网去搜大量的JVM调优文章或博客,结果发现都是零零散散的、不成体系的JVM参数讲解,根本下不了手,这个时候你就需要一份较为通用的JVM参数模板了,不能保证性能最佳,但是至少能让JVM这一层是稳定可控的,

    在这里给大家总结了一份模板:

    基于4C8G系统的ParNew+CMS回收器模板(响应优先),新生代大小根据业务灵活调整!

    -Xms4g
    -Xmx4g
    -Xmn2g
    -Xss1m
    -XX:SurvivorRatio=8
    -XX:MaxTenuringThreshold=10
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=70
    -XX:+UseCMSInitiatingOccupancyOnly
    -XX:+AlwaysPreTouch
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log

    如果是GC的吞吐优先,推荐使用G1,基于8C16G系统的G1回收器模板:

    G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,

    即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单!

    同时也不要自己显式设置新生代的大小(用-Xmn或-XX:NewRatio参数),

    如果人为干预新生代的大小,会导致目标时间这个参数失效。

    -Xms8g
    -Xmx8g
    -Xss1m
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=150
    -XX:InitiatingHeapOccupancyPercent=40
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log
    G1参数 描述 默认值
    XX:MaxGCPauseMillis=N 最大GC停顿时间。柔性目标,JVM满足90%,不保证100%。 200
    -XX:nitiatingHeapOccupancyPercent=n 当整个堆的空间使用百分比超过这个值时,就会融发MixGC 45

    针对-XX:MaxGCPauseMillis来说,参数的设置带有明显的倾向性:调低↓:延迟更低,但MinorGC频繁,MixGC回收老年代区减少,增大Full GC的风险。调高↑:单次回收更多的对象,但系统整体响应时间也会被拉长。

    针对InitiatingHeapOccupancyPercent来说,调参大小的效果也不一样:调低↓:更早触发MixGC,浪费cpu。调高↑:堆积过多代回收region,增大FullGC的风险。

    调优总结

    系统在上线前的综合调优思路:

    1、业务预估:根据预期的并发量、平均每个任务的内存需求大小,然后评估需要几台机器来承载,每台机器需要什么样的配置。

    2、容量预估:根据系统的任务处理速度,然后合理分配Eden、Surivior区大小,老年代的内存大小。

    3、回收器选型:响应优先的系统,建议采用ParNew+CMS回收器;吞吐优先、多核大内存(heap size≥8G)服务,建议采用G1回收器。

    4、优化思路:让短命对象在MinorGC阶段就被回收(同时回收后的存活对象

    5、到目前为止,总结到的调优的过程主要基于上线前的测试验证阶段,所以我们尽量在上线之前,就将机器的JVM参数设置到最优!

    JVM调优只是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,大多数的Java应用不需要进行JVM优化,我们可以遵循以下的一些原则:

    • 上线之前,应先考虑将机器的JVM参数设置到最优;
    • 减少创建对象的数量(代码层面);
    • 减少使用全局变量和大对象(代码层面);
    • 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
    • 分析GC情况优化代码比优化JVM参数更好(代码层面);

    通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。

    什么是ZGC?

    ZGC (Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。

    它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。

    在 JDK 11 新加入,还在实验阶段,

    主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。

    优点:低停顿,高吞吐量, ZGC 收集过程中额外耗费的内存小

    缺点:浮动垃圾

    目前使用的非常少,真正普及还是需要写时间的。

    如何选择垃圾收集器?

    在真实场景中应该如何去选择呢,下面给出几种建议,希望对你有帮助:

    1、如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。参数:-XX:+UseSerialGC 。

    2、如果你的应用运行在单核的机器上,或者你的虚拟机核数只有 单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。参数:-XX:+UseSerialGC 。

    3、如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。参数:-XX:+UseParallelGC 。

    4、如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。参数:-XX:+UseConcMarkSweepGC 、 -XX:+UseG1GC 、 -XX:+UseZGC 等。从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。

    选择性其实就集中在 CMS、G1、ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。

    Hotspot为什么使用元空间替换了永久代?

    什么是元空间?什么是永久代?为什么用元空间代替永久代?

    我们先回顾一下方法区吧,看看虚拟机运行时数据内存图,如下:

    方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

    什么是永久代?它和方法区有什么关系呢?

    如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。

    可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。

    在Java7及以前的版本,方法区都是永久代实现的。

    什么是元空间?它和方法区有什么关系呢?

    对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。

    换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。

    为什么使用元空间替换了永久代?

    永久代的方法区,和堆使用的物理内存是连续的。

    永久代是通过以下这两个参数配置大小的~

    • -XX:PremSize:设置永久代的初始大小
    • -XX:MaxPermSize: 设置永久代的最大值,默认是64M

    对于永久代,如果动态生成很多class的话,就很可能出现java.lang.OutOfMemoryError:PermGen space错误,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。

    JDK8之后,方法区存在于元空间(Metaspace)。

    物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器内存有多大,元空间就有多大

    可以通过以下的参数来设置元空间的大小:

    • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
    • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
    • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
    • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

    所以,为什么使用元空间替换永久代?

    表面上看是为了避免OOM异常。

    因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。

    当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

    什么是Stop The World ? 什么是OopMap?什么是安全点?

    进行垃圾回收的过程中,会涉及对象的移动。

    为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World。也简称为STW。

    在HotSpot中,有个数据结构(映射表)称为OopMap

    一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。

    在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

    这些特定的位置主要在:1.循环的末尾(非 counted 循环)

    2.方法临返回前 / 调用方法的call指令后

    3.可能抛异常的位置

    这些位置就叫作安全点(safepoint)。

    用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。