JVM学习之旅

JVM学习之旅

  • 内存结构
  • 一、类加载子系统
    • 1. 作用
    • 2. 三个阶段
      • 1. 加载阶段(Loading)
      • 2. 链接阶段(Linking)
      • 3.初始化阶段(Initialization)
    • 3.类加载器
      • 1. 引导类加载器
      • 2.扩展类加载器
      • 3.系统类加载器
    • 4.双亲委派机制
      • 1. 前言
      • 2. 工作原理
      • 3. 优势
    • 5. 两个class对象是否为同一个类的两个必要条件
  • 二、运行时数据区
    • 1. 组成部分
    • 2. 程序计数器(PC寄存器)
    • 3. 虚拟机栈
      • 1. 可能出现的异常
      • 2. 栈中存储的内容
      • 3. 栈帧的内部结构
        • 1.局部变量表
        • 2. 操作数栈
          • 2.1 栈顶缓存技术
        • 3. 动态链接
        • 4. 方法返回地址
        • 5.附加信息(可选的,了解即可)
      • 4. 本地方法栈
      • 5. 堆【共享区域】
        • 1. 前言
        • 2. 堆的细分结构
          • 2.1 年轻代与老年代
        • 3.Minor GC、Major GC、Full GC
        • 4.内存分配策略
        • 5.TLAB
        • 6.逃逸分析
        • 7. 代码优化
      • 6. 方法区【共享区域】
        • 6.1 存储内容
  • 三、执行引擎
    • 1.任务
    • 2. 编译和执行过程介绍
    • 3.热点代码及探测方式
    • 4.JIT编译器
    • 5.AOT编译器
  • 四、垃圾回收
    • 1. 垃圾回收相关算法
      • 1. 标记阶段
        • 1. 引用计数算法
        • 2. 可达性分析算法
      • 2. finalization机制
      • 3. 清除阶段
        • 1. 标记--清除算法
        • 2. 复制算法
        • 3. 标记--压缩算法
        • 4.分代收集算法
        • 5. 增量收集算法
    • 2. 垃圾回收的相关概念
      • 1. 内存溢出与内存泄漏
        • 1. 内存溢出(OOM)
        • 2. 内存泄漏
      • 2. 并行与并发
        • 1. 并发
        • 2. 并行
      • 3. 安全点和安全区域
        • 1. 安全点
        • 2. 安全区域
      • 4. Java中的几种同引用(强软弱虚)
    • 3. 垃圾回收器

内存结构

JVM学习之旅_第1张图片

一、类加载子系统

1. 作用

  • 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行由Execution Engine决定
  • 记载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

2. 三个阶段

  1. 加载阶段:引导类加载器【用于加载Java核心类库】,扩展类加载器【从java.ext.dirs指定的路径下加载类库;或者从JDK安装目录的jre/lib/ext目录下加载类库。】,系统类加载器【负责加载环境变量classpath或系统属性java.class.path指定的类库】
  2. 链接阶段:验证,准备,解析
  3. 初始化阶段

1. 加载阶段(Loading)

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

2. 链接阶段(Linking)

  1. 验证
    - 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
    - 主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
  2. 准备
    - 为类变量分配内存并且设置该类变量的默认初始值,即零值。
    - 这里不包含用fianl修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
    - 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到Java堆中。
  3. 解析
    - 将常量池内的符号引用转换为直接引用的过程。
    - 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
    - 符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
    - 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。

3.初始化阶段(Initialization)

  • 初始化阶段就是执行类构造器方法()的过程
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量(static修饰的变量)的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • ()不同于类的构造器。(关联:构造器是虚拟机视角下的())
  • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
  • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。

3.类加载器

  • JVM支持两种类型的类加载器,分别为引导类加载器和自定义类加载器
  • 所有派生于抽象类ClassLoader的类加载器都称为自定义类加载器
  • 引导类加载器–>(扩展类加载器–>系统类加载器–>自定义的类加载器)【2-4都可以称为自定义的,因为都继承ClassLoader】
