JVM——JAVA运行时数据区、本地方法接口、执行引擎、垃圾回收

运行时数据区

组成概述

java运行时数据区,不同的虚拟机实现可能略微有所不同,但都会遵从java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行区域。

堆、方法区(元空间)主要用来存放数据 是线程共享的
程序计数器、本地方法栈、虚拟机栈是运行程序的,是线程私有的。
  1. 程序计数器

JVM中的程序计数器不是CPU中的程序计数器,可以简单理解为计数器。

是一块非常小的内存空间,运行速度是最快的,不会出现内存溢出情况。线程私有的。

作用:记录当前线程中的方法执行到的位置。以便于CPU在切换执行其他任务时,记录程序执行的位置,回来继续执行。

在运行时数据区中唯一一个不会出现内存溢出的区域。

  1. 本地方法栈

当我们在程序中调用本地方法时,会将本地方法加载到本地方法栈中执行。

也是线程私有的,如果空间不够,也会出现栈溢出错误。

  1. 虚拟机栈

出现背景:Java为了移植性好(跨平台)所以将运行程序的设计架构为栈结构运行,而不是依赖于CPU的寄存器架构。也正是因此现,其性能下降,实现同样功能需要更过的指令集.

栈是运行时的单位(加载方法运行)
堆是存储的单位 (存储对象的)

作用:运行方法,一个方法就是一个栈帧,栈帧中包含(局部变量(基本类型、引用地址)方法地址、返回地址)

栈的特点:是一种快速有效的分配存储模式,访问熟读仅次于程序计数器。
JVM直接对Java栈的操作只有两个:调用方法,(进栈) 执行结束后(出栈)。

对于栈来说不存在垃圾回收问题。

栈的异常:StackOverflowError(栈溢出)
递归调用方法太多。

栈中存储方法运行时需要的数据

栈的运行原理:第一个方法被加载 入栈 在方法中调用了其他方法,其他方法入栈,方法运行结束后出战,把结果返回给下一个要运行的方法。

栈帧的结构:

  • 局部变量表:方法参数、定义的局部变量、基本类型直接存值,引用类型存地址。
  • 操作数栈:栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。程序中的所有计算过程都是在借助于操作数栈来完成的。
  • 动态链接:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
  • 方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
  1. java堆
    概述
  • 堆是JVM内存中核心的区域,用来存储创建出来的对象。
  • 堆空间在JVM启动时被创建,大小可以设置。
  • 物理上是不连续的,逻辑上是连续的。
  • 堆中会发生垃圾回收。

堆内存的区域划分

  • 新生代(新生区)

新生代分为:伊甸园区:幸存者0:幸存者1=8:1:1

  • 伊甸园区(新生成的对象存储)
  • 幸存者0(from)
  • 幸存者1 (to)
  • 老年代(老年区)

为什么要分区?

把不同的生命周期的对象存储在不同的区域,这样不同的区域可以使用不同的垃圾回收算法。可以提高垃圾回收的效率。

对象在堆内存中存放的过程:

  • 新建的对象 存放在伊甸园区,第一次垃圾回收时,垃圾对象直接被回收掉,存活下来的对象会被存放到幸存者0区或幸存者1区

  • 再次垃圾回收时会把幸存者0区(1区)存活的对象移动到幸存者1区(0区),然后将幸存者0区(1区)清空,依次交替执行。

  • 每次保证有一个幸存者区域是空的,内存是完整的。

  • 当对象经过15(阈值)次垃圾回收后,依然存活的对象将会被移动到老年区(老年区垃圾回收的频率会比较低)

这里说明一下最大为15次的原因:

  • 在对象头中,它是由4位数据来多GC年龄进行保存的,所以最大值为1111也就是15。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
堆各区域的占比:

新生代默认占整堆的1/3
新生代中的 伊甸园区:幸存者0区:幸存者1区=8:1:1
对象经过15次垃圾回收后,依然存活的,将被移动到老年区。

一般所说的JVM优化 就是调整JVM相关各区的参数。

堆中的参数设置参考文献
分代收集思想Minor GC、Major GC、Full GC

一般情况下收集新生代 Minor GC/Yong GC
当老年代的空间不足时 会触发Major GC/Old GC
整堆收集 Full GC

整堆收集触发的条件:

  • System.gc();时
  • 老年区空间不足
  • 方法区空间不足

开发期间应该避免整堆收集。(在垃圾回收时,会有STW stop the world 回收时停止其他的线程的运行)

TLAB机制

  • TLAB线程本地分配缓存区
  • 在多线程情况下,可以在堆空间中通过-XX:UseTLAB设置 在堆空间中为线程开辟一块空间,用来存储线程中产生的一些对象,避免了空间竞争,提高了分配效率。

