JVM < JRE < JDK
JVM包括:
方法区(jdk1.8之后的版本已无)、堆区
虚拟机栈、本地方法栈、程序计数器(PC)
执行引擎、GC
本地库接口、本地方法库
程序计数器是唯一不会出现内存溢出的OutOfMemoryError。
虚拟机栈存放的方法入口地址、操作数栈、局部变量表、动态链接、出口地址,栈帧。
方法区:存储常量、静态变量(非静态对象)、方法
堆区:分配的类实例。
metaspace
永久代有MAX上限,方法区会更容易带来内存溢出问题。老年代和永久代的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久代和老年代的GC。
方法区的垃圾回收的效果很不好。
所以使用元数据区代替方法区,并移出了JVM,使用本地内存。(常量池、方法元信息、类元信息)
注:字符串常量池jdk7上就被转移到堆区了。
参考:Java方法区与元空间 - 知乎 (zhihu.com)
GC
新生代、老年代、永久代(方法区,jdk1.8变成了metadata区)
参考:元空间和永久代的区别,史上最全,看了必会 - 知乎 (zhihu.com)
如何识别出要回收的对象:调用链,可达性分析。
垃圾回收的算法
新生代:标记复制算法(简称复制算法),始终保持一个survivor空闲。
老年代:标记整理算法,每次将存活对象向内存一端移动,然后回收边界外的对象,相比标记复制算法提高内存利用率。适用于存活对象较多的场合。
metaspace:full GC
参考:面试题提问“常见GC算法”_见习程序员的成长之路!-CSDN博客
对于比较大的对象也直接进入老年代,1、由于需要较大的内存,可能会带来频繁的垃圾回收;2、防止频繁的拷贝。
直接内存:非虚拟机运行时数据区的部分。
通过NIO类,使用Native方法库直接分配堆外内存,然后通过一个堆中的DirectByteBuffer对象作为直接内存的引用来对内存进行直接操作,避免数据来回移动耗时。
类加载的过程
1、加载
如果方法区未找到该类,从.class文件里面去加载。
2.1、验证
对文件格式、需要的虚拟机版本号等进行校验。
2.2、预处理
为类变量(static变量)、常量分配内存,并初始化为零值。
2.3、解析
类、接口、字段、方法的符号解析,优先从当前类找,然后再到父类去找。
3、初始化
执行类构造器<clinit>的过程,static变量和static代码块,从上往下依次执行。静态代码块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
PS:实例构造器<init>,new 或者反射的newInstance()方法。并不属于类加载过程。
参考:双亲委派模型,类的加载机制,搞定大厂高频面试题 - 日拱一兵 - 博客园 (cnblogs.com)
【深入Java虚拟机】之四:类加载机制_兰亭风雨的专栏-CSDN博客_java类加载机制
双亲委派模型
双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试自己加载, 如果都没加载到,则会抛出 ClassNotFoundException 异常。
类加载器的类型:
BootstrapClassLoader(启动类加载器):{JAVA_HOME}/lib
ExtClassLoader (标准扩展类加载器):{JAVA_HOME}/lib/ext
AppClassLoader(应用程序类加载器)
CustomClassLoader(用户自定义类加载器)
作用:
1、防止重复加载class文件。JVM两个类是否相等,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个class文件,也被认为是不等的。
2、保证class文件不被篡改。
破坏双亲委派模型:
实现热部署。
参考:面试官:java双亲委派机制及作用 - 简书 (jianshu.com)
JVM 类加载机制及双亲委派模型 (juejin.cn)
引用分类
强引用:默认,当内存不够或者CPU空闲时回收。
弱引用:下一次GC时,回收
软引用:仅内存不够时,才会回收。
虚引用:相当于没有引用,gc时立马回收。
虚拟机性能调优:
jconsole、jstack
老年代和新生代的比例,堆内存大小等
内存模型
主内存、工作内存
lock、unlock
read、write
load、store
use、assign
系统运行缓慢
1、cpu利用率过高。
用户线程
垃圾回收线程。
2、系统阻塞。
死锁
阻塞性操作
线程进入waiting状态。
阻塞之后,首先就是导出jstack堆栈信息和dump内存信息,尽快恢复业务。
cpu过高问题如何处理
1、top 查看CPU利用率最高的进程。
2、top -Hp <pid> 查看最高的线程。
3、jstack <pid> > stack.log 记录问题进程所有线程的堆栈信息。
4、根据线程id从stack.log里面寻找堆栈信息。注意线程id要使用printf "%x\n" xxx转成16进制。
5、如果是VM Thread,说明是垃圾回收过于频繁,需要使用jstat -gcutil <pid> <period> <times>查看GC情况。
如果垃圾回收次数在不停增加,则说明确实是内存溢出了。
然后通过dump导出内存日志,使用eclipse的mat工具来确认到底是哪个对象带来的内存溢出。
6、否则就是用户线程本身业务很耗时,就需要根据堆栈信息分析代码了。
如果直接看代码看不出来,那就压测看堆栈信息,多个线程都阻塞在同一个地方,那就很可能是这里的问题了。
参考:面试被问怎么排查平时遇到的系统CPU飙高和频繁GC,该怎么回答?_u011277123的博客-CSDN博客
高频面试题:Java程序占用 CPU 过高怎么排查 - 简书 (jianshu.com)
OOM异常及处理手段,主要针对堆内存:
1、是否存在超大对象,比如超大数组,一次性查询了数据库的全部记录而没有做结果数的限制。
2、是否是大流量场景,可以采用享元模式分为内部状态和外部状态,尽量少创建对象。或者直接做限流处理。
3、是否存在内存泄露,常见File等资源未回收。
4、过度使用finalize(垃圾回收之前的处理,参考:java中finalize的作用是什么 - 编程语言 - 亿速云 (yisu.com)),该对象没有立即被GC。
5、调高堆内存大小。
6、如果是元空间、或者栈内存溢出,需要另外考虑。
参考:常见9种 OOM 原因及解决方案_小小小跟班的博客-CSDN博客
某个线程进入WAITING状态:可以每隔10s使用jstack抓取堆栈信息,多次抓取,如果发现始终处于等待状态的线程即是我们要找的。
死锁:jstack可以直接看到了,也就可以找到阻塞位置了。
jps:输出JVM中运行的进程状态信息。
jstack:查看线程堆栈信息。
jmap:查看堆内存使用情况。
jstat:统计gc的容量和使用量。
hprof:查看CPU使用率,统计堆内存使用情况。
参考:jvm(四)——JVM自带内存分析工具详解_节至-CSDN博客_java内存分析工具