//获取系统类加载器
ClassLoader sysLoader = ClassLoader.getSytemClassLoader();

//获取其上层:扩展类加载器
ClassLoader extLoader = sysLoader.getParent();

//再获取上层【引导类加载器不是java写的(c/c++)】
ClassLoader bootLoader = extLoader.getParent(); //值是null,代表没办法获取到引导类加载器

//获取自定义类的加载器:默认使用系统类加载器进行加载
ClassLoader classLoader = myMethod.class.getClassLoader();

//String类使用引导类加载器进行加载的-->Java的核心类库都是使用引导类加载的
ClassLoader stringLoader = String.class.getClassLoader(); //值是null

1. 引导类加载器

  • c/c++实现的
  • 用来记载Java的核心库
  • 并不继承java.lang.ClassLoader,没有父加载器
  • 加载扩展类和系统类加载器,并指定为他们的父类加载器
  • 只加载报名为java,javax,sun等开头的类

2.扩展类加载器

  • Java语言编写
  • 派生于ClassLoader类
  • 父加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

3.系统类加载器

  • Java语言编写
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
  • 可以通过ClassLoader.getSystemClassLoader()方法获取到该类加载器

4.双亲委派机制

1. 前言

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

2. 工作原理

  • 如果一个类加载器收到类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

3. 优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    • 例如:自定义类:java.lang.String

5. 两个class对象是否为同一个类的两个必要条件

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

二、运行时数据区

1. 组成部分

  • 所有线程共享的区域
    • 方法区
    • 堆区
  • 每个线程私有的
    • 程序计数器
    • 本地方法栈
    • 虚拟机栈

2. 程序计数器(PC寄存器)

定义:
  JVM中的PC寄存器是对物理PC寄存器的一种物理模拟
作用:
  用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
特点:
  没有GC(垃圾回收),不会出现OOM(内存溢出)
并行和并发:
   并行:同一时刻,两个线程都在执行。
   并发:同一时刻,只有一个执行,但是一个时间段内,两个线程都执行。

3. 虚拟机栈

定义:
  每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。
生命周期:
   生命周期和线程一致。
作用:
  主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

1. 可能出现的异常

  • Java栈的大小是动态的或者是固定不变的
  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线城市时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

2. 栈中存储的内容

  • 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

3. 栈帧的内部结构

1.局部变量表
  • 局部变量表也称之为局部变量数组或本地变量表。
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  • 局部变量表最基本的存储单元是Slot(变量槽)
    • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
2. 操作数栈
  • 每一个独立栈帧中除了包含局部变量表以外,还包含一个后进后出的操作数栈,也可以称之为表达式栈。
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
  • Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
2.1 栈顶缓存技术

将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的效率。

3. 动态链接
  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
4. 方法返回地址

调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址

5.附加信息(可选的,了解即可)

例如:对程序调试提供支持的信息

4. 本地方法栈

  • 一个本地方法(Native Method)就是一个Java调用非Java代码的接口。
  • 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
  • 关键字:native

5. 堆【共享区域】

1. 前言
  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
  • 堆内存的大小是可以调节的
  • 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
2. 堆的细分结构
  • Java 7 之前堆内存逻辑上分为三部分:新生区+养老区+永久区
  • Java 8 之后堆内存逻辑上分为三部分:新生区+养老区+元空间
  • 元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
  • 默认分配为比例为:新生待 :老年代=1 :2
  • \Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
  • \Xmx用于表示堆区的最大内存
2.1 年轻代与老年代
  • 分为三部分:Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
  • Eden : Survivor0 : Survivor1 = 8 : 1 : 1
  • -XX:SurvivorRatio可以调整空间比例。例如:-XX:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的
  • -Xmn可以设置新生代最大内存大小(一般使用默认值就可以了)
3.Minor GC、Major GC、Full GC

