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

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

缩略语

全程

中文

JDK

JAVA Development Kit

JAVA开发工具包

JRE

JAVA Runtime Environment

JAVA运行环境

JVM

JAVA Virtual Machine

JAVA虚拟机

这张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找罪魁祸首的实验算是成功了。

后记

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值