目录
1 JDK8的JVM内存模型
2 JVM中有哪几块内存区域?Java 8之后对内存分代做了什么改进?
3 你知道JVM是如何运行起来的吗?我们的对象是如何分配的?
3.1 首先要知道JVM如何加载class文件
3.2 ClassLoader类加载器
3.3 ClassLoader的双亲委派机制
3.4 最后一定会有线程去执行我们写的代码
4 JVM在哪些情况下会触发垃圾回收可以吗?
4 JVM的年轻代垃圾回收算法?对象什么时候转移到老年代?
5 老年代的垃圾回收算法?常用的垃圾回收器都有什么?
6 生产环境中的Tomcat是如何设置JVM参数的?如何检查JVM运行情况?
7 在实际项目中是否做过JVM GC优化,怎么做的?
8 发生OOM之后,应该如何排查和处理线上系统的OOM问题?
由于笔者能力有限,会不断更新此文章,学到哪里更新到哪里.
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:MetaSpace、Java堆
程序计数器(Program Counter Register)
当前线程所执行的字节码行号指示器(逻辑)
改变计数器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转、异常处理、线程恢复等基础功能
和线程是一对一的关系,即“线程私有”
对Java方法技术,如果是Native方法则计数器值为Undefined
不会发生内存泄漏
程序计数器是逻辑计数器,而非物理计数器;为了线程切换后都能恢复正确的执行位置,每个线程都有一个独立的程序计数器,是线程独立的,只对Java方法计数,对Native方法则为Undefined,不会发生内存泄漏。
Java虚拟机栈(Stack)
Java方法执行的内存模型
包含多个栈帧,方法运行期间的基础数据结构,用于存储局部变量表、操作栈、动态链接、返回地址等。
局部变量表和操作数栈
局部变量表:包含方法执行过程中的所有变量
操作数栈:入栈、出栈、复制、交换、产生消费变量
递归为什么会引发java.lang.StackOverflowError异常?
递归过深,栈帧数超过虚拟机栈深度。限制递归次数,使用循环替代。
public class Fibonacci {
//F(0) = 0, F(1) = 1, 当n >= 2的时候, F(n) = F(n - 1) + F(n - 2),
//F(2) = F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1 + 1 = 2
//F(0) - F(N)依次为 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
public static int fibonacci(int n) {
if (n == 0) {return 0;}
if (n == 1) {return 1;}
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
System.out.println(fibonacci(100000));
}
}
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
public static void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
while (true) {
}
}
}.start();
}
}
虚拟机栈也是Java虚拟机自动管理的,栈类似于一个集合,但是有固定的容量,是由多个栈帧合起来的。每调用一个方法Java虚拟机就会在内存中分配对应的一块空间,这块空间也就是一块栈帧,当方法调用结束后,对应的栈帧就会被自动释放掉。栈的内存不需要通过GC去回收,而会自动释放。
本地方法栈:
与虚拟机栈相似,主要作用于标注了native的方法。
首先执行我们的一些对象的方法,执行代码的时候肯定会有很多的线程,tomcat里就有很多自己的工作线程,去执行我们写的代码,每个工作线程都会有自己的一块数据结构,栈内存,这个里面是存放一些东西
java 8以后的内存分代的改进,永久代里放了一些常量池+类信息,常量池 -> 堆里面,类信息 -> metaspace(元区域)
元空间(MetaSpace)和永久代(PermGen)的区别:
元空间使用本地内存,而永久代使用的是jvm的内存
MetaSpace相比PermGen的优势:
字符串常量池存在于永久代中,容易出现性能问题和内存溢出
类和方法的信息大小难以确定,给永久代的大小指定带来困难
永久代会为GC带来不必要的复杂性,并且回收效率低
方便HotSpot与其他JVM如Jrockit的集成
Java堆(Heap):
对象实例的分配区域
GC管理的主要区域-新生代、老年代
JVM通过Class Loader将符合其格式要求的class文件加载到内存里,并通过Execution Engine去解析class文件里边的字节码,并提交给操作系统去执行。
类从编译到执行的过程:
编译器将Robot.java源文件编译为Robot.class字节码文件
ClassLoader将字节码转换为JVM中的Class
JVM利用Class
谈谈什么是ClassLoader
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据流。他是Java的核心组件,所有的Class都是由ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
ClassLoader的种类:
1.Bootstrap ClassLoader 根类加载器,也被称为引导类加载器,负责Java核心类的加载,比如System,String等。在JDK中jre的lib目录下rt.jar文件中
2.Extension ClassLoader 扩展类加载器负责JRE的扩展目录中jar包的加载。在JDK中JRE的lib目录下ext目录
3.Sysetm ClassLoader 系统类加载器(又叫做Application ClassLoader)负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径
4.自定义ClassLoader:Java编写,定制化加载
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
为什么要使用双亲委派机制去加载类:
避免多份同样字节码的加载,内存是相当宝贵的,没有必要保存两份一样的类对象,即Class对象。
也有效防止自己的类名和已经写好的冲突而去调用父类,不会使用子类的.
比如说我们有一个类里面包含了一个main方法,你去执行这个main方法,此时会自动一个jvm进程,他会默认就会有一个main线程,这个main线程就负责执行这个main方法的代码,进而创建各种对象
tomcat,类都会加载到jvm里去,spring容器而言都会对我们的类进行实例化成bean,有工作线程会来执行我们的bean实例对象里的方法和代码,进而也会创建其他的各种对象,实现业务逻辑
我们的jvm的内存其实是有限制的,不可能是无限的,昂贵的资源,2核4G的机器,堆内存也就2GB左右,4核8G的机器,堆内存可能也就4G左右,栈内存也需要空间,metaspace区域放类信息也需要空间
在jvm里必然是有一个内存分代模型,年轻代和老年代
比如说给年轻代一共是2GB内存,给老年代是2GB内存,默认情况下eden和2个s的比例:8:1:1,eden是1.6GB,S是0.2GB
如果说eden区域满了,此时必然触发垃圾回收,young gc,ygc,谁是可以回收的垃圾对象呢?就是没有人引用的对象就是垃圾对象
如果说你让代码一边运行,一边有变动,一边判断哪些对象是可以回收的,这个是不现实的,垃圾回收的时候有一个概念,叫做stop the world,停止你的jvm里的工作线程的运行,然后扫描所有的对象,判断哪些可以回收,哪些不可以回收的
年轻代,大部分情况下,对象生存周期是很短的,可能在0.01ms之内,线程执行了3个方法,创建了几个对象,0.01ms之后就方法都执行结束了,此时那几个对象就会在0.01ms之内变成垃圾,可以回收的
100个对象,可能90个对象都是垃圾对象,10个对象是存活的对象,5个
复制算法,一次young gc,年轻代的垃圾回收
三种场景,第一种场景,有的对象在年轻代里熬过了很多次垃圾回收,15次垃圾回收,此时会认为这个对象是要长期存活的对象
Spring容器里,对每个bean实例对象就一个,长期存活,一直给我们来使用
老年代对象越来越多,是不是会发现说,老年代的内存空间也会满的,可以不可以使用类似年轻代的复制算法,不合适的,因为老年代里的对象,很多都是被长期引用的,spring容器管理的各种bean
长期存活的对象是比较多的,可能甚至有几百MB
对老年代而言,他里面垃圾对象可能是没有那么多的,标记-清理,找出来那些垃圾对象,然后直接把垃圾对象在老年代里清理掉,标记-整理,把老年代里的存活对象标记出来,移动到一起,存活对象压缩到一片内存空间里去
剩余的空间都是垃圾对象整个给清理掉,剩余的都是连续的可用的内存空间,解决了内存碎片的一个问题
parnew+cms的组合,g1直接分代回收,新版本,慢慢的就是主推g1垃圾回收器了,以后会淘汰掉parnew+cms的组合,jdk 8~jdk 9比较居多一些,parnew+cms的组合比较多一些,是这么一个情况
分成好几个阶段,初始标记,并发标记,并发清理,等等,老年代垃圾回收是比较慢的,一般起码比年轻代垃圾回收慢个10倍以上,cms的垃圾回收算法,刚开始用标记-清理,标记出来垃圾对象,清理掉一些垃圾对象,整理,把一些存活的对象压缩到一起,避免内存碎片的产生
执行一个比较慢的垃圾回收,还要stop the world,需要100mb,此时就会让系统停顿100ms,不能处理任何请求,尽可能的让垃圾回收和工作线程的运行,并发着来执行
面试的时候,面试官很多时候都是针对jvm的一些运行原理去深扣,结合我讲的东西,然后去把jvm专栏里面的内容仔细看一下,应付面试都是很容易的,一般来说都会这么问,你们线上系统的生产环境的jvm参数是怎么来配置的,为什么要这么配置,在你们配置的这个参数之下,线上系统jvm运行的情况如何
你确实必须得去看一下你当前生产系统的jvm参数都是如何设置的,如果说你是tomcat部署的java web系统,jvm进程对应的tomcat自己,你的系统仅仅是在tomcat的jvm进程来执行
tomcat的一个配置脚本,catalina脚本里去找一下,jvm专栏都有说明的,里面是有对应的tomcat启动的一些jvm参数的设置
比如通过java命令直接启动你的一个main方法跑起来的系统,就是你自己启动的时候,java命令可以带上一些jvm参数
对你自己系统的jvm参数有一个了解,内存区域大小的分配,每个线程的栈大小,metaspace大小,堆内存的大小,年轻代和老年代分别的大小,eden和survivor区域的大小分别是多少,如果没有设置,会有一些默认值
jvm专栏里,在中间有一些地方,他是讲了一些命令的,可以查看jvm的启动默认参数
垃圾回收器,年轻代是用了什么,老年代,每种垃圾回收器是否有对应的一些特殊的参数有设置,那些特殊的参数分别都是用来干什么的
为什么要这么设置呢?当前线上系统运行的时候,jvm的表现如何?
救火队队长的《从0开始带你成为jvm实战高手》,有大量的实战案例的讲解,业务背景引出,在一定的业务背景之下,如何去进行系统运行时的对象数量的预估,对内存的压力进行预估,对整个jvm运行的状况进行预估
预估完毕之后,根据预估的情况,可以去设置一些jvm参数
进行压测,在压测的时候,其实就需要去观察jvm运行的情况,jstat工具去分析jvm运行的情况,他的年轻代里的eden区域的对象增长的情况,ygc的频率,每次ygc过后有多少对象存活,s能否放的下,老年代对象增长速率,老年代多久会触发一次fgc
就可以根据压测的情况去进行一定的jvm参数的调优,一个系统的QPS,一个是系统的接口的性能,压测到一定程度的时候 ,机器的cpu、内存、io、磁盘的一些负载情况,jvm的表现
可能需要对一些代码进行优化,比如优化性能,或者减轻一点cpu负担,减轻io和磁盘负担,发现jvm的gc过于频繁,内存泄漏,此时就需要对jvm的各个内存区域的大小以及一些参数进行调优
跑到线上实际生产环境里去,运行的过程中,也需要基于一些监控工具,或者是jstat,除了观察系统的QPS和性能,接口可用性,调用成功率,机器的负载,jvm的表现,gc的频率,gc耗时,内存的消耗
如何通过预估 + 压测,做一份生产环境的jvm参数出来的,如何去观察jvm运行的情况,jvm出现频繁full gc的问题,你有没有尝试过生产环境的系统去进行gc的一个优化,对于这个问题
儒猿技术窝公众号上的救火队队长的《从0开始带你成为jvm实战高手》,有非常详细的案例的分析,通过很多个案例,去分析如何在各种各样奇葩的背景之下,发现jvm的gc很频繁,导致系统卡顿问题
如何一步一步去分析系统的jvm的性能问题,如何去进行jvm gc调优
假设你没看过jvm专栏,自己做过jvm gc的生产调优,恭喜你了,直接实话实说,你当时怎么调优,你们的问题如何暴露出来的,你如何一步一步定位问题的,如何进行调优,最后的结果是什么
你看了jvm专栏,在过程中,或者看完以后,在自己生产环境中根据专栏学习到的知识,去调优过jvm,这个时候,你可以专栏里学习到的知识,去讲。最好对自己系统的生产环境的jvm,进行一个分析,gc频繁的问题
你尽可能的去调优一下参数,如果效果比较好
发现分析了一下生产环境的jvm的运行情况,非常好,并发量很低,几十分钟才一次young gc,存活的对象特别少,几乎都在s区域,老年代几乎没什么对象,几天或者几周才发生一次full gc
在自己本地单机部署,测试环境里,去压测,每秒单机有500并发请求,去观察jvm的运行情况,这个时候他会不会存在频繁gc的问题,你就去调优一下,你就可以基于这个压测的例子去说了
一定要结合你自己的业务,系统,接口,干什么,并发请求,jvm运行的情况,问题出在哪儿,如何调优,效果如何
oom可能发生在哪几个区域,解决的一个思路,在jvm里可以设置几个参数,如果一旦jvm发生了oom之后,就会导出一份内存快照,就会有当时的线上内存里的对象的一个情况,可以用MAT这样的工具,可以去分析
无非就是找出来当时的时候占用内存最大的对象都是谁,找出来那些对象是在代码中哪些地方创建出来的,一般来说就是可能会对内存去做一个调优
还是得去参考jvm专栏里的大量的案例背景,从业务背景出发,一步步去说明,在什么样的业务背景之下,为什么会产生oom的问题呢?必然会导致系统可能就是崩溃了,客服会反馈说,XX功能不能用了,说某个系统崩溃了
找他自动导出的内存快照,分析,XX对象,直接去定位代码,修改代码
你一定要把案例的业务、背景和思想给吸收了,就得融入到自己的业务里去,我负责的业务系统,在什么样的情况下,可能说会出现一大批的对象卡在内存里,无法回收,导致我系统没法放更多的对象了
产生OOM,内存泄漏的问题,少数场景在互联网公司,超高并发下的oom问题,瞬时大量存活对象占据内存, 导致没法创建更多的对象了
你也得去思考,甚至去模拟一下,最好可以模拟出来,oom不是你自己的代码,可能是你依赖的第三方的组件,netty导致的,结合自己的项目去一步一步的分析,oom问题的产生,和解决的过程