作者: chenyl

  • 帅呆!接口开发不用写Controller、Service、Dao、Mapper、XML、VO,全自动生成

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

    今天给小伙伴们介绍一个Java接口快速开发框架-magic-api

    简介

    magic-api 是一个基于 Java 的接口快速开发框架,编写接口将通过 magic-api 提供的 UI 界面完成,自动映射为 HTTP 接口,无需定义 Controller、Service、Dao、Mapper、XML、VO 等 Java 对象即可完成常见的 HTTP API 接口开发

    访问 http://localhost:9999/magic/web 进行操作

    文档地址:

    • https://ssssssss.org

    在线演示:

    • https://magic-api.ssssssss.org

    开源地址:

    • https://gitee.com/ssssssss-team/magic-api

    特性

    • 支持MySQL、MariaDB、Oracle、DB2、PostgreSQL、SQLServer 等支持jdbc规范的数据库
    • 支持非关系型数据库Redis、Mongodb
    • 支持集群部署、接口自动同步。
    • 支持分页查询以及自定义分页查询
    • 支持多数据源配置,支持在线配置数据源
    • 支持SQL缓存,以及自定义SQL缓存
    • 支持自定义JSON结果、自定义分页结果
    • 支持对接口权限配置、拦截器等功能
    • 支持运行时动态修改数据源
    • 支持Swagger接口文档生成
    • 基于magic-script脚本引擎,动态编译,无需重启,实时发布
    • 支持Linq式查询,关联、转换更简单
    • 支持数据库事务、SQL支持拼接,占位符,判断等语法
    • 支持文件上传、下载、输出图片
    • 支持脚本历史版本对比与恢复
    • 支持脚本代码自动提示、参数提示、悬浮提示、错误提示
    • 支持导入Spring中的Bean、Java中的类
    • 支持在线调试
    • 支持自定义工具类、自定义模块包、自定义类型扩展、自定义方言、自定义列名转换等自定义操作

    快速开始

    maven引入


     org.ssssssss
        magic-api-spring-boot-starter
        1.7.1

    修改application.properties
    server.port=9999
    #配置web页面入口
    magic-api.web=/magic/web
    #配置文件存储位置。当以classpath开头时,为只读模式
    magic-api.resource.location=/data/magic-api
    项目截图

    整体截图

    代码提示

    DEBUG

    参数提示

    远程推送

    历史记录

    数据源

    全局搜索

  • 这款 IDEA 插件太好用了,堪称日志管理神器!

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

    1.简介

    Grep Console是一款方便开发者对idea控制台输出日志进行个性化管理的插件。

    2.功能特性

    Grep Console的主要功能特性:

    • 支持自定义规则来过滤日志信息;
    • 支持不同级别的日志的输出样式的个性化配置;

    总结:通过过滤功能、输出日志样式配置功能,可以更方便开发者在大量的日志信息中筛选出自己比较关注的日志信息。

    3.下载

    官网下载地址:https://plugins.jetbrains.com/,在搜索输入中输入“Grep Console”,就可以到达下载页面。

    图片

    4.安装

    Grep Console的安装方式有两种,优化推荐第一种在线安装:

    4.1 在线安装

    在线安装,需要可以连接到外网,file-->settings-->plugins-->browse repositories,在查询输入框中输入“grep console”,然后点击install,最后重启idea就可以使用了。

    图片

    4.2 离线安装

    有的时候,无法连接到外网的时候,可以使用离线安装的方法进行idea插件安装 :

    a.从官网下载与自己使用idea的版本号相匹配的插件安装包;(我的idea版本是2018.2),所以我下载的grep console版本是9.6.162.000.1

    图片

    图片

    b.file-->settings-->plugins-->install plugin from disk,然后选中下载好的插件安装包进行安装,完装完成后重启idea就可以使用了;

    图片

    4.3 卸载插件

    file-->settings-->plugins,在搜索输入框中输入“grep console”,然后点击uninstall进行插件卸载;

    图片

    5.使用方法

    5.1 配置

    1、grep console的配置界面弹出有两种方法:

    第一种:file-->other settings-->grep console

    图片

    第二种,项目启动后,点击控制台上左上角的小图标,也可以弹出配置界面

    图片
    2、比较关注的配置有两处:

    第一处:input filtering,这里主要是对输入到控制台的日志进行过滤。

    • expression:配置正则表达式;
    • unless expression:和expression表达的意义相反;
    • whole line:勾选中,表示匹配整行;
    • case insensitive:表示忽略大小写
    • action:表示命中正则表达式后,要作出什么样的操作,有三种:1、移除(除非前面已有其他配置项匹配不移除),实际意思是说如果前面有其他不移除的配置项已经匹配上,这里就可以不移除,否则还是要移除匹配上的内容;2、移除(匹配上就移除);3、什么也不做;
    • continue matching:勾选中,表示下一个匹配项依然可以继续匹配当前匹配项命中的行,即多个匹配项可以同时去匹配同一行日志信息;
    • clear console:勾选中,表示清除控制台内除被当前配置项命中的其他日志信息,即只显示与配置项匹配的日志信息;
    • soud:这个就比较厉害了,命中配置项时会有声音提示,有兴趣的可以尝试一下;

    注:实际使用的时候,无论我怎么写expression表达式都没有办法完全匹配整行,遇到空格就不匹配,只能匹配到一部分,有可能是我写的expression表达式不正确,也有可能插件本身在这块有问题,有知道原因的小伙伴,可以在评论区告诉我吧,非常感谢。

    图片

    图片

    第二处:highlighting&folding

    • expression:配置正则表达式;
    • unless expression:和expression表达的意义相反;
    • whole line:勾选中,表示匹配整行;
    • case insensitive:表示忽略大小写
    • continue matching:勾选中,表示下一个匹配项依然可以继续匹配当前匹配项命中的行,即多个匹配项可以同时去匹配同一行日志信息;
    • bold:勾选中,表示命中的日志信息粗体显示;
    • italic:勾选中,表示命中的日志信息斜体显示;
    • background:设置命中日志信息的背景颜色;
    • foreground:设置命中日志信息的字体颜色;
    • statusbar count:勾选中,可以在Status Bar statistics panel中显示命中日志信息的次数(找了很久,没找到这个面板,有知道的小伙伴在评论区告诉我,多谢);
    • console count:勾选中,可以在Console statistics panel上显示命中日志信息的次数(找了很久,没找到这个面板,有知道的小伙伴在评论区告诉我,多谢);
    • fold:勾选中,可以把命中的日志信息折叠在一起(感觉这个功能没什么用);
    • sound:这个就比较厉害了,根据配置项命中日志信息时会有声音提示,有兴趣的可以尝试一下;

    5.2 实战

    下面实际演示一个这个插件怎么用,测试用的源代码:https://gitcode.net/fox9916/fanfu-web.gitgrep-console-test分支。

    1、有三个定时调度类,在被执行的时候会输出info级别日志信息;

    2、对这三个调度任务输出的日志信息进行配置,要求:匹配整行日志信息;忽略大小;EatTask调度任务输出日志信息背景色为黄色;DrinkTask调度任务输出日志信息为绿色;SportTask调用任务输出日志信息为蓝色;配置信息如下:

    图片

    3、启动项目,输出日志信息如下:

    图片

    4、在控制台输出的日志信息选中“com.fanfu.task.EatTask”,然后在选中的信息上右键弹窗中选中“Grep”,可以把包含“com.fanfu.task.EatTask”的日志信息在单独的弹窗中显示,可以通过这个功能把自己比较关注的日志信息集中显示。右键中还有另外一个功能“Add highlight”,可以把自己关注的日志信息标记为高亮,很方便在众多的日志里找到自己最想要的。

    图片

    图片

    6.总结

    这个插件还是很用的,终于可以在满屏的日志中,迅速找到自己关注的内容,调试程序的绝佳小帮手呀,以上就是这个插件的主要内容,可以根据自己的实际需要动手操作起来了,祝各位早点下班,bug绕着走,如果觉得很有用,麻烦各位关注加收藏,永远不迷路哦。

    作者:凡夫贩夫

    来源:blog.csdn.net/fox9916/article/

    details/128568466

    
    

  • 某团面试题: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产生较大的影响。

  • 灵魂一问:SELECT COUNT(*) 会造成全表扫描吗?

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


    来源:程序员大彬

    • 前言
    • SQL 选用索引的执行成本如何计算
    • 实例说明
    • 总结

    前言

    SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?

    SELECT COUNT(*) FROM SomeTable  

    网上有一种说法,针对无 where_clauseCOUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这种说法对不对呢

    针对这个疑问,我首先去生产上找了一个千万级别的表使用  EXPLAIN 来查询了一下执行计划

    EXPLAIN SELECT COUNT(*) FROM SomeTable  

    结果如下

    图片

    如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是 COUNT(1),还是 COUNT(*),MySQL 都会用成本最小 的辅助索引查询方式来计数,也就是使用 COUNT(*) 由于 MySQL 的优化已经保证了它的查询性能是最好的!随带提一句,COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用COUNT(*)查询表的行数!

    所以这种说法确实是对的。但有个前提,在 MySQL 5.6 之后的版本中才有这种优化。

    那么这个成本最小该怎么定义呢,有时候在 WHERE 中指定了多个条件,为啥最终 MySQL 执行的时候却选择了另一个索引,甚至不选索引?

    本文将会给你答案,本文将会从以下两方面来分析

    • SQL 选用索引的执行成本如何计算
    • 实例说明

    SQL 选用索引的执行成本如何计算

    就如前文所述,在有多个索引的情况下, 在查询数据前,MySQL 会选择成本最小原则来选择使用对应的索引,这里的成本主要包含两个方面。

    • IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关
    • CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。

    实例说明

    为了根据以上两个成本来算出使用索引的最终成本,我们先准备一个表(以下操作基于 MySQL 5.7.18)

    CREATE TABLE `person` (  
      `id` bigint(20) NOT NULL AUTO_INCREMENT,  
      `name` varchar(255) NOT NULL,  
      `score` int(11) NOT NULL,  
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  
      PRIMARY KEY (`id`),  
      KEY `name_score` (`name`(191),`score`),  
      KEY `create_time` (`create_time`)  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  

    这个表除了主键索引之外,还有另外两个索引, name_scorecreate_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下:

    CREATE PROCEDURE insert_person()  
    begin  
        declare c_id integer default 1;  
        while c_iddo  
        insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));  
        set c_id=c_id+1;  
        end while;  
    end  

    插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引

    EXPLAIN SELECT COUNT(*) FROM person  

    图片

    从结果上看它选择了 create_time 辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的!

    我们再来看以下 SQL 会使用哪个索引

    SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   

    图片

    用了全表扫描!理论上应该用 name_score 或者 create_time 索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用 SELECT * 造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下

    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18'   

    结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧

    -- 全表扫描执行时间: 4.0 ms  
    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   
      
    -- 使用覆盖索引执行时间: 2.0 ms  
    SELECT create_time FROM person force index(create_time) WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   

    从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。

    前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令

    SHOW TABLE STATUS LIKE 'person'  

    图片

    可以发现

    1. 行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是估算 ,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是 100264 * 0.2 = 20052.8
    2. 数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。

    也就是说全表扫描的成本是 20052.8 + 353 = 20406

    这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用 optimizer trace 功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。

    optimizer_trace 功能使用如下

    SET optimizer_trace="enabled=on";  
    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18';  
    SELECT * FROM information_schema.OPTIMIZER_TRACE;  
    SET optimizer_trace="enabled=off";  

    执行之后我们主要观察使用 name_scorecreate_time 索引及全表扫描的成本。

    先来看下使用 name_score 索引执行的的预估执行成本:

    {  
        "index""name_score",  
        "ranges": [  
          "name84059   
        ],  
        "index_dives_for_eq_ranges"true,  
        "rows": 25372,  
        "cost": 30447  
    }  

    可以看到执行成本为 30447,高于我们之前算出来的全表扫描成本:20406。所以没选择此索引执行

    注意:这里的 30447 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和。

    再来看下使用 create_time 索引执行的的预估执行成本:

    {  
        "index""create_time",  
        "ranges": [  
          "0x5ec8c516   
        ],  
        "index_dives_for_eq_ranges"true,  
        "rows": 50132,  
        "cost": 60159,  
        "cause""cost"  
    }  

    可以看到成本是 60159,远大于全表扫描成本 20406,自然也没选择此索引。

    再来看计算出的全表扫描成本:

    {  
        "considered_execution_plans": [  
          {  
            "plan_prefix": [  
            ],  
            "table""`person`",  
            "best_access_path": {  
              "considered_access_paths": [  
                {  
                  "rows_to_scan": 100264,  
                  "access_type""scan",  
                  "resulting_rows": 100264,  
                  "cost": 20406,  
                  "chosen"true  
                }  
              ]  
            },  
            "condition_filtering_pct": 100,  
            "rows_for_plan": 100264,  
            "cost_for_plan": 20406,  
            "chosen"true  
          }  
        ]  
    }  

    注意看 cost:20406,与我们之前算出来的完全一样!这个值在以上三者算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。

    实际上 optimizer trace 详细列出了覆盖索引,回表的成本统计情况,有兴趣的可以去研究一下。

    从以上分析可以看出, MySQL 选择的执行计划未必是最佳的,原因有挺多,就比如上文说的行数统计信息不准,再比如 MySQL 认为的最优跟我们认为不一样,我们可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。

    总结

    本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用  EXPLAIN, optimizer trace 来优化我们的查询语句。

  • 发誓将Notepad++拉下马,大佬推出了一款国产开源编辑器…..

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

    大家知道,Notepad++作者经常大量发表错误言论,且拒不认错,引发了广大程序员的强烈反对,很多人因此而弃用这款软件,纷纷改用其它文本编辑器。而今天推荐的这款开源文本编辑器呢,就是国内大佬写的类似Notepad++的文本编辑器—Notepad–!

    这位大佬也是性情中人,看不惯那小人嘴脸,发誓要自己开发一款超越Notepad++的国内开源文本编辑器,埋头苦干一段时间,终于在Gitee上推出了他的作品。至于名称取为Notepad–,大佬说:

    鉴于Notepad++作者的错误言论,Notepad–的意义在于:减少一点错误言论,减少一点自以为是。严正声明,台湾是中国的一部分!

    下面向各位小伙伴简单介绍一下:

    项目特点

    作为一个来自中国的、支持windows/linux/mac、绿色免费的、开源的文本编辑器,目标是要替换notepad++。该项目是采用了C++进行开发,针对NotePad++中比较受欢迎的诸多功能与特点进行了分析和吸收,逐渐在项目中加以实现。

    功能特点

    • 支持国产uos和苹果os系统

    • 文件夹对比同步

    • 文件对比同步

    • 二进制文件对比

    • 文件编码批量转换

    功能界面

    支持国产uos和苹果os系统
    文件夹对比同步
    支持文件对比及同步,编辑修改,撤销修改等
    支持文本单词高亮
    支持文件夹中批量查找
    支持文件编码批量修改
    支持皮肤切换
    支持二进制文件显示和跳转查看

    以上截图都是截取了一部分内容,如果大家有兴趣了解或者试用更详细的内容,可以去项目地址:

    https://gitee.com/cxasm/notepad–/tree/master

    最后

    Notepad–开源项目也提供全部源代码,方便大家下载研习。大家不仅可以通过使用Notepad–来体验和提升工作效率,尝试替换掉Notepad++,还可以通过对源代码学习和了解如何编写一个主流MDI文本编辑器,也许将来的最好最实用的文本编辑器就是出自你们手中哦!

    作者:cxasm

    来源:zhuanlan.zhihu.com/p/590551947

    
    

  • 一次由热部署导致的 OOM 排查经历 !

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

    来源:juejin.cn/post/7079761581546373127

    • 查看 JVM 的内存使用情况
    • 分析 MetaSpace OOM 原因
    • 快速止血
    • 代码分析
    • 总结

    开发中,将代码部署到 test 环境测试的时候,容器经常因为 OOM 重启,而相同的代部署在 prod 环境则没有问题,怀疑是因为近期 test 环境更换了热部署基础镜像包导致的(由于我们的服务只能在 test 环境进行测试,因此使用了公司的代码修改热部署插件)。于是想通过一些 JVM 工具排查下 OOM 原因,最终发现了历史代码中存在的 bug,同时新的热部署基础镜像包放大了这种影响,导致 metaspace 内存泄漏,排查过程如下:

    查看 JVM 的内存使用情况

    观察JVM的内存使用情况,主要用了 Arthasdashboard 命令和jdk自带的 jstat 工具。首先,查看 arthasdashboard 数据面板的 Memory 区域,发现 metaspace 区内存使用率一致在增加,直到超过 MaxMetaspaceSize 设置的值,发生 metaspace 的OOM。

    观察JVM的内存使用情况

    进一步,通过jdk的 jstat 工具查看gc情况,上一次gc是因为 metaspace 内存占用超过了gc 阈值,同时 metaspace 的使用率一直在90%以上,更加验证了 metaspace OOM:

    观察JVM的内存使用情况

    分析 MetaSpace OOM 原因

    JDK8 之后,metaspace 对应于java运行时数据区的 方法区 ,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,采用本地内存(Native Memory)来实现 方法区 ,本身没有大小限制(受物理内存大小限制),但可以使用 -XX:MaxMetaspaceSize 参数设置上限,我们这边设置的是2G。

    既然是因为类的元数据信息撑爆了元空间,那就看一下类的装载和卸载情况吧,使用 jstat 工具看下类加载情况,并对比 testprod 两个环境的差异:

    「test」

    分析 MetaSpace OOM 原因

    「prod」

    分析 MetaSpace OOM 原因

    prod 环境的服务加载了很多类也卸载了很多类(这其实是个线上问题,通过这次排查才发现的,后面分析原因),而 test 更加严重,加载了很多类,但几乎没有卸载过类。

    重点看下 test 服务的类加载情况,使用 Arthasclassloader 命令:

    分析 MetaSpace OOM 原因

    第一眼就觉得 AviatorClassLoader 比较可疑,其他加载器都是spring和jdk的,这个是谷歌的,同时这个类加载器的实例数量和加载的类的数量非常大,同时随着服务的运行在不断的增长。仅仅是类加载器的实例数量大倒还好,毕竟它的 Class 对象就一份,不会撑爆元空间,但它加载的 Class 会有问题,因为判断是不是同一个类,是由 「加载它的类加载器+全限定类名」 一起决定,于是乎,可以从这个类加载器入手,代码中全局搜索一下,果然找到了引入点。

    快速止血

    我们的项目是接手的一个报表类的服务,项目中用到了一个表达式计算引擎 AviatorEvaluator ,用来根据表达式字符串计算复合指标,其伪代码如下表示:

    public static synchronized Object process(Map eleMap, String expression) {
        // 表达式引擎实例
        AviatorEvaluatorInstance instance = AviatorEvaluator.newInstance();
        // 生产表达式对象
        Expression compiledExp = instance.compile(expression, true);
        // 计算表达式结果
        return compiledExp.execute(eleMap);
    }

    其中 expression 是表达式字符串,eleMap 是表达式中变量名和具体值的键值对,比如 expression 对应 a+b ,eleMap对应的键值对为 {"a":10, "b":20} ,则process方法的结果为30

    之前从未仔细看过这段代码,现在看下来似乎有点不太对劲:synchronizednewInstance 是重复了吗?synchronized 是用来做同步的,说明这段代码中有共享资源竞争的情况,应该就是 AviatorEvaluator 实例了,目前的逻辑每次执行 process 都会实例化一个 AviatorEvaluator 对象,这已经不仅仅是线程私有了,而是每一个线程每次调用这个方法都会实例化一个对象,已经属于 「线程封闭」 的场景了,不需要用 synchronized 关键字做同步。我们的业务场景对这个方法的调用量非常大,每个指标的计算都会调用这个方法。至此,有以下结论:

    1. synchronized 同步和线程封闭每个线程私有一个对象二者选其一就行;
    2. 如果 AviatorEvaluator 是线程安全的话,使用单例模式就行,可以减轻堆区的内存压力;

    谷歌出品的工具类不是线程安全的?不会吧?

    查阅资料得知,AviatorEvaluatorexecute 方法是线程安全的,代码里的使用姿势不对,修改代码如下,重新发布,不再OOM了。

    // 删除 synchronized
    public static Object process(Map eleMap, String expression) {
       AviatorEvaluator.execute(expression, eleMap, true); // true 表示使用缓存
    }

    代码分析

    但是,还是有两点难以解释:a) 为什么是 metaspace ?b) 为什么使用热部署镜像的 test 环境出现 OOM,而 prod 没有出现 OOM?

    如果是因为 AviatorEvaluator 对象太多导致的,那也应该是堆区 OOM;同时,prod 环境的请求量远大于 test 环境,如果是像目前 test 这种 metaspace 的膨胀速度,线上肯定也是会 OOM 的,差异在于 test 用的热部署的基础镜像包。

    「首先探寻第一个问题的答案,为什么是 metaspace ?」

    热部署?ClassLoader? 方法区?此时,脑海里回想起一道经典八股文:动态代理的两种方式?(JDK动态代理和CGLIB动态代理的区别?AOP的实现原理?运行期、类加载期的字节码增强?)

    感觉这次 OOM 很大可能是 ** 「ClassLoader」 和 ** 「热部署(字节码增强)」** 撞出的火花 …**

    通过阅读 AviatorEvaluator 的源码,发现调用链简化后大概是这样:

    public Object execute(final String expression, final Map env, final boolean cached) {
      Expression compiledExpression = compile(expression, expression, cached); // 编译生成 Expression 对象
      return compiledExpression.execute(env);  // 执行表达式,输出结果
    }
    private Expression compile(final String cacheKey, final String exp, final String source, final boolean cached) {
     return innerCompile(expression, sourceFile, cached);  // 编译生成 Expression 对象
    }
    private Expression innerCompile(final String expression, final String sourceFile, final boolean cached) {
      ExpressionLexer lexer = new ExpressionLexer(this, expression);
      CodeGenerator codeGenerator = newCodeGenerator(sourceFile, cached); //!!这个方法 new AviatorClassLoader 的实例
      return new ExpressionParser(this, lexer, codeGenerator).parse(); 
    }
      public CodeGenerator newCodeGenerator(final String sourceFile, final boolean cached) {
        // 每个 AviatorEvaluatorInstance 一个 AviatorClassLoader 的实例作为成员变量
        AviatorClassLoader classLoader = this.aviatorClassLoader;
        //!!这个方法通过上面的类加载器不断生成并加载新的Class对象
        return newCodeGenerator(classLoader, sourceFile);
      }
    public CodeGenerator newCodeGenerator(final AviatorClassLoader classLoader, final String sourceFile) {
     ASMCodeGenerator asmCodeGenerator = 
        // 使用字节码工具ASM生成内部类
        new ASMCodeGenerator(this, sourceFile, classLoader, this.traceOutputStream);
    }
    public ASMCodeGenerator(final AviatorEvaluatorInstance instance, final String sourceFile,
        final AviatorClassLoader classLoader, final OutputStream traceOut) {
      // 生成了唯一的内部类
      this.className = "Script_" + System.currentTimeMillis() + "_" + CLASS_COUNTER.getAndIncrement();
    }

    这时候原因差不多清晰了,使用 AviatorEvaluatorInstance 对象计算表达式会使用成员变量里的一个 AviatorClassLoader 加载自定义的字节码生成 CodeGenerator 对象。AviatorEvaluatorInstance 用单例模式使用固然没什么问题,但是如果每次都 new 一个AviatorEvaluatorInstance 对象的话,就会有成百上千的 AviatorClassLoader 对象,这也解释了上面通过 Arthas 查看 classloader 会有这么多对应的实例,但 metaspceClass 还是只有一份的,问题不大。同时看到生成的字节码对象都是 Script_ 开头的,使用 arthassc 命令看下满足条件的类(数量非常多,只截取了部分),果然找到了 OOM 的元凶:

    OOM 的元凶

    「第二个疑问:为什么 prod 没有OOM?」

    上面通过 jstat 发现 prod 卸载了很多类,而 test 几乎不卸载类,两个环境唯一的区别是 test 用了热部署的基础镜像。

    咨询了负责热部署的同事,了解到热部署 agent 会对 classloader 有一些 强引用,监听 classloader 的加载的一些类来监听热更新,这会导致内存泄漏的问题。同时得到反馈,热部署后面会用 弱引用 优化。这里引用下《深入理解java虚拟机》中的解释:

    弱引用优化

    因为大量的 AviatorEvaluatorInstance 创建了大量的 AviatorClassLoader ,并被热部署 agent 强引用了,得不到回收,那么这些类加载器加载的 Script_* 的Class对象也得不到卸载,直到 metaspace OOM。

    通过 JVM 参数 -Xlog:class+load=info-Xlog:class+unload=info 看下 prod 环境的类加载和类卸载日志,确实有大量 Script_* 类的加载和卸载:

    类的加载和卸载

    类的加载和卸载

    test 环境这没有此种类的卸载,这边就不再贴图了。

    此刻,我比较好奇的是热部署包里的什么类型的对象强引用了我们的自定义类加载器?使用 jprofiler 看下两个环境的堆转储文件,不看不知道,一看吓一跳,问题比想象的更加严重:

    「prod」

    堆转储文件

    「test」

    堆转储文件

    对比 prodtest 环境的引用情况,test 环境存在热更新类 WatchHandler 对象到 AviatorClassLoader 的强引用,而 prod 环境不存在;进一步,选择一个具体的 AviatorClassLoader 实例看下引用情况,又有了重大发现:

    「prod」

    AviatorClassLoader

    「test」

    AviatorClassLoader

    prod 环境的 AviatorClassLoader 除了加载我们业务需要的自定义类 Script_*,还加载了很多热更新相关的类,同时不同 AviatorClassLoader 实例加载的热更新相关的类的 hashcode 也是不同了,说明每个 AviatorClassLoader 实例都加载了一轮,这才是元空间占用内存的大头。

    当然,在正确使用 AviatorEvaluator 的情况下(使用单例模式),就不会出现这么严重的问题了,但依然存在热部署 agent 对自定义 classloader 的强引用问题。

    总结

    这是我第一次系统地排查 OOM 问题,把之前一些零散模糊的知识点给串起来的,下面总结了一些本次排查涉及到的 JVM 基础概念和工具:

    「元空间:」

    • https://segmentfault.com/a/1190000012577387

    「类加载器:」

    • https://segmentfault.com/a/1190000037574626
    • https://segmentfault.com/a/1190000023666707

    「JDK工具:」

    • jps:JVM Process Status Tool 进程状况工具
    • jstat:JVM Statistics M onitoring Tool 统计信息监视工具
    • jinfo:Configuration Info for Java Java配置信息工具
    • jmap:Memory Map for Java 内存映像工具
    • jhat:JVM Heap Analysis Tool 堆转储快照分析工具
    • jstack:Stack Trace for Java 堆栈跟踪工具
    • Jcmd:多功能诊断命令行工具
    
    

  • 针对 jar 和 vue 的一键自动化部署工具,界面友好,操作简单(已开源)

    前言

    easy-jenkins是一款对vue和jar的部署工具,操作简单,实行一键部署,内部结构采用流水线形式架构,每次部署,时时提供部署过程,部署记录,界面友好简洁,使用方便,符合用户常规操作

    easy-jenkins面向分支形式,无需登录,默认分支为jenkins,每个分支可以配置多个数据源,切换不同分支可以管理不同数据源

    easy-jenkins采用本地存储的结构无需配置数据库,简单易上手

    提示:以下是本篇文章正文内容,下面案例可供参考

    一、项目地址

    开源地址:

    • https://gitee.com/susantyp/easy-jenkins

    二、使用步骤

    先把代码拉入你的本地

    1.项目结构

    2.启动主类 EasyJenkinsApplication

    3.安装

    启动后弹出当前窗体 点击下一步

    来到这里后,填写相应的信息

    • 安装路径
    • maven路径 打包需要
    • 以及项目端口的启动,避免不要和本地端口冲突,我们可以设置 8332 8899 9900 等端口

    点击安装并启动, 点击确认 等待几秒,项目自动启动

    4.项目启动图

    三、功能点介绍

    • 部署列表
    • 部署记录
    • 数据分支
    • 基本设置

    1.部署列表

    部署列表主要显示我们的连接信息

    1.添加连接(部署jar)

    我们点击按钮,添加连接

    添加本地项目地址

    添加本地项目地址后,它下面的文本款会根据本地项目地址自动生成,如图

    在这边需要确保:

    • 你的本地项目地址是正确的
    • jar名称正确的
    • pom.xml文件是正确的

    我部署项目的端口为8080

    根据你自己的项目设置端口

    添加服务器相关信息

    在我们的右边填写我们的服务器信息

    • 服务器ip
    • 账号
    • 密码
    • 端口

    上传的位置 后面不需要带 /

    上传的位置 默认生成一个命令

    如果上传位置是 /home/springboot

    则生成如下

    nohup java -jar /home/springboot/wall.jar & tailf /home/springboot/nohup.out

    可以修改为你自己需要运行的命令

    或者直接使用当前命令

    2.部署jar

    我们点击部署按钮即可

    部署过程效果图

    后台会实时返回部署的消息,返回给前端显示

    3.部署成功

    部署成功返回:Successfully deployed

    4.删除

    点击table 直接删除

    5.编辑

    编辑小伙伴可以自己玩一下

    2.部署记录

    部署记录主要记录了,最近部署的情况和统计信息

    3.数据分支

    easy-jenkins 是面向分支的

    不同分支存储不同的连接,默认分支为jenkins

    1.创建分支

    创建一个root的分支

    2.切换分支

    切换完成后,可以查看当前分支的状态

    当前我们就是root分支的环境下

    我们点击部署记录

    部署记录此时为空的,刚刚创建分支下面是没有连接数据的,需要重新添加连接

    我们切换为jenkins分支后,前面我们在jenkins添加了一条连接数据,下面就显示数据了,同时上面会标注当前的环境为jenkins

    注意了,正在使用的分支是不可以删除

    4.基本设置

    • 安装路径
    • maven路径
    • 项目端口号

    当前这三个值,是我们最初刚刚开始安装的时候的需要录入的值,我们可以点击编辑操作

    5.启动

    • 第一次启动会启动安装向导程序
    • 第二次启动直接启动浏览器,则不再启动安装向导程序

    6.如何部署vue

    dist 是vue项目默认build的位置

    同样 后面 不需要 ‘/’

    需要注意

    上传位置名字保持跟本地相同的名字,如图:

    然后填写你相应的服务器信息即可

    7.exe启动项目

    在我们exe文件夹下面,有一个easy-jenkins.exe文件

    可以将他拷贝到桌面,直接点击它运行即可,不需要每次启动springboot程序

    总结

    此部署工具主要针对于个人本地的部署

    针对于小型项目的部署,轻量级的,一键部署,操作简单

    作者:来自上海的这位朋友

    来源:blog.csdn.net/Susan003/article/

    details/128223343

  • Springboot代码混淆,别再让代码在线上进行裸奔

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

    来源:blog.csdn.net/qq_35387940/article/details/127426354

    • 编译
    • 反编译
    • 混淆
    • 正文

    编译

    简单就是把代码跑一哈,然后我们的代码 .java文件 就被编译成了 .class 文件

    图片

    反编译

    就是针对编译生成的 jar/war 包 里面的 .class 文件 逆向还原回来,可以看到你的代码写的啥。

    比较常用的反编译工具 JD-GUI ,直接把编译好的jar丢进去,大部分都能反编译看到源码:

    图片

    那如果不想给别人反编译看自己写的代码呢?

    怎么做?

    混淆

    该篇玩的代码混淆 ,是其中一种手段。

    我给你看,但你反编译看到的不是真正的代码。

    先看一张效果示例图 :

    图片

    开搞

    正文

    先看一下我们混淆一个项目代码,要做啥?

    图片

    一共就两步

    第一步, 在项目路径下,新增一份文件 proguard.cfg :

    proguard.cfg

    #指定Java的版本
    -target 1.8
    #proguard会对代码进行优化压缩,他会删除从未使用的类或者类成员变量等
    -dontshrink
    #是否关闭字节码级别的优化,如果不开启则设置如下配置
    -dontoptimize
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    # 对于类成员的命名的混淆采取唯一策略
    -useuniqueclassmembernames
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    #混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代
    -adaptclassstrings
     
    #对异常、注解信息予以保留
    -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
    # 此选项将保存接口中的所有原始名称(不混淆)-->
    -keepnames interface ** { *; }
    # 此选项将保存所有软件包中的所有原始接口文件(不进行混淆)
    #-keep interface * extends * { *; }
    #保留参数名,因为控制器,或者Mybatis等接口的参数如果混淆会导致无法接受参数,xml文件找不到参数
    -keepparameternames
    # 保留枚举成员及方法
    -keepclassmembers enum * { *; }
    # 不混淆所有类,保存原始定义的注释-
    -keepclassmembers class * {
                            @org.springframework.context.annotation.Bean *;
                            @org.springframework.beans.factory.annotation.Autowired *;
                            @org.springframework.beans.factory.annotation.Value *;
                            @org.springframework.stereotype.Service *;
                            @org.springframework.stereotype.Component *;
                            }
     
    #忽略warn消息
    -ignorewarnings
    #忽略note消息
    -dontnote
    #打印配置信息
    -printconfiguration
    -keep public class com.example.myproguarddemo.MyproguarddemoApplication {
            public static void main(java.lang.String[]);
        }

    注意点:

    图片

    其余的看注释,可以配置哪些类不参与混淆,哪些枚举保留,哪些方法名不混淆等等。

    第二步,在pom文件上 加入proguard 混淆插件 :

    build标签里面改动加入一下配置


        
            
                com.github.wvengen
                proguard-maven-plugin
                2.6.0
                
                    
                    
                        package
                        
                            proguard
                        

                    

                

                
                    
                    ${project.build.finalName}.jar
                    
                    ${project.build.finalName}.jar
                    
                    true
                    
                    ${project.basedir}/proguard.cfg
                    
                    
                        ${java.home}/lib/rt.jar
                        ${java.home}/lib/jce.jar
                        ${java.home}/lib/jsse.jar
                    

                    
                    !META-INF/**,!META-INF/versions/9/**.class
                    
                    ${project.basedir}/target
                    
                    
                        
                    

                

            

            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            repackage
                        

                        
                            com.example.myproguarddemo.MyproguarddemoApplication
                        

                    

                

            

        


    注意点:

    图片

    图片

    然后可以看到:

    图片

    然后点击package,正常执行编译打包流程就可以 :

    图片

    然后可以看到jar的生成:

    图片

    看看效果:

    图片

    好了,该篇就到这。

    
    

  • 阿里终面:每天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)。

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

    
    

  • 切记,任何时候都不要在 for 循环中删除 List 集合元素!!!

    前言

    首先说结论:无论什么场景,都不要对List使用for循环的同时,删除List集合元素,因为这么做就是不对的。

    阿里开发手册也明确说明禁止使用foreach删除、增加List元素。

    正确删除元素的方式是使用迭代器(Iterator),代码如下:

    List list = new ArrayList();
    Iterator iterator = list.iterator();
    while (iterator.hasNext()) {
        // 删除元素
        iterator.remove();
    }

    JDK8后lambda写法:list.removeIf(s -> s.contains("a"));

    不想知道为什么不能使用for循环删除List集合元素的,看完前言就可以关闭本页面了,想知道原因的继续往下看

    实例

    下面举个实例场景,看一下为什么不能使用for循环。

    需求

    一个List集合,元素类型为String,有N个元素,删除这些元素中包含字符”a”的元素。

    假设集合内容如下:

    List list = new ArrayList(4);
    list.add("a");
    list.add("ab");
    list.add("abc");
    list.add("abcd");

    正确答案

    先上正确答案

    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            if (iterator.next().contains("a")) {
                // 删除元素
                iterator.remove();
            }
        }
        System.out.println(list);
    }

    输出结果为

    []
    错误答案1:普通for循环(for-i)
    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        for (int i = 0; i         if (list.get(i).contains("a")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }

    输出结果为

    [ab, abcd]

    分析

    普通for循环遍历List集合的同时,删除List中的元素是可以运行的代码,但在大多数场景下,不能使用这种方式,上边的结果也印证了这一点,虽然你的代码不会报错,运行也正常,但在本实例中,这么写就是BUG。

    BUG原因:索引为i的元素删除后,后边元素的索引自动向前补位,即原来索引为i+1的元素,变为了索引为i的元素,但是下一次循环取的索引是i+1,此时你以为取到的是原来索引为i+1的元素,其实取到是原来索引为i+2的元素。

    如下图示例:

    看图可以发现,只要每删除一个元素,就会漏掉下一个元素,所以这种方式从逻辑上来说是存在bug的,无论什么需求场景,都不建议用这种方式,因为不可控因素太多(鬼知道生产环境中他会删掉多少元素,同时漏掉多少元素)。

    既然这么写不报错,那么个别特殊场景确实可以使用这种普通for循环删除元素的,比如我们把实例的需求变动一下,改为:一个List集合,元素类型为String,有N个元素,删除这些元素中包含字符’a’的元素,如果有连续两个或以上元素包含’a’,那么只删除当前连续元素中的奇数位元素。虽然这种场景适用,但仍然不推荐,还是因为太不可控。

    错误答案2:增强for循环(foreach)
    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        for (String str : list) {
            if (str.contains("a")) {
                list.remove(str);
            }
        }
        System.out.println(list);
    }

    运行报错:

    Exception in thread "main" java.util.ConcurrentModificationException
     at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
     at java.util.ArrayList$Itr.next(ArrayList.java:859)
     at top.oldmoon.learn.test.ListTest.main(ListTest.java:24)

    使用百度翻译可以知道:Concurrent Modification Exception:并发修改异常

    分析

    其实这里没啥好分析的,直接报错了,你还这么写干嘛?没事找罪受吗。。。

    可以简单的理解为:foreach就不支持对集合中的元素进行增删操作,但是可以修改。

    作者:DingDangDog

    来源:https://oldmoon.top/post/12