JVM唠嗑(一)—— 堆栈 内存溢出 OOM 堆溢出 dump visualVM

前言——关于JVM调优的看法

JVM调优,听起来是个玄幻高深的技术,java程序员里面你说自己会jvm调优,就感觉,同是九年义务教育,为什么你这么秀:)所以有些人似乎特别急切学JVM调优,希望看那些大部头的书来“精通它”,

但实际上,经过一系列调查,实际用JVM调优,需要有以下前提:

  • 要求很高 最大限度发挥硬件效能
  • 极端的业务环境,比如大数据计算引擎 要求堆区足够大,吞吐量足够高等
  • 特殊业务场景出现性能问题 查漏补缺找BUG
  • 代码本身质量本身要ok

有些人代码习惯不好,比如构造了很多用完即扔的对象,比如嵌套着循环调接口,查数据库,造成不必要的性能过度浪费,又盲目去调优补救。其实如果通过观察jvm相关信息,找到bug所在,那我们学jvm目的也达到了,但是如果只是跟着网上教程调调堆大小,换换GC算法,然后一个个试,只知其一不知其二,那就是舍本逐末了,最后心态崩溃,认为不是我错了,是世界错了(bushi)

所以我认为,不能为了调优而调优,一方面,努力提升自己代码质量,另一方面,对这方面学习不能心急,一点一点学习,万丈高楼平地起,然后工作中特殊业务场景出现问题,可以学以致用,利用JVM监控信息去debug,一点点积累调参经验,慢慢的就会了。

聊回本笔记,本系列的笔记主要是科普性质,说实话也不够系统,我们可能从操作系统聊到JVM基础,可能看看JVM的内存模型,也可能聊聊常见的OOM问题,然后看看所谓调优工具是什么样子的。没吃过猪肉,得见过猪跑,我们遇到问题后有个大致的思路,能够看得懂大佬在说什么,能面试造火箭航母的时候问到jvm不懵逼,我觉得足矣,毕竟一上来给自己太大的心理负担——我要精通JVM调优,我觉得学习效果不一定好

JVM JRE JDK

名词解释一下 省的一开始就劝退

缩略语全程中文
JDKJAVA Development KitJAVA开发工具包
JREJAVA Runtime EnvironmentJAVA运行环境
JVMJAVA Virtual MachineJAVA虚拟机

这张java se的架构图 稍微看看就好
在这里插入图片描述

另外,经常听大佬说什么Hotspot VM板块划分HotSpot 虚拟机 啥玩意儿啊?

其实JVM 种类有很多,比如 Oralce-Sun,也就是Java官方的 Hotspot虚拟机,当然别的虚拟机也有,算是想在前人肩膀上改进?,比如 Oralce JRockit, IBM J9, Taobao JVM(真的是中国那个淘宝)等等。当然,Hotspot主流,必学。

堆 栈

面向对象,我们操作的对象实例 大部分空间 存储的位置就在堆Heap里面

而我们栈Stack,因为其先入后出的特性,特别适合存方法的调用的相关上下文context

问题一 啥是上下文,你可以简单理解,比如方法里面的局部变量,存状态,存标志位,存数据的那些信息,只要方法不执行结束(return),他这些上下文信息必须存在栈里面,否则凉凉,

为啥?举个不那么恰当的例子,假设这个方法A执行途中调用别的方法B,比方说循环调用,

int i=LEN;
for(; i>=0; i--) {
	B();
}

B是需要A存储的变量i的,否则执行多少次他自己都不知道。。所以只要A不执行结束则其上下文必须存着,存在栈里。

问题二 为什么方法context适合用栈存

我们再看看经典的递归
为什么?你想,递归意思,方法自我调用,一层一层的调用自己,每次调用都要用栈存这个方法相关的context,那么最终,当最后一次被调用的方法开始执行,我们知道,这个方法的context是存在栈顶的,他是最后进栈的(context最后存到栈里边的),这时栈的深度(存进去的context数量)是最大的,

好,最后进栈的那个方法,他执行完了,要return返回了,那么,返回以后我们自然不需要存它的context了——他执行完已经没用了,这时发生出栈(弹栈 pop)操作,换言之,最后进栈的最先出栈

所以这个栈结构,因为他后入先出,先入后出,所以特别适合存方法调用,我们可能不递归,但是一定会有方法调用方法的情况,这时也同理。

接下来先不讲深入的、例外的知识,我们就用IDEA简单做个实验——想办法堆栈溢出

堆溢出

只要我们JVM,他作为一个应用程序,向OS申请的内存够大,我们OS就能内存爆炸:)
同样的,只要我们的变量够大,还不让JVM内存回收,JVM就必须腾位置给我们,我们就能让堆爆炸,艺术就是爆炸~ 诶,玩,就是玩

爆炸,也就是溢出,堆溢出(HeapOverflow)

之前我们说不要盲目改JVM配置参数,那有哪些配置参数?
这里我们学两个JVM:Xms Xmx

Xms 是指设定JVM堆启动时占用内存大小,JVM应用程序,在启动就会直接分配的,是JVM实打实占用的内存大小,如果大于电脑内存,会造成内存溢出(存不下就爆炸)

Xmx 是指设定程序运行期间 最大可占用的内存大小,这只是个上限,有保护OS的意味,如果用不了那么多,实际上OS并不会给JVM分配这么大的内存,因为没必要。
如果如果程序实际运行需要占用的内存,超出了这个设置值,就会抛出OutOfMemory异常。

其实Xms Xmx有全称:-XX:InitialHeapSize -XX:MaxHeapSize,但是太长了我们就用简写吧