字符串常量池

JDK7之前字符串常量池在方法区(永久代)中存储。
JDK7及以后的版本将字符串常量池放到了堆空间。因为方法区只有触发了Full GC时才会回收,而在程序中需要大量的使用字符串,所以将字符串常量池的位置改变到了堆中,可以及时的回收无效的字符串。

  1. 方法区
    概述
  • 方法区也是一块内存空间,逻辑上属于堆,为了区分,称为元空间(JDK8之后)。
  • 主要用来存储类的信息。
  • 在JVM启动时创建,大小可以分配。
  • 如果加载的类太多,也会报内存溢出错误。
  • 是线程共享的

在这里插入图片描述


public class Demo2 {
    public static void main(String[] args) {
        String temp = "world";//字符串是常量,值存储在字符串常量池中
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = temp + temp;
            temp = str;
            str.intern();//将字符串存储到字符串常量池中
        }
    }

在这里插入图片描述

在这里插入图片描述

  • 方法区的大小可以通过-XX:MetaspaceSize设置。
  • 方法区在windows中默认的大小是21MB。
  • 如果达到21MB会触发Full GC 因此可以将其值设置大一些,减少Full GC的触发。
  • 方法区中主要回收运行时常量池类的信息
  • 类的信息卸载(回收)条件是比较苛刻的。要满足三个条件:
    1. 该类以及子类的对象没有被引用。
    1. 该类的类加载器被卸载。
    1. 该类的Class对象也没有被引用。

方法区的内部结构
在这里插入图片描述


本地方法接口

  1. 什么是本地方法接口
  • 一个Native Method 就是一个Java调用非Java代码的接口。
  • 非Java语言实现的,例如C/C++。
  1. 为什么要使用本地方法?
  • 我们的Java程序,需要与外部(计算机硬件)进行数据交互(例如:hashCode read() start() )
  • 可以直接调用外部的本地方法实现。
  • JVM解释器是用C写的,可以更好的与本地方法进行交互。

执行引擎

概述

  • 前端编译(.java---->.class)
  • 字节码 不等于 机器码。
  • 需要JVM将字节码加载到内存中。
  • 需要通过执行引擎将字节码解释/编译成机器码。
  • 后端编译(.class—>机器码)

执行引擎机制:

  • 解释器:将字节码逐行解释执行。
  • JIT编译器(即时编译器):将字节码整体编译为机器码执行。

为什么JVM的执行引擎设计为半解释型,半编译型?

  • 逐行解释执行效率低。
  • JVM会针对使用频率较高的热点代码进行编译,并缓存起来。这样执行效率就会提高。
  • 虽然编译型执行效率高,但是编译需要消耗时间,所以JVM刚刚启动后可以通过解释器去解释执行代码,(编译器开始编译)之后再使用编译器编译执行,两者结合在一起,效果更好。

一张图片让你明白
在这里插入图片描述


垃圾回收

概述

  • 垃圾收集机制并不是Java语言首创的,但是又是Java的招牌,Java可以自动垃圾回收。

垃圾回收:

什么是垃圾?

  • 垃圾是指运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。

为什么要GC?

  • 垃圾如果不及时清理就会越积越多,可能会导致内存溢出。
  • 垃圾多了,内存碎片较多。(例如数组,就需要连续的内存空间)

回收哪些区域:

  • 频繁回收内存。
  • 较少回收方法区。
  • 栈、本地方法栈、程序计数器没有垃圾回收。

早期是手动回收不被使用的对象,例如C++。
Java语言是自动垃圾收集的。

垃圾回收机制:

自动内存管理:

  • 无需开发人员手动参与内存的分配与回收,这样降低内存泄漏内存溢出的风险,将程序员从繁重的内存管理中释放出来,可以个更加专注于业务开发。

自动收集的担忧

  • 自动回收方便了程序员的开发,但是降低了程序员处理内存问题的能力。
  • 自动虽好,但还是应该了解并掌 握一些相关内存管理的知识。

java堆是垃圾回收的重点:
从次数上讲:

  • 频繁收集新生区。
  • 较少收集Old区。
  • 基本不收集原空间(原方法)。
    在这里插入图片描述

内存溢出与内存泄漏

  • 内存溢出:内存不够用了。
  • 内存泄漏:有些对象已经在程序中不被使用了,但是垃圾回收机制不能判断其为垃圾对象,因此不能将其回收掉,这样的对象越积越多,长久也会导致内存不够用。

例如:

  • 与数据库连接完成之后,需要关闭连接通道,但是没有关闭
  • IO读取完成后,没有关闭。

