执行一个Java进程对应启动一个JVM进程; Java虚拟机使用的是HotSpot虚拟机;18年oracle公开了GraalVM;
Jvm内存结构
栈代表了处理逻辑,而堆代表了数据; 从线程角度可以分为2个区域:
- 线程私有区;
- 程序计数器;
- 虚拟机栈;
- 本地方法栈;
- 线程共享区;
- Heap区;
- Non-heap 方法区;(元空间)
- 直接内存;
1. 线程私有区:栈
每个线程运行时所使用的内存结构,包含:程序计数器、多个栈帧对应每次方法调用,其中包含一个活动栈帧,对应正在执行的方法,以及调用Native方法的本地方法栈;
- 垃圾回收不涉及栈内存;
- 且栈内存并不是越大越好;-Xss 1024k、-Xss 1m
- 栈帧内局部变量是线程安全的;
栈的结构:
- 程序计数器:记录当前线程下一条要执行的指令;
- 虚拟机栈:用来存储局执行方法执行所需要的数据(生命周期跟随线程);
- 存储单元为栈帧·,每一个方法执行到结束,对应一个栈帧的入栈出栈
- 每个栈帧:包含部变量表、操作数栈、动态链接、方法出口等信息
- 局部变量表:用来生成此方法所需要的所有局部变量,提前给定内存空间;(编译期确定)
- 操作数栈:存储方法中的计算过程的中间结果;
- 动态链接:指向常量池中的符号引用,如需要调用别的方法,就通过动态链接找到方法引用;
- 方法出口
- 本地方法栈:与虚拟机栈的作用类似,用于执行Native方法(非Java方法);
- 如Unsafe类,用于与操作系统交互;
- Object下的wait()、notify()、notifyAll()都是Native方法;
2. 线程共享区:堆
主要用于存放对象实例以及数组;(垃圾回收的主要区域)
- 年轻代
- 老年代
- 字符串常量池(JDK1.8):存放编译/运行时创建的字面量:new String("xx")
2.1 年轻代
年轻代目标:尽可能快速的收集掉那些生命周期短的对象,减少存入老年代的对象;
结构:Eden、Survivor(两个)
年轻代存储过程:
- 大多数新创建的对象都位于Eden内存空间中;
- 当Eden区满时,触发MinorGC,存活的对象被复制到Survivor区中的一个
- 之后每经历一次MinorGC,gc年龄+1(gc年龄初始为1,记录在对象头中)
- gc年龄达到阈值仍存活的对象,则可进入老年代;也可能由于空间不足,提前进入老年代,默认50%;即按照年龄排序后,超出50%的大年龄对象,会在MinorGC时移入老年代 (复制算法:对象从Survivor一个区移动到另一个区,并回收不可达对象)
2.2 老年代
目标:存储生命周期长的大对象(需要大量内存),不会轻易触发MajorGC
根据使用的垃圾收集器不同,收集算法也不同;通常为FullGC;
如:MajorGC只有CMS收集器有,如果使用G1收集器,还会触发MixedGC;
2.3 堆的内存分配方式
JVM使用不同的垃圾收集器,堆内存的分配方式也不同:
1、指针碰撞:使用带有整理的算法收集器,采用此分配;堆内存规整;(Serial、Parallel) 2、空闲列表:使用基于清除的算法,采用此分配,有内存碎片,不规整;(CMS) 3、本地线程分配缓存(TLAB):G1
2.4 老年代空间担保机制
MinorGC触发时,会检测:
1、老年代空闲内存是否大于年轻代对象总和? 如果大于,则正常执行MinorGC,如果小于,则判断第二个条件:
2、老年代空闲内存是否大于之前每次MinorGC后进入老年代的总对象的平均大小 如果大于,则正常执行MinorGC,如果小于,则执行FullGC
3. 线程共享区:运行时常量池
常量池的目的:
- 避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
字符串常量池(堆中):String Pool;专门为字符串开辟的内存空间;在堆空间存放字符串对象的一张字符串表;
- 当需要创建字符串常量时,先看字符串常量池有没有该字符串;存在则使用;
- 不存在,则实例化要创建的字符串,并放入池中;
- 字符串常量池不会参与GC;常驻内存;
常量池的相关参数:
- -XX:+PrintStringTableStatistics:打印常量池信息;
- -XX:StringTableSize=60009:限制StringTable的bucket个数,99.9不需要此参数,设置太大太小,都会影响性能;
4. 线程共享区:方法区(Non-Heap)
方法区是虚拟机规范中定义的一个抽象概念,虚拟机规范中也并没有规定方法区一定要在堆内存中,不同的虚拟机的方法区有不同的实现方式;
Hotspot虚拟机的实现:
- 永久代(JDK7):堆内存;包含:类的元信息、常量池、静态变量;
- 元空间(JDK8):堆外本地内存;包含:类的元信息;(常量池、静态变量存放在堆中)
元空间工作方式
元空间存储:
- class文件常量池:已经被虚拟机加载的类的元数据信息:类名、方法、字段信息、字节码;
- [符号引用]就是字符串;
- 直接引用,可以被程序识别的内存地址;
- 运行时常量池:每个类都有一个运行时常量池;从class常量池中构建的运行时使用的常量引用和符号引用;
元空间特点
- 不占用JVM内存,占用本地内存
- 为每一个类加载器分配一块内存,此内存与对应的类加载器生命周期相同
- 元空间默认不会卸载Class,只会通过FullGC来回收元空间中不再使用的class信息(对应的类加载器已死)
5. 线程共享区:直接内存
- 不受JVM管理;
- 回收成本高,读写性能高,少一次内存拷贝;常用于数据缓冲,如NIO的ByteBuffer;
- 直接内存也会内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
直接内存不由GC管理,由
- 直接内存手动分配:unsafe.allocateMemory()
- 直接内存手动释放:unsafe.freeMemory()
当ByteBuffer被GC回收时,其使用直接内存也会被释放,但并不是GC回收了直接内存 直接内存的自动释放,是由一个
直接内存相关JVM参数
- 禁止使用此参数,会导致直接内存无法回收,会造成直接内存长时间得不到释放;
- 一定要手动释放内存,请使用Unsafe
6. 相关问题
3.1 为什么要分年轻代和老年代?
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
1、如果不考虑GC性能的话,完全不需要新生代,全部对象创建在一个区域,一起回收即可;
2、因为每次回收都需要遍历所有存活对象,对于生命周期长的对象而言,这种遍历是没有效果的,他们依旧存在
3.2 为什么用元空间代替永久代
即:为什么元空间不占用Heap内存?
1、类的元数据内存回收调优困难
2、程序运行过程中,动态加载频繁,如果动态加载类太多,容易OOM;不放入堆区,可以不限制其内存;