针对HotSpot Vm的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集:不是完整收集整个java堆的垃圾收集。
    • 新生代收集(Minor GC/Young GC): 只是新生代的垃圾收集【时间短,会引发STW(暂停其他用户的线程,等垃圾回收结束,用户线程恢复运行)】
    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集【时间长,速度慢,STW时间长】
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集【开发或调优中尽量要避免的。】
4.内存分配策略
  • 优先分配到Eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保:-XX:
5.TLAB
  • 产生原因
    • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
    • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
    • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
  • 定义
    • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
    • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因为我们可以将这种内存分配方式称为快速分配策略
6.逃逸分析
  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸。例如作为调用参数传递到其他地方中。
  • 没有发生逃逸的对象,可以分配到栈上,随着方法执行的结束,栈空间就被移除。
7. 代码优化
  1. 栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
public class StackAllocation{
	public static void main(String[] args){
		long start = System.currentTimeMillis();
		for(int i=0;i<100000;i++{
			alloc();//直接在栈上分配空间而不是在堆上
		}
		long end = System.currentTimeMillis();
		System.out.println("花费的时间为:" + (end - start) + "ms");
		try{
			Thread.sleep(100000);
		}catch(InterruptedException e1){
			e1.printStackTrace();
		}
	}
	
	private static void alloc(){
		User user = new User();//未发生逃逸
	}
}
  1. 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于整个对象的操作可以不考虑同步。
public void f(){
	Object hollis = new Object();
	synchronized(hollis){
		System.out.println(hollis);
	}
}

//优化后
public void f(){
	Object hollis = new Object();
	System.out.println(hollis);
}
  1. 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
public static void main(String[] args){
	alloc();
}
private static void alloc(){  //未发生逃逸
	Point point = new Point(1, 2);
	System.out.println("point.x="+point.x+";point.y="+point.y);
}
class Point{
	private int x;
	private int y;
}

//优化后
private static void alloc(){
	int x = 1;
	int y = 2;
	System.out.println("point.x="+x+";point.y="+y);
}

6. 方法区【共享区域】

  • 方法区看作一块独立于Java堆的内存空间。
  • 逻辑上的东西,是JVM的规范,所有虚拟机必须遵守。
  • 用于存储类的信息、常量池、方法数据、方法代码等。
6.1 存储内容

类型信息、常量、静态常量、即时编译器编译后的代码缓存等

  1. 类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息
    1. 这个类型的完整有效名称(全名=包名.类名)
    2. 这个类型的直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
    3. 这个类型的修饰符(public, abstract,final的某个子集)
    4. 这个类型直接接口的一个有序列表
  2. 域信息[属性信息]:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
  3. 方法信息:方法名称,方法的返回值类型,方法参数的数量和类型,方法的修饰符,方法的字节码、操作数栈、局部变量表及大小,异常表

三、执行引擎

1.任务

执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

2. 编译和执行过程介绍

解释器:对字节码采用逐行解释的方式执行

  • 程序源码–>词法分析–>单词流–>语法分析–>抽象语法树–>指令流(可选)–>解释器–>解释执行

JIT编译器:将源代码直接编译成和本地机器平台相关的机器语言

  • 程序源码–>词法分析–>单词流–>语法分析–>抽象语法树–>优化器(可选)–>中间代码(可选)–>生成器–>目标代码

3.热点代码及探测方式

热点代码:
  一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”。
探测方式:
  基于计数器的热点探测
基于计数器的热点探测方式:
  方法调用计数器:用于统计方法的调用次数。
  回边计数器:用于统计循环体执行的循环次数。

4.JIT编译器

  • -client:指定Java虚拟机运行在Client模式下,并使用C1编译器;C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度
  • -server:指定Java虚拟机运行在Server模式下,并使用C2编译器;C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高

5.AOT编译器