垃圾收集算法分为两大类:

  1. 垃圾标记阶段算法:
  • 主要判定哪些对象已经不再被使用,然后标记为垃圾对象。
  • 判断对象为垃圾的标准:不被任何引用指向的对象
  1. 垃圾回收阶段的算法:
  • 引用计数算法(在JVM中不被使用)
  • 如果有一个引用指向此对象,那么计数器加1,如果没有引用指向这个对象,那么计数器为0,此时就判定为垃圾。
  • 优点:方便使用,设计简介。
  • 缺点:增加了计数器的存储空间,计数需要消耗时间,会导致循环引用问题。(好几个对象之间,相互引用,但是没有其他的引用指向它们,此时垃圾回收不能回收它们,但是也没有引用指向,这样就造成了内存泄漏)。
    在这里插入图片描述
  • 可达性分析算法/跟搜索算法(Java目前所使用的垃圾标记算法)
  • 可以解决循环引用问题,设计简单,运行高效防止内存泄漏。
  • 思路:
  • 从一些活跃引用(GCRoots)根开始查找,如果对象被根直接或间接引用,那么此对象不是垃圾,否则标记为垃圾对象。
  • 哪些引用被用来当作根:
  1. 虚拟机栈中引用的对象(方法中引用的对象)。
  2. 本地方法栈中引用的对象。
  3. 静态变量所引用的对象。
  4. 方法区中常量引用的对象(例如:字符串常量池里面的引用)。
  5. 所有被同步锁synchronized持有的对象。
  6. java虚拟机内部的引用。
  • 总结:
    栈中引用的(正在使用的)方法区,常量池中(生命周期较长的),被synchronized当作锁的对象。

final、finally、finalize() 三者的区别?

  • final:关键字
  • finally:代码块
  • finalize() 是一个方法,它是Object类中的一个方法,在对象被最终回收之前调用,且只调用一次。

finalization机制
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

finalize()方法机制

  • java允许对象在销毁前去调用finalize()方法去处理一些逻辑,一般不用(不建议使用)。
  • 不要自己显示的去调用finalize()方法,在里面写代码一定要慎重。
  • .在 finalize()时可能会导致对象复活。
  • finalize()是由垃圾回收器调用的,没有固定的时间。
  • 一个糟糕的finalize()会严重的影响GC的性能。比如finalize()是一个死循环。

对象状态

  • 可触及的:从根节点开始,可以到达这个对象。(没有被标记为垃圾)
  • 可复活的:对象的所有引用都被诠释,但是对象有可能在finalize()方法中复活。确定为垃圾了,但是没有调用finalize()方法。
  • 不可触及的:对象的finalize()方法已经被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

代码演示

public class CanReliveObj {
    public static CanReliveObj obj;//类变量,属于GC Root


    //此方法只能被调用一次.
    @Override
    protected void finalize() throws Throwable {
       //  super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        //当前待回收对象在finalize()方法中引用链上的一个对象obj建立了联系.
        obj=this;
    }

