目录
2 JVM中有哪几块内存区域?Java 8之后对内存分代做了什么改进?
3 你知道JVM是如何运行起来的吗?我们的对象是如何分配的?
6 生产环境中的Tomcat是如何设置JVM参数的?如何检查JVM运行情况?
8 发生OOM之后,应该如何排查和处理线上系统的OOM问题?
1 JDK8的JVM内存模型
由于笔者能力有限,会不断更新此文章,学到哪里更新到哪里.
完整JVM内存模型
动态链接:就是指向方法区记录成员方法的引用地址
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享: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的方法。
2 JVM中有哪几块内存区域?Java 8之后对内存分代做了什么改进?
首先执行我们的一些对象的方法,执行代码的时候肯定会有很多的线程,tomcat里就有很多自己的工作线程,去执行我们写的代码,每个工作线程都会有自己的一块数据结构,栈内存,这个里面是存放一些东西
java 8以后的内存分代的改进,永久代里放了一些常量池+类信息,常量池 -> 堆里面,类信息 -> metaspace(元区域)
元空间(MetaSpace)和永久代(PermGen)的区别:
-
元空间使用本地内存,而永久代使用的是jvm的内存
MetaSpace相比PermGen的优势:
-
字符串常量池存在于永久代中,容易出现性能问题和内存溢出
-
类和方法的信息大小难以确定,给永久代的大小指定带来困难
-
永久代会为GC带来不必要的复杂性,并且回收效率低
-
方便HotSpot与其他JVM如Jrockit的集成
Java堆(Heap):
-
对象实例的分配区域
-
GC管理的主要区域-新生代、老年代
3 你知道JVM是如何运行起来的吗?我们的对象是如何分配的?
3.1 首先要知道JVM如何加载class文件
JVM通过Class Loader将符合其格式要求的class文件加载到内存里,并通过Execution Engine去解析class文件里边的字节码,并提交给操作系统去执行。
3.2 ClassLoader类加载器
类从编译到执行的过程:
-
编译器将Robot.java源文件编译为Robot.class字节码文件
-
ClassLoader将字节码转换为JVM中的Class对象
-
JVM利用Class对象实例化为Robot对象
谈谈什么是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编写,定制化加载
3.3 ClassLoader的双亲委派机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
为什么要使用双亲委派机制去加载类:
-
避免多份同样字节码的加载,内存是相当宝贵的,没有必要保存两份一样的类对象,即Class对象。
-
也有效防止自己的类名和已经写好的冲突而去调用父类,不会使用子类的.
3.4 最后一定会有线程去执行我们写的代码
比如说我们有一个类里面包含了一个main方法,你去执行这个main方法,此时会自动一个jvm进程,他会默认就会有一个main线程,这个main线程就负责执行这个main方法的代码,进而创建各种对象
tomcat,类都会加载到jvm里去,spring容器而言都会对我们的类进行实例化成bean,有工作线程会来执行我们的bean实例对象里的方法和代码,进而也会创建其他的各种对象,实现业务逻辑
4 JVM在哪些情况下会触发垃圾回收可以吗?
我们的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,谁是可以回收的垃圾对象呢?就是没有人引用的对象就是垃圾对象
4 JVM的年轻代垃圾回收算法?对象什么时候转移到老年代?
如果说你让代码一边运行,一边有变动,一边判断哪些对象是可以回收的,这个是不现实的,垃圾回收的时候有一个概念,叫做stop the world,停止你的jvm里的工作线程的运行,然后扫描所有的对象,判断哪些可以回收,哪些不可以回收的
年轻代,大部分情况下,对象生存周期是很短的,可能在0.01ms之内,线程执行了3个方法,创建了几个对象,0.01ms之后就方法都执行结束了,此时那几个对象就会在0.01ms之内变成垃圾,可以回收的
100个对象,可能90个对象都是垃圾对象,10个对象是存活的对象,5个
复制算法,一次young gc,年轻代的垃圾回收
三种场景,第一种场景,有的对象在年轻代里熬过了很多次垃圾回收,15次垃圾回收,此时会认为这个对象是要长期存活的对象
Spring容器里,对每个bean实例对象就一个,长期存活,一直给我们来使用
5 老年代的垃圾回收算法?常用的垃圾回收器都有什么?
老年代对象越来越多,是不是会发现说,老年代的内存空间也会满的,可以不可以使用类似年轻代的复制算法,不合适的,因为老年代里的对象,很多都是被长期引用的,spring容器管理的各种bean
长期存活的对象是比较多的,可能甚至有几百MB
对老年代而言,他里面垃圾对象可能是没有那么多的,标记-清理,找出来那些垃圾对象,然后直接把垃圾对象在老年代里清理掉,标记-整理,把老年代里的存活对象标记出来,移动到一起,存活对象压缩到一片内存空间里去
剩余的空间都是垃圾对象整个给清理掉,剩余的都是连续的可用的内存空间,解决了内存碎片的一个问题
parnew+cms的组合,g1直接分代回收,新版本,慢慢的就是主推g1垃圾回收器了,以后会淘汰掉parnew+cms的组合,jdk 8~jdk 9比较居多一些,parnew+cms的组合比较多一些,是这么一个情况
分成好几个阶段,初始标记,并发标记,并发清理,等等,老年代垃圾回收是比较慢的,一般起码比年轻代垃圾回收慢个10倍以上,cms的垃圾回收算法,刚开始用标记-清理,标记出来垃圾对象,清理掉一些垃圾对象,整理,把一些存活的对象压缩到一起,避免内存碎片的产生
执行一个比较慢的垃圾回收,还要stop the world,需要100mb,此时就会让系统停顿100ms,不能处理任何请求,尽可能的让垃圾回收和工作线程的运行,并发着来执行
6 生产环境中的Tomcat是如何设置JVM参数的?如何检查JVM运行情况?
面试的时候,面试官很多时候都是针对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耗时,内存的消耗
7 在实际项目中是否做过JVM GC优化,怎么做的?
如何通过预估 + 压测,做一份生产环境的jvm参数出来的,如何去观察jvm运行的情况,jvm出现频繁full gc的问题,你有没有尝试过生产环境的系统去进行gc的一个优化,对于这个问题
在通常意义上⼈们⼝中说的Full GC为⼀次特殊GC⾏为的描述,这次GC会回收整个堆的内存,包含⽼年代,新⽣代,metaspace等,这个是最常见的⼀种认知,很多⼈也就了解到这个程度,因此在遇到⼀些特殊场景的时候就会发现实际情况和⾃⼰的认知会发⽣冲突从GC⽇志上在gc.log中会发现在部分gc⽇志头中也有Full GC这样的字眼,这⾥表⽰的含义是在这次GC的全过程中,都是Stop The world的状态,也就是说在这次GC的全过程中所有⽤户线程都是处于暂停的状态.
通过很多个案例,去分析如何在各种各样奇葩的背景之下,发现jvm的gc很频繁,导致系统卡顿问题
如何一步一步去分析系统的jvm的性能问题,如何去进行jvm gc调优
假设你没看过jvm专栏,自己做过jvm gc的生产调优,恭喜你了,直接实话实说,你当时怎么调优,你们的问题如何暴露出来的,你如何一步一步定位问题的,如何进行调优,最后的结果是什么
你看了jvm专栏,在过程中,或者看完以后,在自己生产环境中根据专栏学习到的知识,去调优过jvm,这个时候,你可以专栏里学习到的知识,去讲。最好对自己系统的生产环境的jvm,进行一个分析,gc频繁的问题
你尽可能的去调优一下参数,如果效果比较好
发现分析了一下生产环境的jvm的运行情况,非常好,并发量很低,几十分钟才一次young gc,存活的对象特别少,几乎都在s区域,老年代几乎没什么对象,几天或者几周才发生一次full gc
在自己本地单机部署,测试环境里,去压测,每秒单机有500并发请求,去观察jvm的运行情况,这个时候他会不会存在频繁gc的问题,你就去调优一下,你就可以基于这个压测的例子去说了
一定要结合你自己的业务,系统,接口,干什么,并发请求,jvm运行的情况,问题出在哪儿,如何调优,效果如何
GC调优
各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。
活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小
通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。
根据对象生命周期的分布情况:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。
8 发生OOM之后,应该如何排查和处理线上系统的OOM问题?
oom可能发生在哪几个区域,解决的一个思路,在jvm里可以设置几个参数,如果一旦jvm发生了oom之后,就会导出一份内存快照,就会有当时的线上内存里的对象的一个情况,可以用MAT这样的工具,可以去分析
无非就是找出来当时的时候占用内存最大的对象都是谁,找出来那些对象是在代码中哪些地方创建出来的,一般来说就是可能会对内存去做一个调优
还是得去参考jvm专栏里的大量的案例背景,从业务背景出发,一步步去说明,在什么样的业务背景之下,为什么会产生oom的问题呢?必然会导致系统可能就是崩溃了,客服会反馈说,XX功能不能用了,说某个系统崩溃了
找他自动导出的内存快照,分析,XX对象,直接去定位代码,修改代码
你一定要把案例的业务、背景和思想给吸收了,就得融入到自己的业务里去,我负责的业务系统,在什么样的情况下,可能说会出现一大批的对象卡在内存里,无法回收,导致我系统没法放更多的对象了
产生OOM,内存泄漏的问题,少数场景在互联网公司,超高并发下的oom问题,瞬时大量存活对象占据内存, 导致没法创建更多的对象了
你也得去思考,甚至去模拟一下,最好可以模拟出来,oom不是你自己的代码,可能是你依赖的第三方的组件,netty导致的,结合自己的项目去一步一步的分析,oom问题的产生,和解决的过程