JVM相关知识——内存分布和垃圾回收机制

目录

 1.JVM的概念

1.1JVM执行流程

2.JRE/JDK/JVM之间的关系

3.有关JVM的经典问题

3.1  JVM的内存布局

3.1.1内存布局中的异常问题

3.2类加载机制

3.2.1类加载的流程(5个)

3.3类加载机制(双亲委派机制)

3.4 垃圾回收

3.4.1垃圾回收的概念

3.4.2垃圾回收的内存有哪些

3.4.3 如何找到垃圾回收的对象

3.4.4如何回收垃圾


 1.JVM的概念

JVM :Java虚拟机( Java Virtual Machine )
虚拟机是:通过软件模拟的具有完整硬件功能的、运行在一个完全隔离环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:


1. VMwave、VirtualBox是通过 软件 模拟物理CPU的指令集,物理系统中有很多寄存器。
2. JVM(解释器)则是通过软件模拟Java字节码的指令集,JVM主要保留了PC寄存器,其他的寄存器都进行了裁剪。

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键

1.1JVM执行流程

1.程序执行前,把java代码转换成字节码(class文件),JVM 首先把字节码通过类加载器(ClassLoader)加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部 分的职责与功能

JVM相关知识——内存分布和垃圾回收机制_第1张图片

 JVM 主要分为以下 4 个部分,来执行 Java 程序

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

2.JRE/JDK/JVM之间的关系

  • JDK(Java Development Kit):Java开发工具包,程序开发者用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
  • JRE(Java Runtime Environment):Java运行环境,所有的Java程序在JRE下才能运行。
  • JVM(Java Virual Machine):Java虚拟机,支持跨平台。(JVM的作用:将字节码class文件解释为机器码文件。)

关系:JDK包含JRE  JRE包含JVM

Java运行步骤:java源码—javac编译器—>字节码文件—Java解释器—>机器码文件。


3.有关JVM的经典问题

JVM相关知识——内存分布和垃圾回收机制_第2张图片

3.1  JVM的内存布局

详细可参考文章:深度理解JAVA中的栈、堆、对象、方法区、类和他们之间的关系

JVM 运行时 数据区域也叫内存布局(分为:堆、栈、程序计数器、方法区)和 Java 内存模型(JMM)完全不同; JVM本质是一个Java进程,JVM启动后就会从操作系统处申请到一块内存

JVM相关知识——内存分布和垃圾回收机制_第3张图片

  •  堆:存放new的对象(即成员变量)和数组(特殊的对象)
    • 1. 存储的全部是对象,每个对象包含一个与之对应的class信息(得到操作指)
      2. jvm只有一个堆区(heap)被所有线程共享,只存放对象本身。
      3. 堆的优势:可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。
      4. 缺点:由于要在运行时动态分配内存,存取速度较慢。
  • 方法区:存放类对象(字节码class文件(二进制文件,就是一些指令)被JVM加载到内存,就成了类对象),类的static成员也是类属性(静态变量)(类对象包含:类各种属性的名字、类型、访问权限;类的各种方法的名字、参数类型、返回类型、访问权限、方法实现的二进制代码)
  • 程序计数器:存放内存地址(下一步执行指令的地址),是内存区域中最小的部分
  • 栈: 存放局部变量 (方法下的变量) 

每个线程 都有自己的   栈  和  程序计数器

本地方法栈:JVM内部的方法

虚拟机栈:给上层java代码使用的

JVM相关知识——内存分布和垃圾回收机制_第4张图片

 

(1)如下代码所示:变量t在方法下面,属于局部变量,存在于栈里

JVM相关知识——内存分布和垃圾回收机制_第5张图片

(2)如下代码所示:变量t是 成员变量,存在于堆里

JVM相关知识——内存分布和垃圾回收机制_第6张图片

(3)如下代码所示:变量t是 静态成员变量(类对象),存在于方法区

JVM相关知识——内存分布和垃圾回收机制_第7张图片


3.1.1内存布局中的异常问题

(1)堆溢出:

