#JVM内存结构
JVM内存空间分为5个部分:
- 程序计数器(PC)
- Java虚拟机栈
- 本地方法栈
- 堆
- 方法区
其中,程序计数器、本地方法栈、Java虚拟机栈时线程隔离的,堆区和本地内存时线程共享的。
##程序计数器(PC)
保存当前正在执行的字节码指令地址,程序代码每执行完成一条,PC就指向下一个需要被执行的字节码。
生命周期同线程生命周期
##Java虚拟机栈
Java虚拟机栈是描述Java方法运行过程的模型。
Java虚拟机栈为每个即将运行的Java方法创建一块区域(栈帧),栈帧用于保存方法运行过程中的信息。这些信息包括(局部变量表、操作数栈、动态链接、方法返回地址),当方法运行过程中需要创建局部变量时,就将局部变量的地址的值存入栈帧中的局部变量表中。Java虚拟机栈栈顶的栈帧是当前正在执行的活动栈,保存当前正在执行的方法的相关信息,PC也会指向这个地址。
栈的大小是JVM在编译时就确定的,不可变
每一个栈帧保存以下信息:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
局部变量表
存放局部变量的表,一个数字数组,用于保存方法参数,方法体局部变量,基本数据类型,对象引用,returnAddress等。
局部变量表大小以槽(shot)为单位,64位数据占用两个槽(long、double),32位占用一个槽。
操作数栈
保存计算过程中的中间结果,计算过程中变量临时存储空间
动态链接
指向运行时常量池的方法引用
方法返回地址
方法正常退出或异常退出的地址
压栈与出栈
栈顶总是当前正在执行的方法,当当前执行的方法调用另一个方法时,另一个方法会入栈执行。当一个方法执行完,该方法出栈。如果该方法有返回值,那么该方法的返回值变成新方法栈帧操作数栈中的一个操作数。
##本地方法栈
本地方法栈时JVM为Java中Native方法准备的空间。同Java虚拟机栈一样,运行时会创建栈帧。
##堆
用于保存对象的空间,(几乎)所有对象都存放在堆中。
整个堆分为三个部分,新生代和老年代和元空间,新生代又分为三个部分(Eden、From Survivor、To Survivor)
年轻代
对象创建的地方。年轻代被分为三个区域,Eden,survivor0和survivor1区,比例8:1:1。当年轻代空间已满时,会触发MinorGC。
MinorGC:(to区一直为空,如果to区有from区没有,就叫唤to区和from区)
- Eden区满,执行MinorGC,将Eden区幸存对象转移到survivor0区
- 在survivor0区执行MinorGC,检查survivor0区,再次将幸存对象转移到survivor1区
- 经过15次循环,仍然幸存的对象会被移动至老年代
老年代
年轻代存活次数较多的对象,当老年代空间满时,触发MajorGC
堆的特点:
- 线程共享,整个JVM只有一个堆,所有线程共同访问这一个堆。
- 垃圾回收的主要场所。
对象的分配过程
- 逃逸分析
- 根据逃逸分析的结果,判断对象是否可以在栈上生成
- 判断是否为大对象
- 如果是大对象,则在Eden区生成
- 判断是否在TLAB中分配
- TLAB(线程本地分配缓存区)
逃逸分析
将线程私有的对象在栈上生成,该对象在线程结束时自动销毁,不需要使用GC,同时栈上分配速度更快,提高性能,但栈内存大小有限,无法存放大对象。
当对象没有逃逸时,该对象在栈上生成同时该对象也拥有以下特性:
- 锁消除
如果编译器确定该对象只在当前线程使用时,会消除该对象的同步锁。
- 标量替换
如果一个对象不逃逸,则不用创建该对象,只需要在栈上创建该对象的成员标量。
TLAB(Thread Local Allocation Buffer)
JVM为每个线程分配的私有缓冲区域,在Eden区中。
##方法区
JDK7以前方法区在堆中,又称永久代,JDK8方法区在本地内存中的元空间。
方法区保存以下信息:
- 类信息
- 常量池
- 静态变量
- 即时编译器编译后的代码
方法区线程共享,方法区中的信息需要永久保存,所以方法区又称为永久代。
方法区中有运行时常量池
Java8 变化
- 移除了永久代(PermGen),替换为元空间(Metaspace);
- 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
- 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
- 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中
##直接内存(堆外内存)
#垃圾回收
##判断是否需要被回收
引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器+1,引用失效时计数器-1,当引用计数为0时,此时该对象不被引用,可以判定为垃圾进行回收。循环依赖的对象引用计数永远不为0,引用计数法无法回收产生循环依赖的对象。
可达性分析法
以GC Roots(根对象)为起点进行搜索,能够到达的对象存活,不可到达的对象被回收。
##垃圾回收算法
标记清除
标记存活对象,清除未标记对象。
标记清除容易产生不连续内存碎片,导致无法为大对象分配内存
标记整理
标记存活对象,将存活对象向内存的一端移动,清除剩余未标记的对象。
复制算法
将内存空间分为两块,每次创建对象时只使用一块区域,当一块区域满之后,将这块区域上存活的对象全部复制到另一块内存,清除一块内存中所有对象。
复制算法在hotspot虚拟机上的应用在于,将堆内存分为了三分Eden区和两个survivor区,比例为8:1:1。每次使用Eden区和一个survivor区,内存利用率90%。垃圾回收时,先将Eden区和survivor0区对象复制到另一个survivor1区,在清除Eden区和survivor0区。
分代收集
堆内存被分为年轻代和老年代,在年轻代使用复制算法,在老年代使用标记清除和标记整理算法。
##MinorGC、MajorGC、FullGC
- 部分收集(MinorGC和MajorGC)
- 分为MinorGC和MajorGC,区域在年轻代和老年代
- 整堆收集(FullGC)
- 在堆和方法区收集
FullGC触发条件:
- 调用了
System.gc()
方法 - 老年代空间不足
- 空间分配担保失败
#内存分配策略
- 对象优先在Eden区分配:Eden区空间不足时进行MinorGC
- 大对象在老年代生成
- 长期存活对象移动到老年代:对象在Eden区每经过一次MinorGC年龄+1,年龄到达一定则移动到老年代
- 动态判定对象年龄:如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代
- 空间分配担保:执行MinorGC前,确定老年代最大连续可用空间是否大于Eden区所有存活对象大小总和,再执行MinorGC,如果不是则执行FullGC
#类的加载过程
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
- 加载:查找并加载类的二进制数据
- 验证:确保被加载的类正确
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 准备:静态变量分配内存,并赋值
- 解析:将常量池内的符号引用替换为直接引用
- 初始化
#类加载器
- 启动类加载器:Bootstrap ClassLoader
- 扩展类加载器:Extension ClassLoader
- 应用程序类加载器:Appliacation ClassLoader
#类加载机制
- 全盘负责:当加载某个类时,同时加载该类所依赖和引用的类
- 父类委托:优先使用父类加载器,父类加载器无法使用,再尝试从自己类路径中加载类
- 缓存机制:缓存所有加载过的类,要使用某个类时,先从缓存中获取,没有在加载
- 双亲委派机制:加载一个类时,将加载请求委托给父加载器完成,依次向上,直到传递到定增启动类加载器中。如果父加载器未找到需要的类,无法完成加载时,再使用子加载器加载。
##双亲委派机制加载过程
- 当AppClassLoader加载一个class时,将类加载请求委托给父类加载器ExtClassLoader。
- ExtClassLoader加载class时,将类加载请求委托给BootStrapClassLoader(启动类加载器)。
- 如果BootStrapClassLoader加载失败,使用ExtClassLoader加载。
- 如果ExtClassLoader加载失败,使用AppClassLoader加载。如果失败则报出ClassNotFoundException。