    public static void main(String[] args) {
        try {
            obj=new CanReliveObj();
            //对象第一次拯救自己
            obj=null;
            //调用垃圾回收器,触发full GC 也不是调用后立刻就回收的,因为线程的执行权在操作系统
            System.gc();
            System.out.println("第一次GC");
            //因为finalizer线程的优先级很低,暂停两秒,以等待它
            Thread.sleep(2000);
            if (obj==null){
                System.out.println("obj is deal");
        } else{
                System.out.println("obj is still alive");
            }


            System.out.println("第二次 GC");
            //下面这段代码与上面的完全相同,但这次自救失败了.
            obj=null;
            System.gc();
            //因为Finalizer线程优先级很低,暂停两秒,以等待它
            Thread.sleep(2000);
            if (obj==null){
                System.out.println("obj is deal");
            } else{
                System.out.println("obj is still alive");
            }

    }
        catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

效果展示:
在这里插入图片描述


  1. 垃圾回收算法

     1. 标记清除算法
    

在这里插入图片描述

分为两个阶段:

  • 标记:标记可以从根可达的对象,标记的是引用的对象。
  • 清除:此清除并非直接讲垃圾对象清除掉,而是将垃圾对象的地址维护到一个空闲列表中,之后如果有新的对象产生,判断空闲列表中的对象空间能否存放的下新的对象。如果能放的下就覆盖垃圾对象。
  • 优点:简单,容易理解。
  • 缺点:效率低,会产生STW(在回收时,停止整个应用程序),会产生内存碎片。
     2.  复制算法:

在这里插入图片描述

  • 将内存分为大小相等的两块,每次只使用其中的一块儿区域即可。
  • 当回收时,将不是垃圾的对象复制到另一块内存中,排放整齐,然后再将原来的内存块清空。
  • 使用场景:在新生代中的幸存者0区与幸存者1区使用这种算法。
  • 优点:没有标记和清除过程,实现简单,运行高效。 复制过去之后保证了空间的连续性,不会出现内存碎片问题。
  • 缺点: 需要两倍的内存空间。
    3.标记-压缩算法

在这里插入图片描述

背景:

  • 复制算法需要移动对象,移动对象数量如果多的情况下,效率低。对于年轻代来说还是不错的。但是老年代中大量的对象是存活的,如果移动就比较麻烦,效率低。

实现:

  • 将存活对象标记出来,重新在本内存空间中排放位置。
  • 清除其他空间的垃圾对象。

标记-清除与标记-压缩对比

  • 标记-清除:不移动对象,不会把垃圾对象清除掉(维护在一个空闲列表中)
  • 标记-压缩:要移动对象,要清除掉垃圾对象
  • 优点:不会像标记-清除算法那样产生内存碎片也不会像复制算法那样需要两倍的内存空间
  • 缺点:效率相对较低,对象位置移动后需要重新设置对象的地址。也会有STW(在回收时,停止整个应用程序)。
		4.小结

效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
在这里插入图片描述

     5.分代/分区收集

为什么分代收集?

  • 由于对象的生命周期的长短不同,讲不通的对象存储在不同的区域,针对不同的区域进行分区收集,提高收集效率。

年轻代里面:

  • 年轻代区域相对老年代小,对象生命周期短、存活率低,回收频繁。【一般使用复制算法速度最快】

老年代里面:

  • 老年代区域较大,对象生命周期较长、存活率高,回收没有新生代频繁。【复制算法明显不合适,一般由标记-清除算法或标记-压缩算法混合实现】。

分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

垃圾回收中相关概念

    System.gc()的理解
  • 调用System.gc()方法,会触发FULL GC(整堆收集)。但是不一定调用后回立刻生效,因为垃圾回收是自动的。
  • 一般情况下,不要在项目中显示的去调用。

内存溢出:内存不够用
内存泄漏:垃圾对象无法回收。

    Stop the World
  • Stop the World—>STW 在垃圾回收时,会导致整个应用程序停止。
  • 在标记垃圾对象时,需要以某一个时间节点上内存的情况进行分析(拍照、快照)。
  • 因为如果不停顿的话,内存中的对象不停的变化,会导致结果分析不准确。
  • 停顿是不可避免的,优秀的垃圾回收器尽可能减少停顿的时间。
    对象引用

JDK1.2版之后,将对象分为四个等级:强引用、软引用、弱引用、虚引用。

  • 强引用(有引用指向的对象)
    Object obj = new Object();
    obj引用创建的对象,那么此对象就是被强引用的。
    这种情况下即使内存不够用了,报内存溢出,也不会回收。

软引用

  • 当内存足够使用时,先不回收这一类对象,当虚拟机内存不够用时,要回收此类对象。

弱引用

  • 此类对象只能生存到下次垃圾回收之前,只要发生垃圾回收,就会回收此类对象。

虚引用

  • 发现即回收。

垃圾回收器

  • 比较底层,了解垃圾回收器的一些种类及实现。
  • 垃圾回收器(具体实现垃圾回收的收集器名称)

垃圾收集器分类

按照线程数区分,可以分为串行垃圾回收器和并行垃圾回收器
按照工作模式区分,可以分为并发式垃圾回收器和独占式垃圾回收器。
按照工作内存区间区分,可以分为年轻代垃圾回收器与老年代垃圾回收器。

垃圾收集器的性能指标

吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)

垃圾收集开销:垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

收集频率:相对于应用程序的执行,收集操作发生的频率。

内存占用:Java 堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

常见的垃圾收集器

  1. Serial 垃圾收集器(单线程)
    特点:单线程、简单高效,采用复制算法,对于限定单个 CPU 的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
    应用场景:适用于 Client 模式下的虚拟机

  2. Serial Old 垃圾收集器(单线程)
    Serial Old 是 Serial 收集器的老年代版本。
    特点:同样是单线程收集器,采用标记-整理算法。
    应用场景:主要也是使用在 Client 模式下的虚拟机中。也可在 Server 模式下使
    用。

  3. ParNew 垃圾收集器(多线程)
    ParNew 收集器其实就是 Serial 收集器的多线程版本。
    除了使用多线程外其余行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
    特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在CPU 非常多的环境中,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题。
    应用场景:ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 收集器外,唯一一个能与 CMS 收集器配合工作的。

  4. Parallel Scavenge 垃圾收集器(多线程)
    Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。
    但是两者有巨大的不同点:
    Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算,。

  5. Parallel Old 垃圾收集器(多线程)
    是 Parallel Scavenge 收集器的老年代版本。
    特点:多线程,采用标记-整理算法。
    应用场景:注重高吞吐量以及 CPU 资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值