堆用于存储对象实例(成员变量),只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。

典型场景:无限递归(堆容量不够)

  • 内存泄漏 : 泄漏对象无法被GC
  • 内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大;或者检查对象的生命周期是否过长。

(2)栈溢出:

  • 如果线程请求的栈深度 > 虚拟机所允许的最大深度,抛出StackOverFlow异常
  • 如果虚拟机在拓展栈时无法申请到足够的内存空间,抛出OOM异常

3.2类加载机制

Java中的 类加载 是JVM中一个很核心的流程,就是将字节码class文件站换乘JVM中的类对象。

 一个类的生命周期如下图(7个):

JVM相关知识——内存分布和垃圾回收机制_第8张图片

类加载:.class文件(编译器生成)---->类对象的过程

3.2.1类加载的流程(5个)

(1)加载 Loading 阶段,Java虚拟机需完成:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构------>方法区的运行时数据结构
  •  内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

(2) 验证
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,确保其不会危害虚拟机自身的安全。

(3) 准备

是正式为类中定义的变量(静态变量,static修饰的)分配内存设置类变量初始值

(4)解析

是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程(也即是:初始化常量

(5)初始化

Java 虚拟机真正开始执行类中编写的 Java 程序代码,初始化阶段就是执行类构造器方法的过程。(初始化静态变量、static静态代码块的执行,在对象实例化之前完成)

Java 虚拟机的角度,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):用 C++ 语言实现,是虚拟机的一部分;
  • 其他所有的类加载器:由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

3.3类加载机制(双亲委派机制)

JVM是三层架构,类的加载是通过双亲委派模型(类加载器之间的层次关系)来完成 :

这个过程也即是定义三个目录的优先级:标准库>>扩展库>>自定义库

  • 启动类加载器Bootstrap:负责加载java标准库中的一些类:Java_HOME/lib 目录中的类库
  • 扩展类加载器ExtClassLoader:加载JVM扩展库涉及的类:Java_HOME/lib/ext 目录的类库
  • 应用程序类加载器AppClassLoader:加载程序员自己写的类:用户路径上的类库
  • 自定义的类加载器:通过继承 java.lang.ClassLoader 实现

JVM相关知识——内存分布和垃圾回收机制_第9张图片

​​​​​如果一个类加载器接收到类加载的请求(从应用程序类加载器AppClassLoader开始,触发类加载),AppClassLoader把请求委派给父加载器去完成(直至走到Bootstrap,他无法再向上层继续委派,只能在自己的目录下寻找符合的类,若找到就进行加载)若Bootstrap无法挖完成加载请求时,子加载器才会尝试自己去加载这过程中有的加载请求最终都会传送到启动类加载器中


双亲委派模型有什么好处:

  • 确保安全,避免Java核心类库被修改;
  • 避免重复加载
  • 保证类的唯一性.

3.4 垃圾回收

3.4.1垃圾回收的概念

注意!!!  回收内存(死亡对象的回收),释放内存(因为内存是有限的)

程序在使用内存时,才能申请内库存空间,不使用了则需要释放,确保后续进程有足够的空间

内存泄漏: 程序一直申请内存,但不释放,导致内存越来越少,直至耗尽,此时其他进程若想在申请内存,就申请不到。这种现象成为内存泄漏

C++手动回收内存:申请内存的人负责释放内存

java垃圾回收机制:无论谁申请内存,由一个固定的角色(JVM)统一来释放内存

  1)垃圾回收机制的优点:

  • 能够很好地保障不出现内存泄露的情况(但不是100%)

  2)垃圾回收机制的缺点:

  • 需要消耗额外的系统资源
  • 内存是方存在延时(内存不用之后,JVM不是里面就释放,可能会间隔一个延迟时间)
  • 可能导致出现STW(stop  the  world)问题: 当前进程不能正常工作了,只能停下来,由JVM先把内存释放完之后才能继续进行(延时的时间过长)

3.4.2垃圾回收的内存有哪些

(垃圾回收的对象:一般指  堆 )

