前言
昨天咱们说了类加载器、本地方法栈、程序计数器、方法区、今天来看看剩下的虚拟机栈、堆、以及垃圾回收器,还是放个JVM的结构图~
虚拟机栈
简介
虚拟机栈负责代码的运行,也时候也叫做局部变量表,它是Java 方法执行的内存模型。会对局部变量、方法等进行存储,总的来说,就是负责存储八大基本类型、对象的引用、实例的方法。
栈的生命周期和线程同步,线程结束,栈内存就释放,所以对于栈来说,不存在垃圾回收。
栈的异常
① StackOverflowError
线程请求的栈的深度大于虚拟机栈的最大深度(递归死循环)
② OutOfMemoryError
虚拟机随着扩展会不断地申请内存,当无法申请足够内存,就会出现这个错误,HotSpot 虚拟机不支持栈动态扩展,所以只有在创建线程申请内存时因为无法获得足够的内存才会导致 OutOfMemoryError异常
【拓展】
有三种 JVM :
Hotspot:Sun 公司 HotSpot java Hotspot™64-Bit server VM
BEA JRockit
IBM 39 VM
我们主要用的是第一种 JVM
栈的运行
从上面的 JVM 体系结构也可以看出栈中的数据是以栈帧的形式存在的,栈帧包含了方法的局部变量表、操作数栈、动态链接信息、方法的返回地址等信息。可以把栈帧看作一个方法,一个方法的调用到执行结束,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
在这个例子中,main()方法先被调用,被压入栈底,然后依次调用 testA() 、testB(),等这个线程执行完以后,testB()会先被弹出,然后是 testA()、main(),如果 testB()里面还会调用 testA(),testA()又会调用 testB(),即出现死循环,导致栈溢出错误。
【注】每执行一个方法都会产生一个栈帧,程序正在执行的方法,一定在栈的顶部 !
堆
Heap 区,一个JVM中只有一个堆内存,堆内存的大小是可调节的。类加载器读取了类文件后,会把类、方法、常量、变量以及所有引用类型的真实对象放到堆中。
堆内存可以分为以下三部分:
- 新生区 Young Generation Space (Young/New)= 伊甸园区 + 幸存区(0、1)
- 养老区 Tenure generation space (Old/Tenure)
- 永久区 Permanent Space (Perm)
【扩展】
永久区的发展史:
jdk1.6之前 :永久代,常量池在方法区
jdk1.7 :永久代,慢慢退化,去永久代,常量池在堆中
jdk1.8之后 :永久代更名为元空间,无永久代,常量池在元空间(MetaSpace),区别在于 MetaSpace 不存在 JVM中,使用的是本地内存
新生区
类诞生、成长、甚至死亡(不会全死)的区域。
- 伊甸园 (Eden):堆的起始区域,用于存放新创建的对象实例
- 幸存者区 (Survivor 0 & Survivor 1):两者一直在进行动态交换,其中一部分为To Space,另一部分为 From Space。To Space 区永远空闲。
当我们 new 出一个对象,会先被放到 Eden 划分出来的一块内存空间中,当 Eden 空间满了之后,会触发 Minor GC(轻量级垃圾回收,发生在新生区),存活下来的对象移动到 Survivor 0 区,当进行垃圾回收时,存活的对象将被移动到空闲区中,非存活的对象将被回收。经过多次的 Minor GC 后仍然存活的对象会移动到老年区。
【拓展】
这里的存活判断条件为 15 次,因为 HotSpot 会在对象头中的标记字段里记录年龄,分配到的空间仅有 4 位,所以最多只能记录到 15
//-XX:MaxTenuring Threshold = 5 通过这个参数可以设定进入老年代的时间
老年区
老年区用于存放长时间存活的对象和年龄大于一定阈值的对象。当老年区满时,将触发年老代的重垃圾回收(Major GC或Full GC),期间会停止所有线程等待 GC 的完成。所以对于响应要求高的应用应该尽量去减少发生 Full GC 从而避免响应超时的问题。
若老年区执行了 Full GC 之后仍然无法进行对象保存的操作,就会产生虚拟机堆内存不足的OOM 异常(java.lang.OutOfMemoryError:java heap space ),原因如下:
①堆内存设置过小,可通过参数-Xms、-Xmx 来调整。
②代码中创建的对象大且多,且一直在被引用导致长时间不能被垃圾收集器收集。
【拓展】
参数名称 含义 默认值 说明 -Xms 初始堆大小 物理内存的 1/64(<1GB) 默认(MinHeapFreeRatio 参数可以调整)空余堆内存小于 40%时,JVM 就会增大堆直到-Xmx 的最大限制 -Xmx 最大堆大小 物理内存的 1/4(<1GB) 默认(MaxHeapFreeRatio 参数可以调整)空余堆内存大于 70%时,JVM 会减少堆直到 -Xms 的最小限制
永久区
常驻内存区域,用来存放JDK自身携带的Class对象,Interface(接口数据)、元数据,存储的是Java运行时的一些环境或类信息,所以这个区域不存在垃圾回收,关闭VM(虚拟机)就会释放这个区域的内存。
若出现 OOM 异常(java.lang.OutOfMemoryError:PermGen space),原因如下:
① 一个启动类加载了大量的第三方jar包;
② Tomca部署了太多的应用;
③ 大量动态生成的反射类不断地被加载,导致Perm区被占满。
堆内存调优
线程共享数据区大小 = 新生区 + 老年区 + 永久区。
永久区一般为固定大小,如果在堆中增加新生区大小,那么老年区的大小就会减小,这样是不利于系统性能的,因为老年区过小会增加 Full GC(全局GC)。
JVM 的调优,主要就是集中在堆内存调优。常见的调优方式有下面几种:
调整最大堆内存和最小堆内存
根据应用程序的需求和服务器的物理内存情况,设置合适的堆内存大小,避免频繁的垃圾回收。实际开发过程中,通常会将 -Xms 与 -Xmx 两个参数配置成相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
//-Xms 设置初始化内存分配大小 1/64
//-Xmx 设置最大分配内存 默认1/4
//-XX:+ PrintGCDetails 打印GC垃圾回收信息
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError //获取OOM Dump 文件
调整新生区和老年区和幸存区的比值
根据应用程序的特性和内存使用模式,调整新生区和老年区的大小比例,以及幸存区的大小比例。
-XX:NewRatio //新生区(eden+2\*Survivor)和老年区(不包含永久区)的比值
例如-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整个堆的 1/5。在 Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置
-XX:SurvivorRatio //设置两个 Survivor 区和 eden 的比值
例如:8,表示两个 Survivor:eden=2:8,即一个 Survivor 占年轻代的 1/10
堆内存溢出的处理
前面在讲解老年区和永久区时都谈到 OOM 异常,实际就是堆内存溢出,可以通过以下手段来尝试处理:
① 尝试扩大堆内存并看结果
-Xms1024m -Xms1024m -XX:+PrintGCDetails // 打印GC垃圾回收信息
② 通过内存快照分析工具( MAT、Jprofiler)分析内存看哪个地方出现了问题:
MAT、Jprofiler作用:
① 分析Dump内存文件,快速定位内存泄漏
【注】dump文件是指程序产生异常时,用来记录当时的程序状态信息(例如堆栈的状态),用于程序开发定位问题的文件
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=输出的日志路径
② 获得堆中对象的统计数据
③ 通过树形结构展现对象之间的引用关系
GC(垃圾回收器)
用于检测和回收不再使用的内存资源,释放已经不需要的对象所占用的空间,确保内存资源的高效利用,防止内存泄漏和内存溢出问题。JVM在GC时,大部分回收都在新生代。
GC 分类
针对 HotSpot VM 的实现,GC 分类有两种:部分收集和全局收集。
部分收集 (Partial GC)
- 新生代收集(Minor GC / Young GC):只对新生区进行垃圾收集,每次GC后都会将Eden活的对象移到幸存区重,一旦Eden区被GC后,就会是空的。
- 老年代收集(Major GC / Old GC):只对老年区进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生区和部分老年区进行垃圾收集。
整堆收集 (Full GC)
收集整个 Java 堆和方法区。在发生全局回收的时候,JVM 会暂停应用程序的执行,即停止所有线程,再进行 GC 操作,当垃圾回收完成以后,才会恢复应用程序的执行,重新启动所有线程。这个过程相对耗时,会降低系统性能,所以应当减少 Full GC ! !
GC常用算法
引用计数器算法
引用计数算法(Reference Counting):为每个对象维护一个引用计数器,记录对象被引用的次数。当计数器为0时,表示对象不再被引用,可以被回收。但这种算法不能解决循环引用的情况,即对象之间相互引用,导致计数器无法归零。
目前虚拟机基本都是采用可达性算法,通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,那么整个连通图中的对象边都是活对象,对于GC Roots 无法到达的对象变成了垃圾回收对象,随时可被GC回收。
复制算法
复制算法(Copying):复制算法将内存划分为三个部分:Eden和两个大小相等的幸存区。在垃圾回收时,首先将所有存活对象从Eden区复制到一个幸存区中,然后清空Eden区。当一个幸存区被填满时,存活的对象将被复制到另一个幸存区中,同时清空原先的幸存区。当“To” 区被填满之后,会将所有的对象移动到老年代中。这种算法避免了内存碎片问题,但需要额外的复制操作。
优点:没有内存碎片
缺点:浪费了内存空间,多了一半空间永远是 To 区
复制算法最佳使用场景:对象存货率较低的地方(新生区)
标记清除法
标记清楚算法分为两个阶段:标记阶段和清除阶段。首先,从根对象出发,递归遍历所有可达的对象,并将其标记为存活对象。然后在清除阶段,遍历堆中的所有对象,回收未被标记的对象。这种算法可以解决循环引用的问题,但会产生内存碎片。
缺点:两次扫描严重浪费时间,会产生大量不连续的内存碎片
优点:不需要额外的空间
标记-整理算法
标记-整理算法(Mark and Compact):标记-整理算法是一种综合了标记-清除和复制算法的优点的算法。首先,标记阶段标记出所有存活的对象。然后进行压缩,将存活的对象向一端移动,紧凑排列,之后直接清理掉边界外的无效内存。这种算法消除了内存碎片,但需要额外的整理操作。
优点:不会产生内存碎片
缺点:多了一个移动成本,效率低
标记-整理算法最佳使用场景:对象存货率较低的地方(新生区)
【小结】
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率: 标记压缩算法 = 标记清除算法 > 复制算法
GC 分代收集算法
分代算法根据对象的生命周期采用不同的回收策略,提高垃圾回收的效率。
年轻代:区域小,存活率低—> 复制算法
老年代:区域大,存活率高 - >标记算法(内存碎片不是太多的)+ 标记压缩混合实现
写在后面
总算总结完毕,希望能够对你们有所帮助,总结有不对的地方,欢迎小伙伴们指出,我会及时更正!