天天看点

JVM内存结构和垃圾回收GCJVM

文章目录

  • JVM
    • 外部
      • 类装载
    • 内部
      • 本地方法栈
      • PC寄存器
      • 虚拟机栈
      • 方法区(元空间)
      • 堆 heap
        • 新生代
          • Minor过程
        • 老年代
    • JVM调优
    • GC算法
      • GC识别垃圾
      • GC垃圾回收算法
      • 垃圾回收器
        • G1收集器
        • 并发标记扫描CMS收集器

JVM

JVM内存结构和垃圾回收GCJVM
注:方法区在1.8后为元空间。

外部

类装载

外部class文件装在入JVM 中的方法区。

类加载器:

  1. Bootstrap Class Loader 启动类加载器 C++编写
  2. Extension Class Loader 扩展类加载器 JAVA编写
  3. App Class Loader 应用加载器 加载当前classpath下的类
  4. Java.lang.ClassLoader的子类,用户可以定制加载类的方式

双亲委派模型:所有类的加载都会交给父类加载器。类似递归,从根加载器开始逐层向下加载。

这样保证了JAVA类的安全,避免了自己的编写的类污染java内部类,这就是沙箱安全机制

内部

本地方法栈

JAVA中有本地方法,一般由C++编写,用来和硬件交互,运行时在本地方法栈中。

PC寄存器

线程私有,记录了方法之间的调用和执行情况。 拥有很小的内存地址,指向下一条指令的地址。字节码解释器通过改变计数器的值来选取下一条要执行的指令。不会发生OOM。

虚拟机栈

线程私有,主管java程序的运行,在线程创建时创建,生命周期和线程一致,不存在垃圾回收。会出现SOF异常(StackOverFlowError),栈中的数据都是以栈帧的格式存在,(简单理解就是一个方法)是一个有关方法和运行时数据集的数据集。**每个方法被调用的同时会创建一个栈帧,用于存储,局部变量表、操作数栈、动态链接、方法出口等信息 **

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-km0m6Gpk-1585465596141)(E:\脱发大法\java\java图\栈帧2.png)]

栈帧中保存三类数据:

  • 本地变量 : 输入输出的参数以及方法内变量
  • 栈操作 : 记录入栈、出栈的操作
  • 栈帧数据 : 包括类文件、方法等

方法区(元空间)

线程共享,存放了类的结构信息,类加载器会把类文件加载入方法区。

方法区包括:运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。

同时还包括java程序运行所需要的必要环境(元数据)。

注意:jdk1.8中,方法去变为元空间,且元空间不在虚拟机中,而是使用本机物理内存。

在JVM规范中,逻辑上方法区属于堆。在不同的虚拟机中实现不同,例如永久代和元空间。但物理上与堆无关,别称 “非堆 ”。

堆 heap

堆逻辑上分为三部分:新生代(yong/new)、老年代(old/tenure)、永久代/元空间(perm)。物理上分为:新生代、老年代。元空间与堆相独立。

新生代

新生代分为三部分:

  • 伊甸区 (Eden)
  • 幸存区(Survivor-from)
  • 幸存者(Survivor-to)

from和to并不是固定的,GC之后,哪个为空哪个为to。三部分的比例为:8:1:1。

新生代默认占用堆内存的1/3。新生代发生 Minor GC/Young GC。

年轻代的特点为区域相对老年代较小,对象存活率低(朝生夕死)。适合使用复制算法GC。

Minor过程

复制-> 清空-> 互换

触发GC时,会扫描Eden和from,对两个区同时回收,将存活对象使用复制算法复制到Survivor-to区,然后清理掉原本的对象。最后,from和to角色互换(谁空谁为to),GC结束。默认情况下,对象经历15次过GC会进入老年代。(存活次数由JVM参数-XX:MaxTenuringThreshold控制)

老年代

默认占用堆内存的2/3。老年代发生Full GC/Major GC,老年代满了后会发生OOM错误。

老年代的特点是区域较大,对象存活率高。一般由标记清除和标记整理混合实现。

堆、栈、方法区的交互

HotSpot通过指针访问对象

JVM内存结构和垃圾回收GCJVM

JVM调优

调优常用参数:

JVM参数 功能
-Xms 设置堆的初始值,默认为物理内存的1/64(在正式环境中一般设置和最大值相同)
-Xmx 设置堆的最大值,默认为物理内存的1/4
-XX:+PrintGCDetails 输出详细的GC处理日志
-XX:+PrintHeapAtGC 每次GC前和GC后,打印堆信息
-XX:NewRatio 新生代(eden+2*s)和老年代的比值
-XX:SurvivorRatio 设置两个Survivor区和eden的比值
-XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件,根据这个文件,我们可以看到系统dump时发生了什么。

输出当前电脑CPU数量:Runtime.getRuntime().availableProcessors();

GC日志内容:

//young GC
[GC (Allocation Failure) [PSYoungGen: 1733K->32K(2560K)] 6877K->6022K(9728K), 0.0003299 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Full GC 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 5989K->3450K(7168K)] 6021K->3450K(9728K), [Metaspace: 3740K->3740K(1056768K)], 0.0037368 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

