一、JVM是什么
JVM 是Java Virtual Machine
的缩写,即Java虚拟机。
二、JVM的组成
需要注意的是,在Java8以前,常量池是放在堆中的永久代,在8以后,取消永久代选择使用元空间后,常量池就放在了方法区中。
程序计数器(PC计数器)
与操作系统中的PC计数器是一样的,用于指向下一条将被执行指令的地址。
方法区
存储静态变量、常量、类信息、运行时常量池。
本地方法栈
主要保存一些本地接口,这些本地接口用来调用的C或者C++的库,这些接口在Java程序中多表现为Native关键字声明。
例如Thread类中,启动线程和线程礼让yield都需要调用本地接口,这些可调用的本地接口被“登记”在本地方法栈中。
public static native void yield();
private native void start0();
虚拟机栈
虚拟机栈主要保存方法的信息,例如方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
当线程结束时,栈内存也就释放,所以栈不存在内存溢出问题。
堆
堆算是最大一块结构了,堆主要用来保存对象的示例,一般对象都保存在堆中,(但Java中的对象也不一定是在堆上分配的,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。)
如果一个对象被JVM分析是在内部创建和使用,那么这个对象就是非逃逸的
对于非逃逸的对象,会分配到栈上,同时会基于标量替换技术,将对象换成基本数据类型存在方法的局部变量
逃逸分析还会判断一个对象是不是只在一个线程中运行,这时候不需要同步,减少同步开销
如果堆溢出,那么会报OOM异常,即Out Of Memory。
JVM堆内存被分为两部分:年轻代(Young Generation)和老年代(Old Generation)。
事实上,还有永久代,年老代对象经历了多次 Major GC 仍然没有被回收即进入永久代。
但仅 JDK1.7 及之前的版本拥有,后来换成了元空间,这里不再赘述。
年轻代
年轻代是所有新对象产生的地⽅。
当年轻代内存空间被用完时,就会触发垃圾回收。这时垃圾回收叫做Minor GC
。
年轻代还可被细分为3个部分
- Enden区
- Survivor区From区
- Survivor区To区
需要注意的是,survivor区同⼀时间只会有⼀个满⼀个空,是交替的,并且空的是To区。
这里实际上是对应了垃圾回收的复制算法
年轻代空间的要点:
- ⼤多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中⼀个survivor区,所以在GC后,Eden区是空的。
- Minor GC同样会检查存活下来的对象,并把它们转移到另⼀个survivor区。这样在⼀段时间内,总会有⼀个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间,通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。这个年龄阈值一般是15,即经过15次GC还没有被回收,那么就会进入年老代。这个值可以通过
-XX:Maxtenuing Threshold
设置,默认为15
年老代
年⽼代内存⾥包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。
通常会在⽼年代内存被占满时进行垃圾回收。
三、GC种类
Minor GC
新生代的垃圾收集叫Minor GC,因为 Java 对象大多都是生命周期很短,所以 Minor GC 非常频繁,一般回收速度也比较快。
Major GC
老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。
Full GC
Full GC定义是相对明确的,就是针对整个新生代、老年代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
CMS和G1都是具体的垃圾回收器,详情可见JVM知识梳理(三) JVM常见垃圾回收器解析
使用工具排查OOM异常
命令行下输入jvisualvm
,就可以使用JVM自带的分析工具
在这个软件中,左上角工具栏-》工具-》插件,我们去下载Visual GC插件。
下载完成后,记得激活,这里我下载完成后,自动激活了。
下载完成后,我们就可以使用它啦,是一个不错的图形化界面
模拟一个堆溢出的程序
public class Test {
public static void main(String[] args) throws InterruptedException {
List<byte[]> list = new ArrayList<>();
final Integer K = 1024;
int size = K*K*8;
for (int i = 0; i <K ; i++) {
System.out.println("写入"+(i+1)+"M");
TimeUnit.MILLISECONDS.sleep(200);
list.add(new byte[size]);
}
}
}
运行后,左边就可以看到多了一个Java进程。
控制台提示写入的内存信息。
同样,可以看到堆占用不断上升
最终达到堆峰值,最终OOM异常,程序结束,内存释放,所以会出现断崖式下降。
在程序运行时,我们还可以dump下文件
可以看到具体的类信息。
参考
阿里面试官说出内存溢出排查过程,我用了MAT和jvisualvm他对我点赞
B站狂神说
扒一扒JVM的垃圾回收机制,下次面试你准备好了吗l