一、JDK体系结构图
二、JVM整体架构
三、JVM组成
3.1、JVM内存区域的执行底层原理
编辑
3.1.1、程序计数器
3.1.2、堆栈关系的发现
3.1.3、方法去和堆的关系
3.1.4、堆(重点)
3.1.4.1、可达性分析算法
3.1、内存泄漏测试以及堆区的GC监控
3.2、Visual GC插件的安装
3.3、堆区内存监控
四、JVM调优
4.1、Centos7安装Arthas
4.2、测试调优工具
4.2.1、jdk环境查看
编辑 4.2.2、执行ArthasTest.class
4.2.3、问题排查:
4.2.4、执行arthas-boot.jar
4.2.5、Arthas常见的功能命令以及场景分析
4.3、调优案例
4.3.1、亿级流量电商
4.3.2、单机几十万并发案例
五、类加载器分类和核心功能
一、JDK体系结构图
二、JVM整体架构
三、JVM组成
说起JVM组成是什么,我们的第一印象就是堆、栈、方法区、程序计数器等等,但是这样是不对的,真实的JVM的组成由类装载子系统、运行时数据区和字节码执行引擎这三部分组成。而我们之前的回答只是片面的,所以需要留意一下,防止面试被问到。
其中栈、本地方法区和程序计数器是线程独有的,私有的堆和方法区是线程共享的
其中最重要的部分就是内存区域这部分,也是面试问的最多的地方,后续的调优也是针对内存区域进行调优的。
3.1、JVM内存区域的执行底层原理
以下面的程序为例进行说明并结合class字节码文件进行底层剖析:
中间可能会穿插一些概念和小的思考。。。
所以为什么java虚拟机的开发人员使用栈这种先进后出的数据结构?(为什么使用这种设计思想)
答:因为它和方法的嵌套调用执行顺序是相吻合的
3.1.1、程序计数器
这里插入一个概念:什么是程序计数器?
概念:是一种特殊的寄存器,用于存储当前线程正在执行的指令的地址或者下一条即将执行的指令的地址。在多线程环境下,每个线程都有自己独立的程序计数器,用于跟踪该线程当前执行的位置。
为什么java虚拟机设计师要设计程序计数器?
主要是因为多线程。。。。。比如当程序正在运行L1这段代码时(线程1),现在来了一个cpu时间变的更高的线程2将cpu抢占,那么当前线程将会挂起,这里的挂起不是这行代码还没执行完成就挂起,而是执行完才会挂起,等到线程2运行结束了,之前的线程1才会恢复,难道这个时候将要从头开始运行吗?如果是这样的话就有问题了,肯定是出bug了,正确的做法应该是接着线程1后面的线程继续执行(比如L2),那程序是怎么知道的呐?这个时候就要用到计数器了,计数器里的值都是记录好的,当L1执行完时,此时的线程程序计数器的值对应的就是L2,所以程序将从L2继续执行
然后继续往下说:
这里再拓展一下:当程序运行到L2的时候,还没运行完,此时的程序计数器的值是多少?当然是L3了,程序计数器是动态变化修改的,那么问题来了,程序计数器的值是怎样动态修改的????
其实是我们的字节码文件(Math.class)被加载到内存区域的方法区里的(后面会说),加载到这里,是由字节码执行引擎去执行的,字节码文件是知道我们的代码怎么执行的,以及下一步执行什么代码,说白了,其实就是字节码执行引擎去动态修改程序计数器的值(这只是字节码执行引擎的一个功能,其他功能后面还会再聊)
3.1.2、堆栈关系的发现
main栈帧和math栈帧有一点点区别:
首先main栈帧中肯定有一个math局部变量,但是这个局部变量不是个常量,而是new出来的,我们都知道new出来的对象都是放在堆区的,那么问题就来了:
堆中的math对象和math局部变量空间有什么关系:
其实就是,堆中的对象的内存地址存到了math局部变量中了,后续可以根据变量中的地址可以找到堆中的math对象,所以就可以发现栈和堆的关系了,
当栈中有很多不同线程时,每个线程中可能有很多局部变量,其中一部分的局部变量是对象的话,就有一部分指针从栈区指向堆区
3.1.3、方法去和堆的关系
方法区(jdk1.8之后就改成原空间) ,使用的是直接内存
主要放的是:常量,静态变量,类信息
比如:initData这个常量就是放在方法区,然后还有个user这个静态变量也放在方法区,但是,他的结果又是一个对象(new 出来的),当然我们是知道的对象一般都是放在堆中的。
那么方法区中的user和堆中的user是什么关系?其实和上面的是一样的,方法区中的user存放的是指向堆中的user的指针,所以方法区和堆之间的关系和栈与堆的关系其实是一样的,这样方法区和堆的关系也出来了。
3.1.4、堆(重点)
堆内存通常被划分为不同的区域:年轻代和老年代;这种划分主要是为了优化内存管理和垃圾回收效率
年轻代又被划分为:Eden区和Survivor区
大部分new出来的方法堆一般放在Eden区,当Eden空间满时,触发Minor GC(小型垃圾回收)。在Minor GC中,存活的对象会被复制到Survivor空间,而不再存活的对象会被回收。
那么Minor GC的机制是什么?(面试题)
GC整个底层是由字节码执行引擎向后台发起一个垃圾收集线程回收整个年轻代的垃圾
那么Minor GC的机制是什么?(面试题)
GC整个底层是由字节码执行引擎向后台发起一个垃圾收集线程
Full GC回收整个堆内存的垃圾
3.1.4.1、可达性分析算法
这部分包括OOM的产生,以及堆的内存底层机制
可达性分析算法:GC底层使用到的垃圾收集算法
将“GC Root”对象作为起点,从这些节点开始向下搜索引用的对象,然后继续搜索这些引用的对象下面是否还引用其他对象,直到最后一个对象的成员变量不在引用其他对象,而是一些常量上面的,就不再搜索了,凡是在这引用指针链条上的,对于能够通过这些链访问到的对象的,都会被标记成非垃圾对象, 然后将这些非垃圾对象复制到Survivor其中空的区域(通常是Form区),剩余的存在于Eden区域的对象,没有指针指向他们,就被称为垃圾对象,就会被清理或者回。。。。。。。。。。。。。当Eden再次放满,就会再一次触发Minor GC,存活在From空间的对象将被复制到另一个Survivor空间(通常是一个称为To的Survivor空间),同时,From空间和Eden空间都会被清空,如果一直有引用者,一直存活着非垃圾对象,就是一直触发GC,一直会在Form和To之间来回挪动,每挪动一次分代连理+1,当分代连理达到一定次数,就会将存活的非垃圾对象复制到老年代,当老年代满了之后,就会触发Full GC(或者Major GC)进行垃圾回收,如果此时老年代里面都是存活的非垃圾对象,连Full GC也无法回收了,就是出现一种现象:内存溢出(OOM),当然以上也是OOM的发生过程。
GC Root根节点:线程栈上的本地变量、静态变量、本地方法栈上的变量等等
比如上面的:math和user
3.1、内存泄漏测试以及堆区的GC监控
针对以上的堆的讲解,写一个案例,可能更直观一些:
下面的代码一定会内存溢出的
import java.util.ArrayList;
public class HeapTest {
// 分配100KB的堆空间
byte[] a = new byte[1024 * 100];
/**
* 主函数,用于不断创建 HeapTest 实例并加入到列表中,以演示内存占用增长。
*
* @param args 命令行参数,未使用。
* @throws InterruptedException 如果线程在睡眠时被中断则抛出此异常。
*/
public static void main(String[] args) throws InterruptedException{
ArrayList<HeapTest> list = new ArrayList<>();
while (true) { // 持续添加 HeapTest 实例到列表中
list.add(new HeapTest());
Thread.sleep(5);// 线程休眠 5 毫秒
}
}
}
jvisualvm是JDK提供给我们的一个功能强大的jvm(java虚拟机)监控客户端,默认其并不包含对垃圾回收的监控,我们可以通过其插件扩展的机制为 jvisualvm 增加 Visual GC 的功能
打开控制台输入:
jvisualvm
即可打开Java VisualVM
3.2、Visual GC插件的安装
打开Java VisualVM,上方工具栏选择:工具----->插件
选择可用插件,找到Visual GC勾选,就可以进行安装了,我这里已经安装过了,安装好之后,看看插件是否加载进去
随便带你一个进程进去,看看有没有Visual GC,没有的话重新启动Java VisualVM试试
3.3、堆区内存监控
启动程序,打开Java VisualVM,发现进程已经开始了
双击进入进程:
可以监控cpu、堆、类信息、线程等等
下面的图其实就和上面分析的一模一样
当内存泄漏的时候,控制台报错:
四、JVM调优
4.1、Centos7安装Arthas
wget https://alibaba.github.io/arthas/arthas-boot.jar
如果显示没有wget插件就先下载插件:
yum install wget
4.2、测试调优工具
4.2.1、jdk环境查看
执行一下语句:java com.ysy.ArthasTest
注意:这里的ArthasTest文件时class字节码文件,如果不是的话,你可以先上传java文件,然后使用javac ArthasTest.java命令进行编译为class文件
因为我的jdk安装命令是:
yum install -y java-1.8.0-openjdk.x86_64
/usr/lib/jvm/java-1.8.0-openjdk
的 bin
目录下没有 javac
可执行文件
如果你的服务器jdk里没有javac,就执行以下的命令:
yum install java-1.8.0-openjdk-devel
4.2.2、执行ArthasTest.class
4.2.3、问题排查:
出现这个错误的话,可能是文件夹目录和这个java类里的包路径对不上
项目的包路径:
centos7下的服务器的路径:
4.2.4、执行arthas-boot.jar
会列出所有java进程,如果想让arthas帮你诊断的某一个进程的话,就直接输入序列号即可
比如我这个就是输入1,回车,出现logo就成功了。
4.2.5、Arthas常见的功能命令以及场景分析
输入dashboard,显示大盘数据,可以很直观的看一些东西
细心一点的话,我们就可以看到,这个线程一直占用cpu,arthas可以帮助我们很快的定位出问题的代码位置。
输入命令:
去这个类里查看代码:
再次进入大盘数据:dashboard可以查看一些其他的问题:
会发现10和11线程一直处于阻塞状态,这个大屏数据是一只持续刷新的:
引申一个问题,这样的程序会出现上面问题,比如从这个线程状态上来猜一下?
大概率会出现的问题是死锁的情况
再次定位:thread -b
意思就是线程id为11的需要这个java对象,但是现在这个对象被线程id为10的所占用,说明相互之间有依赖等待,才导致的资源阻塞,代码问题在79行;
这样就很容易产生死锁
但是arthas的功能远不止于此。。。
场景一:比如线上发布的产品,测试的时候明明没有问题,发布到线上之后就出问题了,开发组成员可能会认为是运维组版本发错了,然后这个时候运维组就可能觉得是开发组的代码没有改好,这样子两边就开始相互僵持,都觉得不是自己的责任
jad com.ysy.ArthasTest 可以将线上正在运行的代码,反编译为源代码,这样就可以直观地看出正在线上运行的代码是不是真的有问题
场景二:
使用ognl命令可以查看线上系统变量的值,甚至可以修改变量的值
比如双十一,那种的大促活动场景,一般都是代码提前一两周都写好了上线的,后面是不允许修改的,然后再活动开始前一天,你发现有些代码是有一些问题的,比如说开关值,控制程序执行的流程,开关值不对,恰巧数据库中或其他地方设置可以修改的值,就仅仅想修改一下开关值,总不能重新修改之后,打包部署上线吧,一般不是严重的问题是不允许重新打包上线的,但是确实有一个小问题会对业务有影响,这个时候arthas就可以发挥作用,执行上面的命令就可以修改命令;
当然arthas有很多nb的功能。。。
难道使用arthas仅仅只是为了调优吗?解决一些小问题就完事了吗?难道这些就是JVM调优吗?
我们不管是触发Minor GC还是Full GC都会发生STW的,那STW是干什么的,其实就是他会停掉我们的用户线程
举个例子:比如电商网站,凡是由用户行为发生的后端对应的执行线程,比如点击下单,点击购物车等等都是用户发起的行为,这些行为再后台有线程执行,这些线程一般为用户线程,而GC是非用户线程,所以在发生垃圾回收线程的GC操作的时候,就会停掉由用户发起的所有的线程,对于用户来说,现在正在下单点击这个下单按钮,如果这个时候后端发生了垃圾回收线程的GC,用户的这个下单行为就可能会有点卡顿一下,但是一般GC是很快的,如果GC个很慢,那么你点下单按钮,页面就会卡住。。。。所以这个GC在一定程度上对我们网站的性能是有一定的影响的,所以归根到底其实是STW的影响,所以JVM调优的真正的目的是减少STW的发生,那么用户发生卡顿的概率就会降低,网站性能就会上去了。
这里就会有另一个问题:java虚拟机的开发人员为什么会设计STW这个机制??不要它不行吗?非要需要它吗?
我们可以反着想一下,如果没有它会怎么样,也就是触发GC的时候,不使用STW,,,,,比如我们正在执行GC,就像上面的可达性分析算法那样,一直往下搜索对象,比如math对象一直往下找,找完了,该user对象往下找,而现在我们没有STW这个机制,那么程序会一直往下执行,因为上面说了嘛,STW会停掉用户的所有线程,,,程序就一直运行,然后这个时候程序可能已经运行结束了,但是GC还没有执行完毕,之前在GC之前做的对象搜索,那些都是非垃圾对象,现在因为线程结束了,上面的局部变量里的内存都没有了,也意味着指针也都没了,那么这批对象现在干嘛?这时候还在GC没有结束的时候,又由你标记的非垃圾对象变成了垃圾对象,肯定是不合适的,难道我GC下次再从头搜索一边吗?肯定是不现实的,因为这个堆里面可能由上百万的对象,所以需要设计一个STW,有了STW,GC在做垃圾手机的过程中,用户线程不会执行,也就是说,堆中的对象是否是垃圾已经定死了,不会随便改变了,如果随便边的话,就不知道怎么做垃圾收集了,那就把GC整不会了,其实就是这个原因。
4.3、调优案例
4.3.1、亿级流量电商
比如说,一个系统昨晚之后发布上线了,平时运行没什么问题,就是系统稍微有一些小小的压力,就会频繁的Full GC,可能几分钟Full GC一次,正常的Full GC一般是几小时,甚至几天几周执行一次算正常,但是现在几分钟执行一次,肯定是有问题的,网站会非常卡顿的,用户体验也不佳。。。。。。
大家平时做一个java系统上线的时候,会做JVM的参数设置吗?设置的话,你是怎么设置的?
常做的可能就是内存的划分,比如堆内存三个G,元空间1~2个G,java程序留4~5个G等等,大家肯定由这样分配的,这样来说的话,也不算错,但是就是因为这样的设置才会导致系统频繁的发生Full GC,,,这样就有人说了,这样也可以导致系统频繁Full GC?
首先我们先分析一下,当下这个程序的内存模型到底是怎么样的??
就比如说,我们的程序里肯定会有一个叫createOrder的方法,里面肯定会有一个Order order = new Order()的代码,现在我们每秒钟有300个订单,每个订单的对象大小假设是1kb,因为这个方法里可能还有其他的对象,所以我们就放大20倍,也就是说每秒有6MB的对象产生,期间可能也会有别的操作,所以再放大10倍,也就是每秒产生60MB的对象。
然后就看这个内存模型,每过1秒就会有60MB的对象被放入Eden区,而Eden区也就800MB,所以经过14秒左右Eden区就会被放满,放慢之后,就会触发Minor GC,然后Minor GC也会有STW机制,假设说在13或者14秒的时候。你的Minor GC还在执行的时候,有一部分线程还没有结束,肯定是有这个现象的,这个时候用户的线程全部暂停,还未运行完的线程里的对象还存活着,假设这批存活者的对象大概六七十MB左右,应该从Eden区放到Survivor区,但是,这一批对象不一定会放到S区,而是直接放到老年代,,,,,其实这也是JVM的底层,这个知识点属于,对象什么时候放入老年代,这个情况属于:::长期存活的对象将进入老年代,,,还有一些大对象会放入老奶奶带,,,还有老年代空间分配担保机制,,,还有对象动态年龄判断(当前放对象的S区域里,有一批对象的总大小大于这块S区内存区域的50%,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代,当然这个50%的比例可以自己调),,,,所以每过13或14秒都有60多MB的对象被放入老年代,不够放的,每过几分钟就会放慢2G,就会触发Full GC,但是你也会发现,这些对象根本没有必要放到老年代,因为这些对象大概在一秒之后就会都变成垃圾,后头FullGC你会发现大量的垃圾,老年代本来就不是放垃圾的,他一般是存放长期存活的对象。
面试题:能否对JVM调优,让其几乎不发生Full GC
比如说,将年轻代调大一点,老年代没必要那么大,比例调一调,,比如Eden区改为600MB S0和S1都改为200MB,那么之前的60MB对象进入S区,就不如触发动态年龄判断机制,之后再过14秒,触发Minor GC,这60MB早就变成垃圾了,即使最后一两秒Eden区又有一部分对象存活着,就会放入S区的另一个区域,其实S0区域的那60MB早过了14秒,变成垃圾了,下次Minor GC一定会把这60MB干掉,,这样调整一下内存区域的比例,只要系统并发压力不太发生变化的情况下,基本上不会有那种垃圾被放到老年代,老年代就会很久发生一次Full GC甚至不发生(其中一种),这个是通用的方法
4.3.2、单机几十万并发案例
上面的案例配置根本顶不住这么高的并发,比如一秒钟处理20万并发,一条消息假设1kb,那么就有200MB内存要放到Eden区,但现在600MB根本顶不住,一两秒就放满了,就会触发Minor GC比如那最后一两秒的消息,还没来得及存盘,就还存活者,不会被回收掉的,肯定还会有引用指向着的,一下子放到S0,肯定是放不下的,那就放到老年代,而老年代就2G,没几秒就会被放满,Full GC就会非常频繁 所以单机几十万并发,你的物理设备啊,配置啊要非常高,内存搞得很大,cou核数也要很高,,,,,,,比如说这时候年轻代有一二十个G,之前因为Minor GC挺快的,那是因为之前的年轻代内存比较小,你可以忽略他,但是你现在年轻代要做垃圾回收,可能要遍历几十个G,不会很快,其中的STW可能就有几秒钟或者几十秒钟,你一定是需要做优化的,,,,比如说你现在有10秒的STW,客户端现在发送一条消息,突然发生了Minor GC了,我的消息半天没有返回,对于客户端来说还以为你的消息没发成功,就会重复去发,这时候就有大量重复消息,肯定是有问题的,需要去优化的,,,,,那么针对这种高并发的场景,如何去调优,,这时候就要借助垃圾收集器了,JDK1.8默认使用的是Parallel垃圾收集器,但是这种是收集器一点也不适合这种高并发的情况,这就需要其他的垃圾收集器,,,这里先不说垃圾收集器,,,,,
先想一个问题,能不能对垃圾收集的方法做一些优化,让他的停顿时间减少一点????
五、类加载器分类和核心功能
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
- 自定义加载器:负责加载用户自定义路径下的类包
后续会继续写,因为JVM东西太多了,需要深入理解底层东西~~