内存包含四部分:堆、方法区、程序计数器、栈

  •  程序计数器、栈和具体的线程绑定在一起,会进行自动释放(代码块/线程结束时,内存就释放)
  • 堆、方法区就是垃圾回收的内容(尤其是堆); 而方法区里是“类对象”,是通过“类加载”得到的,对方法取得垃圾回收,相当于“类卸载”

对于堆上的内存,具体回收的内容是???

不使用的成员变量

整个堆的内存分布:

JVM相关知识——内存分布和垃圾回收机制_第10张图片

 堆上,存放的是new·出来的成员变量对象,包括三种:

  • 完全要使用的对象
  • 完全不使用:(回收)
  • 一般要使用,一般不使用

3.4.3 如何找到垃圾回收的对象

先找出垃圾,再回收

如何找出垃圾??标记垃圾??判定垃圾??

(1)引用计数

给对象增加一个引用计数器,每当有一个地方引用它时,计数器+1;当引用失效时,计数器就-1;任何时刻  计数器为0 的对象 不再被使用,即对象已"死"。利用引用,判断对象是否“已死

JVM中 不选用 引用计数法 来管理内存,因为引用计数法无法解决对象的循 环引用问题

如下代码所示,两个引用均指向null,则说明该对象没有引用了(被认为是垃圾,引用计数是0,可以回收):

Test a = new Test();
Test b = a;
a = null;
b = null;

(2)可达性分析

从一组初始位置(GCRoot)出发,向下进行深度遍历,把所有能访问到的对象标记成“可达”(可以被访问到),而不可达的对象(没有标记到)就是垃圾。判断对象是否“存活”

JVM中存在  一个/一组 线程 来周期性的遍历 进行 可达性分析(找出不可达的对象进行垃圾回收

初始位置GCRoot可从如下3种位置出发:

  • 栈上的局部变量表中的引用
  • 常量池里面的引用指向的对象
  • 方法区中,引用类型的静态成员变量

3.4.4如何回收垃圾

经典的3种垃圾回收方法:

(1)标记-清除

  上图引入了额外的问题:内存碎片(空闲内存、正在使用的内存是交替出现的,若申请大块连续内存空间可能会分配失败),内存碎片累积会导致:

  • 效率问题 : 标记和清除这两个过程的效率都不高
  • 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

(2)复制算法

可解决内存碎片的问题它将  可用内存 按容量划分为  大小相等的两块(每次只使用一块),当需进行垃圾回收时,将此区域存活着的对象复制到另一块上面,然后再清理掉使用过的内存。

每次都是对整个半区进行内存回收,内存分配时不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。

JVM相关知识——内存分布和垃圾回收机制_第11张图片

  • 可用空间只有一半
  • 若回收对象较少,需要复制的就很多,开销就很大

(3)标记-整理

解决复制算法空间利用率较低的问题,(类似顺序表删除元素,把不需要回收的对象放到需要删除的地方),开销比赋值算法更大


(4)分代回收

 通过内存中的区域划分,实现不同区域和不同的垃圾回收策略;

  • 根据对象存活周期的不同将内存划分:把Java堆分为:新生代和老年代。
  • 新生代中(存放刚new的对象):每次垃圾回收都有大批对象死去,只有少量存活,采用复制算法;
  • 老年代中对象存活率高、没有额外空间进行分配担保,采用"标记-清理"或者"标记-整理"算法。

JVM相关知识——内存分布和垃圾回收机制_第12张图片

  •  一个新new的对象存放于新生代;经历过一轮GC还存活的对象放于幸存区
  • 幸存区中的对象会经过多次GC,每一次没被回收的对象都通过  复制算法  拷贝到另外的生存区
  •  在生存区中经历多次GC仍存在就把它拷贝到老年代
  • 进入老年代,JVM认为该对象是持久存在的(GC扫描频率就降低了)

如果对象特别大也会存放于老年代(因为新生代会多次拷贝,会导致开销很大)

你可能感兴趣的:(jvm,java,开发语言)