  • 即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。
  • AOT编译指的是在程序运行之前,便将字节码转换为机器码的过程。

四、垃圾回收

1. 垃圾回收相关算法

1. 标记阶段

在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

1. 引用计数算法

算法思想
  对每个对象,保存一个整型的引用计数器属性。用于记录对象被引用的情况。
优点
  实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点
  1. 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
  2. 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
  3. 引用计数器有一个严重的问题,即无法处理循环引用的情况。【这个是最严重的不足】

2. 可达性分析算法

算法思想
  1. 可达性算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径为引用链。
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
  4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots包含的元素种类
  1. 虚拟机栈中引用的对象。例如:各个线程被调用的方法中使用到的参数、局部变量等。
  2. 本地方法栈JNI(通常说的本地方法)引用的对象。
  3. 方法区中类静态属性引用的对象。例如:Java类中的引用类型静态变量。
  4. 方法区中常量引用的对象。例如:字符串常量池里的引用。
  5. 所有被同步锁synchronized持有的对象。
  6. Java虚拟机内部的引用。例如:基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器。
  7. 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

2. finalization机制

调用时机
  垃圾回收此对象之前
作用
  用于在对象被回收时进行资源释放。
finalization机制导致虚拟机中的对象一般处于三种可能的状态
  1. 可触及的:从根节点开始,可以到达这个对象。
  2. 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
  3. 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
判断一个对象objA是否可回收,至少要经历两次标记过程
  1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法
    ①. 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
    ②. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
    ③. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被以处“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就说,一个对象的finalize方法只会被调用一次。

3. 清除阶段

1. 标记–清除算法

算法流程
  1. 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象在Header中记录为可达对象。
  2. 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
JVM学习之旅_第2张图片
缺点
  1. 效率不算高。
  2. 在进行GC的时候,需要停止整个应用程序,导致用户体验差。
  3. 这种方式清理出来的空闲内存不是连续的,产生内存碎片。需要维护一个空闲列表。

2. 复制算法

算法思想
  将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
JVM学习之旅_第3张图片
优点:
  1. 没有标记和清除过程,实现简单,运行高效
  2. 复制过去以后保证空间的连续性,不会出现"碎片"问题
缺点:
  1. 需要两倍的内存空间
  2. 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

3. 标记–压缩算法

算法流程:
  1. 第一阶段和标记–清除算法一样,从根节点开始标记所有被引用对象。
  2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后清理边界外所有的空间。
JVM学习之旅_第4张图片
【相当于标记–清除算法之后又进行了内存碎片整理】
优点:
  1. 消除了标记–清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  2. 消除了复制算法当中,内存减半的高额代价。
缺点:
  1. 从效率上来说,标记–整理算法要低于复制算法。
  2. 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  3. 移动过程中,需要全程暂停用户应用程序。即:STW

4.分代收集算法

算法思想:
   不同生命周期的对象可以采取
年轻代:
   区域相对老年代较小,对象生命周期短,存活率低,回收频繁。
   复制算法
老年代:
   区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。
   标记–清除或者是标记–清除与标记–压缩的混合实现。

5. 增量收集算法

算法思想:
   每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
   总的来说,增量收集算法额基础仍然是传统的标记–清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
   造成系统吞吐量的下降。

2. 垃圾回收的相关概念

1. 内存溢出与内存泄漏

1. 内存溢出(OOM)

定义:
   没有空闲内存,并且垃圾收集器也无法提供更多内存。

2. 内存泄漏

定义:
   对象不会再被程序用到,但是GC又不能回收他们的情况。

2. 并行与并发

1. 并发

定义:
   指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
特点:
   并发并不是真正意义上的"同时进行",只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。

2. 并行

定义:
   当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行。
特点:
   其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。

并发 并行
多个事情,在同一时间段内同时发生了 多个事情,在同一时间点上同时发生了
多个任务之间互相抢占资源 多个任务之间是不互项抢占资源的

3. 安全点和安全区域

1. 安全点

定义:
   程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为”安全点“。

2. 安全区域

定义:
  指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。

4. Java中的几种同引用(强软弱虚)

  • 强引用:最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj = new Object()"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉弱引用关联的对象。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3. 垃圾回收器

你可能感兴趣的:(java,java,JVM)