JVM原理和调优(读这一篇就够了)

前言

 

抛2个问题:

1、export JAVA_OPTS="-Xms256m -Xmx512m -Xss256m -XX:PermSize=512m -XX:MaxPermSize=1024m  -Djava.rmi.server.hostname=136.64.45.24 -Dcom.sun.management.jmxremote.port=9315 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

---上面的"-Xms256m -Xmx512m -Xss256m -XX:PermSize=512m -XX:MaxPermSize=1024m“分别是什么意思

 

2、为什么调整上面这几个参数就能让java程序不卡顿,或者不报错oom

 

3、java.lang.OutOfMemoryError: Java heap space和java.lang.StackOverflowError是否熟悉,分别表示了什么错误,背后出现了什么问题

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

一、JVM内存模型

JVM原理和调优(读这一篇就够了)_第1张图片

  • 堆:所有new对象都放在堆里,这也是JVM优化的重点。

  • 栈:也叫线程栈,是每个线程独立分配的内存空间,存储的包括局部变量,操作数,动态链接,方法出口。它和堆的关系其实是堆的指针引用。

  • 本地方法栈:为虚拟机用到的Native方法服务。Native方法是java通过JNI直接调用本地C/C++库,可以认为是暴露给java的本地接口。

  • 方法区(元空间):类装载系统会把class信息放到方法区,通过字节码引擎执行。会放常量(运行时常量池,那么什么是静态常量池),静态变量,类元信息。jdk8以后就叫元空间。

  • 程序计数器:记录执行的代码行数的行号,比如线程暂停回来后,继续执行,就需要读取行号,继续向下执行

(一)栈的核心组成部分

JVM原理和调优(读这一篇就够了)_第2张图片

  • 局部变量表: 用于存放方法中的局部变量和参数

  • 操作数栈: 用于存放方法执行过程中产生的中间结果

  • 动态链接:方法区里面的符号引用转为直接引用(即:给出地址)

  • 方法出口:记录当前执行方法执行完毕后返回主方法的下一行位置

 

(二)堆

JVM原理和调优(读这一篇就够了)_第3张图片

所有的new对象最开始都放到Eden(伊甸)区,当Eden区沾满之后,JVM会进行一次minor gc,将发现依然存活的对象放到Survivor区,同时讲分代年龄+1(存储在object header里面),会清理eden和form区域,通过复制等算法再次将存活对象放到to区域,依次类推来回复制,当分代年龄达到15时,会挪到老年代,比如静态变量,数据库连接池,缓存就属于“老不死”。当老年代内存被占满时,就会触发full gc,full gc会对整个堆进行垃圾回收,因此非常耗时,时间很长,我们要尽量降低full gc的频次。

题外话:minor gc如何进行垃圾回收呢,这里面运用到一个可达性分析算法,里面有一个GC roots根的概念很关键,要理解一下,gc会根据该算法找出该对象的整条GC root链,定义为有效存活对象,会移动到survivor区域;剩下的对象由于找不到对象引用,因此定义为无效对象,从而进行回收。具体如下:

JVM原理和调优(读这一篇就够了)_第4张图片

 

(为什么年轻代使用复制算法,老年代使用标记-清除算法?)

 

还有一种情况,只要伊甸区移动的内存大于存活区的1/2时,会直接放到老年代。

 

当old区内存满了后,就会触发full gc,但是发现引用还在(list还是运行),因此没法回收,但是死循环又一直在new对象,old区无内存可用,于是就OOM。

在full gc的时候针对整个heap进行寻找垃圾对象,因此会停顿用户线程(STW)。为什么要停止呢,如果不停止的话,意味着回收线程和用户线程同步执行,回收线程会认为用户线程的内存不是垃圾对象,因此不予理会,但如果用户线程很快执行完,它使用的对象就变成垃圾对象了,这时候回收线程不知道该如何处理,因此为了更好的控制内存,full gc的时候,必须暂停用户线程,回收完成后,再恢复用户线程。因此问题就出现了,如果频繁的full gc,而且收集时间非常长(因为针对整个堆内存回收)就会频繁的停止用户线程,导致系统功能暂时不可用,也就是卡顿现象或无响应。因此优化的点就一目了然: 减少full gc次数,让年轻代去收集释放垃圾对象。

JVM原理和调优(读这一篇就够了)_第5张图片

 

二、JVM调优

为什么调优其实上面已经提前说了,所以我们调优的目的是什么呢?就是让系统更加的丝滑,让用户体验变得更好。

以下是一个亿级流量网站的一个并发计算过程demo图:

JVM原理和调优(读这一篇就够了)_第6张图片

JVM原理和调优(读这一篇就够了)_第7张图片

以下是关键数据计算:

  • 日活:1E/20 = 500W。要估算每个人一天的日点击数。

  • 日均订单:50W。这里要找业务确定转化率。

  • 大促并发数:50W/50s = 1000单左右。这里要估算并发时间,这里拍脑袋是1分钟内。

  • 单机并发数:1000/n。n表示是机器节点。

  • 每单请求的数据量:1kb * 1000/n。1kb是单个请求的data大小,这个研发可以根据请求data json算出来。

  • 单机的内存使用:300kb * 20 * 10 = 60M。300kb是n=3,20是下单逻辑中会有加减库存,优惠券,积分系统接口,10是订单查询等其他外部操作。

初始化jvm配置如下:

 

对应的jvm内存模型是:

JVM原理和调优(读这一篇就够了)_第8张图片

 

  1. 每一秒产生60M对象,均放到eden区

  2. 到第13秒时,eden区内存即将占满(60*13 =780M),触发minor gc

  3. 这时候会对前12秒的对象进行回收,第13秒产生的对象由于还在执行,因此判断为有效对象,会被移动到S0区

  4. 这时候会触发jvm另外一个机制--动态年龄判断,即发现该对象大小大于survivor区内存大小的50%,会直接进入old区。因此该60M对象会直接移动到old区。因此会出现每过13秒,会有60M数据进入old区

  5. 所以old区大概多久会被占满,2000M/60 *13 = 433s。也就是大概433s后就会触发一次full gc

  6. 因此问题来了,如果这个大促持续数个小时,而每隔7分钟就执行一次full gc,势必影响用户体验和性能。那么怎么优化呢

解决策略:我们完全可以把old区的这60M对象让它在survivor区就回收,减少old区的full gc次数。因此我们需要对jvm参数进行优化配置。(阿里面试题:能否对JVM调优,使其几乎不发生full gc。答案是能)

将年轻代的内存调大(-Xmn2048M)

 

所以,jvm优化后的内存分配模型就出来了:

 

JVM原理和调优(读这一篇就够了)_第9张图片

 

 

2个好处:

  • 原来13秒就minor gc,现在延长到25秒

  • 60M对象会在survivor区就会被回收,不会跑到old区触发full gc

 

 

(全文完)

(喜欢视频的可以去读下这个:https://www.bilibili.com/video/av79368741/?p=5)

 

 

 

你可能感兴趣的:(JVM,jvm,面试)