内容概要:
[GC名称  [GC位置:GC前占用内存:GC后占用内存(总内存)] GC前堆内存->GC后堆内存 GC时间] [时间:用户时间,系统时间,总时间]
           

GC算法

JVM在进行垃圾回收时(GC),并非每次都是对 新生代、老年代同时回收,大部分的GC发生在新生代。因此GC按照回收的区域分为两种:

  1. 局部GC:Minor GC
  2. 全局GC:Major GC/Full GC。 (速度比Minor GC慢十倍以上)
也有说法Major GC负责回收老年代,这个目前没有官方的定义,按照我的理解,发生Major GC前会发生Minor GC,也相当于一次完整的Full GC,所以可以等价把Major GC看作 Full GC

GC识别垃圾

  1. 引用计数法

    每被引用一次就计数器加一,无法解决循环引用情况。JVM不采用这种方式。

  2. 可达性分析法

    从GC root开始搜索,搜索走过的路径称为引用链。如果发现一个对象无法通过GC root到达,则标记这个对象。第一次被标记的对象,如果有必要执行finalize()方法,则将其放置在F-Queue队列中。之后交由虚拟机自动生成的finalize线程去执行。之后会对F-Queue队列中的对象进行二次标记。如果finalize()方法并没有拯救这个对象。则该对象被回收。注意,一个对象的finalize()方法只会被执行一次。

    可以作为GCRoot的对象有:

    • 虚拟机栈(栈帧中的局部变量区,也叫做局部表量表)中引用的对象
    • 方法区的类属性所引用的对象
    • 方法区中常量所引用的对象
    • 本地方法栈中JNI(Native方法)引用的对象。
    java中引用有四类,强引用(不回收)、软引用(发生OOM前回收)、弱引用(下次GC时回收)、虚引用(不回收,回收会通知,必须配合引用队列ReferenceQueue使用)

GC垃圾回收算法

  1. 复制算法 (Copying)

    Minor GC采用复制算法,将内存分为两块,每次只适用其中的一块,当这块内存使用完,则进行GC,将存活的对象复制到另一块内存。

    • 优点:不会产生内存碎片
    • 缺点:可用内存为原来的一半
  2. 标记清除法 Mark-Sweep
    JVM内存结构和垃圾回收GCJVM
    算法分为“标记-清除”两个阶段,先标出要回收的对象,然后统一回收。这个时候会暂停程序,产生停顿(stop the world)。
    • 优点: 省内存
    • 缺点:两次扫描耗时严重,会产生大量内存碎片
  3. 标记整理法 Mark-Compact

    算法分为“标记-移动-清理”三个阶段,先标记,标记之后不直接清理,而是将存活对象向一端移动,之后清理边界外的对象。会产生程序暂停,停顿(stop the world)。

    • 优点:生内存,没有垃圾碎片
    • 缺点:耗时严重

    老年代一般使用标记清除或者是标记清除与标记整理混合使用。

    三种算法对比:

JVM内存结构和垃圾回收GCJVM
  • 复制算法的效率只和当前存活对象的大小有关,因而很使用年轻代的回收。HotSpot中两个Survivor区域的设置也很好的解决了内存利用率不高的问题。
  • Mark阶段的开销与存活对象的数量成正比,当存货对象数量多时,可以使用多线程并发的形式提高标记效率
  • Sweep阶段的开销与所管理区域的大小成正比。Sweep就地解决对象的方式避免了对象的移动,所以效率较高,但是需要解决内存碎片的问题。
  • Compact阶段的开销与存活对象的数量、大小成正比。移动复制大的对象会有很大的开销。所以作为第一选择不合适

垃圾回收器

G1收集器

G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。可以认为是CMS的升级版。G1回收器将内存结构分成了一个个Region,维护了一个Region的列表,每次判断哪个Region的回收价值最大,便回收该Region。

过程:

  1. 标记阶段 初始标记,会发生停顿,并且触发一次 Mintor GC
  2. 根区域扫描Root Region Scanning 运行过程中会回收survivor区(存活到老年代)。在young GC之前完成。
  3. 并发标记Concurrent Marking 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。
  4. 再标记Remark 会有短暂停顿(STW)。用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);
  5. Copy/Clean up 多线程清除失活对象,会有STW。将回收区域的存活对象拷贝到新区域,并发清空回收区域并把它返回到空闲区域链表中。

特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,G1立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

并发标记扫描CMS收集器

并发垃圾回收器,在垃圾回收的过程中,应用还是可以继续运行。所以,这里的并发多指应用和垃圾回收的并发。同时CMS 也是多线程的,也即它也是Parallel(并行)的。CMS垃圾回收之后得到的空闲空间并不是连续,他会合并相邻的空闲空间。但是还是会产生连续空间,CMS允许整理空闲空间,可以使用参数 UseCMSCompactAtFullCollection指定。cms只会回收老年代和永久带(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled)

过程

  1. 初始标记 存在STW,标记一下GC Roots能直接关联到的对象,速度很快
  2. 并发标记 运行GC Roots Tracing的过
  3. 重新标记 存在STW,修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  4. 并发清除

特点:

并发收集、低停顿

产生大量空间碎片、并发阶段会降低吞吐量