另外,默认配置一般是,初始Xms=物理内存/64,Xmx=物理内存/4(如服务器内存是32G,即JVM最大内存可为8G)

好了,那既然要实现堆溢出,我们怎么设置JVM配置参数呢?如下:

-Xms10m -Xmx10m -Xlog:gc*

其中-Xlog:gc*就是用来打印堆使用情况的,-XX:+PrintGCDetails已经废弃了 别用了

好的
那我们写个这样的代码:

public class HeapOverFlow {
    public static void main(String[] args) {
        List<Object> listObj = new ArrayList<Object>();
        for(int i=1; i<=10; i++){
            // 每次注入1M
            Byte[] bytes = new Byte[1024 * 1024];
            listObj.add(bytes);
            System.out.println("向堆内注入"+i+"M");
        }
        System.out.println("堆被注满了QAQ");
    }
}

然后他会打印出一大堆我们看不懂的东西,如图
在这里插入图片描述
这里我们先知道之基本的信息

  • heap回收(GC)的最小单位region是 1M
  • GC回收使用的算法是G1

好的其他我们先不管,看末尾打印:
在这里插入图片描述
很明显,确实我们实现了内存溢出(OOM),准确的说是堆溢出(java.lang.OutOfMemoryError: Java heap space)
我们之前说的应验了
在这里插入图片描述
另外,在这行也说明了 我们heap大小为 10240K=10M
在这里插入图片描述
这里的“garbage-first”就是指“G1”

另外,我们注意到其实heap没有被用满就报了OutOfMemory(OOM),为啥?实际上它每次申请内存,如果计算出,申请后已经超限,超Xmx设定的限制,就会报错了 反正分配了内存后也必炸

问题一
我开始预想的是,一个Byte元素占一个Byte(8位 8b),因此每次注入1024*1024B 也就是1MB 那么我们分配了10MB 按理来说能注入10次,实际上注入一次就发生了OOM,Why?

我认为有以下几个原因:

  • 首先就是 实际上Byte数组在内存(或者说堆)的存储结构并非直白的就放在里边,实际的结构会更加复杂,层层封箱(后面会讲)会导致占用的空间比实际要存的数据要
  • 堆空间不是铁板一块,而是具有分区的,新生代分区(young,我们后面会细讲)存储新鲜出炉new出来的对象实例,之后很快被GC,而剩下没被GC的转移到survivor区,后面还会转到old区。。。总之是区与区之间有复制,转移的过程,因此不可能直接占满整个堆
  • G1这个GC算法的设计细节,可能它很有前瞻性 已经算出来即将发生OOM 于是提前终止,省的麻烦OS做真正的内存分配

问题二
这里确实能看到heap宏观的一些信息,但是实际上我们如果想要排错debug,我们还是不知道是哪方面因素导致了OOM,这时dump文件派上用场。

dump visualVM 工具基本使用

我们在之前JVM配置参数的宏的基础上加点东西:

-Xms10m -Xmx10m -Xlog:gc* -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/dumps/

-XX:+HeapDumpOnOutOfMemoryError意思,发生OOM就生成dump文件 这相当于一个触发器
-XX:HeapDumpPath=E:/dumps/设置dump文件放置的位置

dump文件类似一个快照,保留事故现场,方便我们时候推测当时的原因。

分析dump文件的工具有很多,我还是喜欢可视化的visualVM

咋弄呢?官网先下载,http://visualvm.github.io/
在这里插入图片描述
然后我们使用IDEA插件与之构建关联
在setting plugin里边 像vscode一样 在市场marketPlace搜索 安装即可
在这里插入图片描述
第一次启动会让你配置一下visual VM的位置,实际上你可以专门弄个文件夹放这些工具 比如maven tomcat等,如图
在这里插入图片描述
然后这里就是刚刚官网下载的文件
在这里插入图片描述
用这个目录配置即可

然后打开visualVM File->载入Load 我们的生成的dump文件:
在这里插入图片描述
然后生成分析报告 如下
在这里插入图片描述
Heap那边显示了 当时有多少类classes 有多少对象实例在里边 有多少classloader(我们后边会讲到),还有总大小Size,可以看到实际堆用到的空间

问题一
既然Xms都定死了是10MB为啥这里的Heap Size是127227659B=12.1MB>10MB?
为什么偏大?这里显示的heap size应该是实际用到的大小 我们说理想与现实有差距,你JVM配置的堆大小,只是申请,请求OS来真正执行这个内存分配,但还是可能存在偏差,差了一点可以接受,
这样也说明了为啥Xms你不能设计,刚刚好为实际内存,否则计算机要不直接内存溢出(因为实际占用内存偏大),要不,可能依靠虚拟内存,当然这会更加消耗CPU,导致运行变慢。

问题二
怎么找到罪魁祸首?、
其实很简单,我们看下面几个栏目,找百分比最大的那个,一般超过30%就很值得注意了
比如,在 Size of instances
在这里插入图片描述
66%接近 这就很明显有问题,

当然另外一个栏目 instance by sizes也同样,这两个其实差不多,一般看前者
在这里插入图片描述
我们来详细看看classes by size of instances
在这里插入图片描述
这里有1048576 = 1024*1024 B 的Byte实例 这与之前我们观察到的,只是注满了一次的结果吻合
在这里插入图片描述
而且这些Byte实例都是NULL 也很符合我们代码,毕竟都是new出来的还没初始化。

到此,我们用visualVM找罪魁祸首的实验算是成功了。

后记

下一篇会做栈溢出相关的实验

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值