JVM内存与垃圾回收
2021-07-19 15:26:50 0 举报
AI智能生成
Java Virtual Machine 深入了解
作者其他创作
大纲/内容
内存结构概述
虚拟机(Virtual Machine)
是什么?
就是一台虚拟的计算机,它是一款软件,用来执行一系列虚拟计算机指令,大体上,虚拟机可以分为 系统虚拟机和程序虚拟机
系统虚拟机
大名鼎鼎的Visual Box,Vmware 就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台
程序虚拟机
程序虚拟机的典型代表就是Java虚拟机,他专门为执行单个计算机程序而设定,在Java虚拟机中执行的指令被我们称为 Java字节码指令
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中
什么是Java虚拟机?
Java 虚拟机是一台执行Java 字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java 字节码也未必由Java 语言编译而成
JVM 平台的各种语言可以共享Java 虚拟机带来的跨平台性,优秀的垃圾回收器,以及可靠的及时编译器
Java的核心技术就是Java 虚拟机(Java Virtual Machine) 因为所有的Java程序都运行在Java 虚拟机内部
Java 虚拟机作用?
Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上机器指令,每一条Java指令,Java虚拟机规范中都有详细的定义,如怎么取操作数,怎么处理操作数,处理结果防在哪里等等
特点
一次编译处处运行
自动内存管理
自动内存回收
JVM 的架构模型
Java 编译器输入的指令流基本上是一种基于 栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构
栈的指令集架构
1), 设计和实现更简单,适用于资源受限的系统
2), 避开了寄存器的分配难题,使用零地址指令方式分配
3), 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小(8位),编译器容易实现
4), 不需要硬件支持,可移植性更好,更好实现跨平台
寄存器的指令集架构
1), 典型的应用是 X86的二进制指令集,比如传统pc以及android 的Davlik 虚拟机
2), 指令集架构完依赖硬件,可移植性差
3), 性能优秀和执行高效
4), 花费更少的指令(16位)去完成一项操作
5), 在大部分情况下,基于寄存器架构的指令集往往都是以1地址指令,2地址指令,3地址指令为主,而基于栈式架构的指令集却是以零地址为主
JVM的生命周期
虚拟机启动
Java 虚拟机的启动是通过引导类加载器(Bootstrap class loader ) 创建一个初始类(init class) 来完成,这个类是由虚拟机的具体实现指定的
虚拟机执行
一个运行中的Java 虚拟机有着一个清晰的任务,执行Java 程序
程序开始执行时他才运行,程序结束时他就停止
执行一个所谓的Java 程序的时候,真真正正在执行的是一个叫做Java 虚拟机的进程
虚拟机退出
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
某线程调用 RunTime 类或System 类的exit方法,或 RunTime类的 halt方法,并且Java 安全管理器也允许这次 exit 或halt 操作
除此之外,JNI (Java Native Inteface ) 规范描述了 JNI Invocation API 来加载或者卸载,Java 虚拟机时,Java虚拟机的退出情况
VM虚拟机
oracle 的HotSpot 虚拟机
oracle 的 JRockit
IBM 公司 J9
类加载器与类加载过程
类加载子系统作用
1), 类加载子系统负责从文件系统或网络中加载class文件,class文件在文件开头有特定的文件标识
2), ClassLoader 只负责 class文件的加载,至于它是否可以运行,则由 Execution Engine 决定
3), 加载的类信息存放于一块被称为方法区的内存空间,除了类的信息外,方法区中还会存放运行时常量信息,可能还包括字符串字面量和数字常量
类加载器的分类
1), Java 支持两种类型的类加载器,分别为 引导类加载器 (Bootstrap ClassLoader) 和 自定义类加载器( User Defined ClassLoader)
2), 从概念上讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类加载器,但是Java 虚拟机却不是这么定义的,而是 将所有派生与抽象类 ClassLoader 的类加载器都划分为 自定义类加载器
JVM虚拟机自带的加载器
启动类加载器(Bootstrap Class Loader)
这个类加载使用C/C++ 语言实现的,嵌套在JVM内部
它用来加载 java 的核心内库,(rt.jar, resource.jar 或 sun.boot.class.path ) 路径下面的内容,提供JVM自身需要的类
并不继承与java.lang.ClassLoader 没有父类加载器
加载 扩展类加载器 和 应用程序加载器 并指定它们的父类加载器
出于安全考虑,Bootstrap 启动类加载器只加载包名为 java , javax , sun 等开头的类
扩展类加载器 (Extension Class Loader)
java 语言编写,有Launcher 的内部类
派生于 ClassLoader类
父类加载器启动为启动类加载器
从 java.ext.dirs 系统属性所指定的目录中加载类库, 或从JDK 的安装目录的 jre/lib/ext 子目录(扩展目录)下,加载类库,如果用户创建的JAR放在此目录,也会由扩展类加载器加载
应用程序加载器(Application Class Loader)
java 语言编写,由Launcher 的内部类
派生于 ClassLoader类
父类加载器启动为 扩展类加载器
它负责加载环境变量 classpath 或系统属性,java.clas.path 指定路径下的类库
该类加载时程序中默认的类加载器,一般来说,Java应用类都是由它来完成
通过 classLoader#getSystemClassLoader() 方法可以获取该类加载器
用户自定义类加载器
在Java 日常开发中,类加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式
什么情况下需要自定义类加载器呢?
1), 隔离加载类
2), 修改类加载的方式
3), 扩展加载源
4), 防止源码泄漏
双亲委派机制
Java 虚拟机对class 文件采用按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成 class 对象,而且加载某个类的 class 时,Java 虚拟机采用的是双亲委派模式,即把请求交给父类进行处理,他是一种委派模式
双亲委派模式优势?
1), 避免类的重复加载
2), 保护程序安全,防止核心API 被恶意篡改
沙箱安全机制
自定义 java.lang.String 类 ,但是在加载自定义的 String 类时,会率先使用 BootStrap 加载器进行加载,而引导类加载器在加载的过程中会先加载JDK 自带的的文件,(rt.jar的java.lang.String) 报错信息说没有main 方法,就是因为加载 rt.jar 包中的String 类 这样既可以保证对java 核心源代码的保护,这就是沙箱安全机制
在 JVM中表示两个 class 对象是否为同一个类存在的两个必要条件
1), 类的完整类名必须一致,包括包名
2), 加载这两个类的 classLoader 必须相同
换句话说, 在JVM中, 即使这两个类对象来源于同一个Class 文件,被同一个虚拟机所加载,但只要加载它们的classLoader 实例对象不同,那么这两个类对象也是不相同的
对类加载器的引用
JVM必须知道一个类型是由启动类加载器加载还是用户类加载器加载的,如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中,当解析一个类型到另一个类型的引用的时候,JVM必须保证这两个类型的类加载器是相同的
类的主动使用和被动使用
主动使用分为以下7种方式
1), 创建类的实例
2), 创建某个类或接口的静态变量,或者对该静态变量赋值
3), 调用类的静态方法
4), 反射 ( Class.fornNme())
5), 初始化一个类的子类
6), java 虚拟机启动时被标记为启动类的类
7), JDK7 开始提供的动态语言支持
除了以上7种以外,其它使用Java 类的方式都被看做是对类 的被动使用,都不会导致类的初始化
运行时数据区概述及线程
内存是非常重要的系统资源,是硬盘和CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,JVM 内存布局规定了Java 在运行过程中内存申请,分配,管理的策略,保证了JVM的高效稳定运行,不同的JVM 对于内存的划分和管理机制存在着部分差异 (JRoict 与 J9 没有方法区 HotSpot 存在方法区 )
什么是线程?
1), 线程是一个程序里的运行单元,JVM 允许一个应用有多个线程并行的执行
2), 在HotSpot 虚拟机中,每个线程与操作系统的本地线程进行映射
3), 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用 Java 线程中的 run() 方法
如果你使用 jconsole 或者任何一个调试工具,都能看到在后台有许多线程在运行,这些后台线程不包括调用 main线程以及所有这个 main 线程自己创建的线程
这些后台系统线程在 HotSpot JVM里面主要是以下几个?
1), 虚拟机线程
2), 周期任务线程
3), GC线程
4), 编译线程
5), 信号调度线程
程序计数器 (PC Register)
概述
JVM 中的程序计数寄存器(Program Counter Register), Register 的名字源于CPU 的寄存器,寄存器存储的指令相关的现场信息,CPU 只有把数据装载到寄存器才能够运行
这里,并非广义上所指的物理寄存器,或许将其翻译为 PC计数器(或指令计数器)会更加贴切(也称为程序钩子) ,并非不容易引起一些不必要的误会,JVM 的PC寄存器是对物理PC寄存器的一种抽象模拟
介绍
它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
在JVM规范中,每个线程都有它自己程序计数器,是线程私有的,声明周期与线程的声明周期一致
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的Java 方法的JVM指令地址 或者 如果是在执行native 方法,则是未指定值(undefind)
它是程序控制流的指示器, 分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成
字节码解释器在工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
它是唯一一个在Java 虚拟机规范中没有规定任何 outofMemoryError 情况的区域
CPU时间片
CPU 时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片
在宏观上,我们可以同时打开多个应用程序,每个程序并不停止,同时在运行
在微观上,由于只有一个CPU,一次只能处理程序要求的一部分,如果处理公平,一种方式就是引入时间片,每个程序轮流执行
两个常见的问题
使用pc 寄存器存储字节码指令地址有什么用呢?
因为cpu 需要不停的切换各个线程,这时候切换回来之后,就要知道接着从哪开始继续执行
JVM 的字节码解释器就需要通过改变pc 寄存器的值来明确下一条应该执行什么样的字节码指令
pc 寄存器为什么会被设定为线程私有的?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个pc寄存器,这样一来各个线程便可以进行独立运算,从而不会出现相互干扰的情况
由于CPU 时间片限制,众多线程在并发执行的过程中,任何一个确定的时刻,一个处理或多个处理器中的一个内核,只会执行某个线程中的一条指令
这样必然导致经常中断或恢复,如何保证分毫无差呢? 每个线程在创建后,都会产生自己程序计数器和栈帧,程序计数器在各个线程之间互不影响
Java虚拟机栈
出现背景
由于跨平台性的设计,Java 的指令都是根据栈来设计的,不同平台CPU 架构不同,所以不能设计为基于寄存器
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
栈是运行时的单位,而堆是存储单位 ,即 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储问题,即数据该往哪里放
是什么?
每个线程在创建时都创建一个虚拟机栈,其内部都会保存一个栈帧,一个栈帧对应一个java方法
生命周期
与线程一致
作用
主管Java程序运行,它保存方法的局部变量,部分结果,并参与方法的调用与返回
优点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM 直接对Java 栈的操作只有两个
1), 每个方法执行,伴随着入栈和出栈
2), 执行结束后的出栈工作
对于栈来说不存在垃圾回收问题
开发中遇到的异常有哪些?
outofmemoryError(内存溢出)
stackOverflowError(栈溢出)
栈的存储单位
基本介绍
每个线程都有自己的栈,栈中的数据都是以栈帧(stack frame) 以基本单位进行存储的
在这个线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一块内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
运行原理
JVM 直接对Java 栈的操作只有两个,入栈 和出栈,遵循先进后出,后进先出的原理
在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧是有效的,这个栈帧被称为当前栈帧
与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
执行引擎运行的所有字节码指令只针对在当前栈帧进行操作
如果在该方法中调用了其它方法,对应的新栈帧会被创建出来,放在栈的顶端,成为新的当前栈
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另一个线程的栈帧
如果当前方法调用了其它方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
Java 方法有两种返回函数的方式,1), 正常函数返回,使用return 指令 2), 抛出异常,不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部架构
局部变量表 (Local Variables)
基本介绍
局部变量表也被称为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对应引用(Reference) 以及 returnA队的类型
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此 不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 code 属性的 maximum local variables 数据项中,在方法运行期间是不会改变局部变量 表的大小的
方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用的次数就越多
局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表来完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
Slot 的理解
参数值的存放总是在局部变量数组的index 0开始,到数组长度-1 的索引结束
局部变量表,最基本的存储单元是Slot(变量槽)
在局部变量表里, 32位以内的类型只占用一个 solt(包括 rerurnAddress类型),64 位的类型(long和double) 占用两个 solt
JVM 会为局部变量表中的每一个solt 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个solt上
如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可(比如long 或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this 将会存放在index 为0的solt处,其余的参数按照参数表的顺序继续添加
Slot 重复利用问题
栈帧的局部变量表中的槽位是可以重复重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量的对比
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域进行分配
类加载的过程中,在链接Linking 中准备阶段,执行初始化,对类变量设置初始值为0, 局部变量则是初始化阶段赋予程序员对定义的变量进行初始化
和类变量不同的是,局部变量表不存在系统初始化的过程,这意外着一旦定义了局部变量则必须认为的初始化,否则无法使用
补充
在栈帧中, 与性能调优关系最为密切的部分就是前面提到的局部变量表,在方法执行时,虚拟机使用局部变量表来完成方法的传递
局部变量表的变量也是重要的垃圾回收根节点,只要局部变量表中直接或间接引用的对象都不会被回收
操作数栈 (Operand Stack)(或表达式栈)
基本介绍
每一个独立的栈帧中除了包括 局部变量表之外,还包含一个先进后出的操作数栈,也可以称为表达式栈
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数,即入栈(push)和出栈(pop)
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就确定好了,保存在方法的code 属性中, 为max_stack的值
栈中的任何一个元素都是可以任意的Java 数据类型
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据的访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中的下一条需要执行的字节码指令
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段再次验证
另外,我们说JVM 的解释引擎是基于栈的执行引擎,其中栈指的是操作数栈
常见的 i++和 ++i的区别
栈顶缓存(Top of Stack Cashing) 技术
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑(8位字符) 但完成一项操作的时候必须引入更多的入栈和出栈指令,这同时也就意味着需要更多的指令分派次数和内存读写次数
由于操作系统时存储在内存中的,因此频繁地执行内存读写操作必然会影响执行速度,为了解决这个问题,HotSpot JVM的设计者提出了栈顶缓存技术
将栈顶元素全部缓存在物理CPU寄存器(16位)中,以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接 (或指定运行时常量池的方法引用)
基本介绍
每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
在Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class 文件的常量池里
比如描述一个方法掉用了另外其他方法时,就是通过常量池中指向方法的符号引用来完成,那么动态链接的作用就是为了将这些符号引用转换为方法的直接引用
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别
方法调用
在JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译器可知,且运行期间保持不变的话,这种情况下将调用方法的符号引用转换为直接引用的过程称之为 静态链接
动态链接
如果被调用的方法在编译期间无法确定下来,也就是说,只能够在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换的过程具备动态性,因此也被称之为动态链接
对应方法的绑定机制为: 早期绑定和晚期绑定,绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次
早期绑定 (Early Binding)
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期间保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是那个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
晚期绑定 ( Late Binding)
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定称之为晚期绑定
随着高级语言的横空出世, 类似于Java 一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别, 但是它们彼此之间都保持着一个共性,那就是支持封装,继承和多态等面向对象特征,既然这一类编程语言具备多态特征,那么自然也就具备这早期绑定和晚期绑定俩种绑定方式
Java 中任何一个普通方法的方法都具备虚函数(晚期绑定)的特征(Java 字节码 invokevirtual 表示 C++用 virtual 表示) 如果Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字 final 来标记这个方法
虚方法
非虚方法
如果方法在编译期间都确定具体的调用版本,这个版本在运行期时是不可变的,这样的方法称为 非虚方法
静态方法, 私有方法, final 方法, 实例构造器, 父类方法都是非虚方法
其它方法称之为 虚方法
虚拟机提供了一下几条方法调用指令
1), invokestatic : 调用静态方法,解析阶段确定唯一方法版本
2), invokespecial : 调用 <init> 方法,私有方法,及父类方法,解析阶段确定唯一方法版本
3), invokevirtual : 调用所有虚方法(晚期绑定)
4), invokeinterface : 调用接口方法
5), invokedynamic : 动态解析出需要调用的方法,然后执行
前 4条指令 固话在虚拟机内部,方法的调用执行不可人为干预, 而 invokedynamic 指令则支持由用户确定方法版本
invokestatic 指令 与 invokespecial 指令 调用的方法称为非虚方法,其余的(final修饰除外)称为虚方法
静态类型语言(Java)与动态类型语言区别
两者区别在于对类型语言的检查是在于编译器还是运行期间确定的,在编译器确定的就是静态类型语言,在运行时确定的就是动态类型语言
Java 语言中方法重写的本质
1), 找到操作数栈顶的第一个元素所执行的对象的实际类型,记做 C
2), 如果在过程结束,如果不通类型C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,如果没有查找到则返回 java.lang.IllegaAccessError 异常
3), 否则,继续继承关系从下往上依次对C 的各个父类进行第2步骤的查找和验证过程
4), 如果始终没有找到合适的方法,则抛出 AbstactMehtodError异常信息
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的话就可能会影响到执行效率
为了提高性能, JVM采用了在类的方法区建立一个虚方法表(virtual method table) 来实现,使用索引来代替查找
每个类中都有一个虚方法表,表中存放着各个方法的实际入口
那么虚方法表什么时候创建?
虚方法表在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成后,JVM会把类的方法也初始化完毕
方法返回地址 (方法正常退出或者异常退出的定义)
简介
存放调用该方法的pc 寄存器的值
一个方法的结束,有两种 1), 正常执行完成, 2), 出现未处理的异常,非正常退出
无论通过哪种方式退出, 在方法退出后都返回到该方法被调用的位置
方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法指令的下一条指令地址
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
当一个方法开始执行后,只有两种方式可以退出这个方法
1), 执行引擎遇到任何一个方法返回字节码指令(return) ,会有返回值传递给上层的方法调用者,简称 正常完成出口
2), 在方法执行的过程中遇到了异常(Exception) ,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索匹配的异常处理器,就会导致方法退出,简称 异常完成出口
本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者的操作数栈,设置pc寄存器的值等,让调用者方法继续执行下去
正常完成出口和异常完成出口的区别在于: 通过异常完成出口退出不会给他的上层调用者产生任何的返回值
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如 对程序调优调试提供支持的信息
面试题
举例栈溢出的情况?(StackOverFlowError)
通过调整栈的大小,就能保证不出现栈溢出?
分配的栈内存越大越好?
垃圾回收是否涉及到虚拟机栈?
方法中定义的局部变量表是否线程安全?
本地方法接口(Native Inteface)
什么是本地方法?
一个 Native Method 就是一个Java 调用非java代码的接口,一个 Native method 是这样一个java 接口,该方法的实现由非Java语言实现
表示符 native 可以与其它的java 标识符连用,但是 abstract 除外
为什么要使用 native method?
1), 与Java 环境外交互,有时java 需要与外面的环境交互,这是本丢方法存在的主要原因
2), 与操作系统交互,通过使用本地方法,我们得以使用 Java 实现 jre 的域底层系统交互,甚至JVM的一部分C写的
3), sun的解释器是C实现的,这使得它能像一些普通的C一样与外部交互
现状
目前该方法使用的越来越少了,除非是与硬件有关的应用
本地方法栈 (Native Method Stack)
简介
Java 虚拟机 用于管理Java 方法的调用,而本地方法栈用于管理本地方法的调用
本地方法栈,线程私有的
允许被实现固定或者是可扩展的内存大小(内存溢出方面是相同的)
本地方法是使用C 语言实现的
它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行加载本地方法库
当某个线程调用了一个本地方法时,它就进入了一个全新的并且不受虚拟机限制的世界,它和虚拟机拥有同样的权限
并不是所有的JVM都支持本地方法,因为Java 虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等,如果JVM产品不打算支持native 方法,也可以无需实现本地方法栈
在 HotSpot JVM中,直接本地方法栈和虚拟机栈合二为一
堆 (heap)
简介
一个JVM实例只存在一个堆内存,堆也是Java 内存管理的核心区域
Java 堆区在JVM启动的时候即被创建,其空间大小也确定了,是JVM管理的最大一块内存空间
<JVM规范> 规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
所有的线程共享Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer ,TLAB)
<JVM规范> 中对java 堆的描述是: 所有的对象实例以及数组都应当在运行时分配在堆上
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在方法结束后,对中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆是GC 执行垃圾回收的重点区域
Java 7 与Java 8堆的区别
Java7之前的内存逻辑上区分为 新生代+老年代+永久区
Java 8之后的内存逻辑上区分为 新生代+老年代+元空间
Java 堆区用于存储 Java 对象实例, 那么堆的大小在JVM启动的时候已经设定好了,大家可以通过选项 -Xms 与 -Xmx 来进行设置
一旦堆区中的内存大小超过 -Xms 所指定的最大内存时,将会抛出 (OutOfMemoryError)异常
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的就是为了能够在Java 垃圾回收器机制清理完堆区以后不需要重新分割计算堆区大小,从而提高性能
默认情况下 初始内存大小 : 物理电脑内存大小的/64
默认情况下,最大内存大小:物理电脑内存大小/4
如何查看使用gc情况
方式1
jps 查看程序运行端口
jstat -gc 端口
方式2
-XX: +PrintGCDetails
年轻代与老年代
年轻代( YongGen)
老年代 (OldGen)
年轻代与老年代参数怎么调整
默认 -XX:NewRatio=2 表示新生占1,老年代占2,新生代占整个堆的1/3
-XX:NewRatio=4 表示新生代1,老年代占4,新生代占整个堆的1/5
简介
在HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例为 8:1:1
当然开发人员可以使用 -XX:SurvivorRatio=value 来调整这个空间比例
几乎所有的Java 对象都在Eden中被new 创建出来的
绝大部分的Java 对象销毁都在新生代进行了
可以使用选项 -Xmn 设置新生代最大内存大小
对象分配过程
简介
为新对象分配内存是一种非常严谨和复杂的任务, JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片
过程
1), new 的对象先放入 Eden ,此区有大小限制
2), 当Eden 的空间填满时,程序又需要创建对象,JVM的垃圾回收器会对 Eden 进行垃圾回收(Minor GC) , 将Eden 中的不再被其它对象所引用的对象进行销毁,在加载新的对象放入 Eden中
3), 然后将Eden 中剩余的对象移动到 幸存者 0区
4), 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区,如果没有回收,就会放入到幸存者1区
5), 如果再次经历垃圾回收,此时会重新放入到幸存者0区, 接着再去幸存者1区
6), 啥时候能去养老区? 可以设置次数,默认是15次 -XX:MaxTenuringThreshold=N 来进行设置
7), 在养老区,相对悠闲,当养老区内存不足时,再次触发Major,进行养老区的内存清理
8), 若养老区执行了 Major,之后依然发现无法进行对象的保存,就会产生OOM异常
总结
针对幸存者0区和1区,复制之后有交换,谁空谁是TO
关于垃圾回收,频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集
常用调优工具有哪些
JDK命令行
Jconsole
VisualVM
Jprofiler
Java Flight Recorder
GCViewer
GC Easy
Minor GC ,Major GC 与Full GC区别
JVM在进行GC时, 并非每次都对上面三个内存(新生代,老年代;方法区) 区域一起回收的,大部分时候的回收都是指新生代
针对 HotSpot 虚拟机的实现,它里面的GC按照回收区域又分为两大类: 一种是部分收集(Partial GC) 一种是整堆收集(Full GC)
部分收集(MajorGC/old GC): 指不是完整收集整个Java 堆的垃圾收集器,其中又分为
新生代收集(Young GC/minor GC)
老年代收集(Major GC / old GC)
目前只有CMS GC 会有单独收集老年代的垃圾收集器
注意,很多时候,Major GC 和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收(包括方法区的回收)
混合回收( Mixed GC) : 收集整个新生代以及部分老年代的垃圾收集
整堆收集(Full GC): 收集整个java堆和方法区的垃圾收集器
年轻代GC(Young GC/Minor GC) 触发机制:
1), 当年轻代空间不足的时候,就会触发 Minor GC ,这里年轻代指的是 Eden满, Survivor 满则不会引发YGC(每次Minor GC 会清理年轻代的内存)
2), 因为java 的对象大多具备朝生夕死的特性, 所以 Minor GC 非常频繁,一般回收速度很快,这一定义即清晰又易于理解
3), Minor GC 会引发 STW(stop the world),暂停其它用户线程,等垃圾回收结束,用户线程才能恢复
老年代GC(Major GC /Full GC) 触发机制
1), 指发生在老年代的GC, 对象从老年代取消时,我们说 Major GC 或 Full GC 发生了
2), 出现 Major GC ,经常伴随至少一次的Minor GC (并非绝对)
3), Major GC 的速度一般是 Minor GC 的十倍以上,STW停留的时间更长
4), 如果 Major GC 后,内存还是不足,就报OOM异常
Full GC 触发机制
1), 调用 System.gc() 时,系统建议执行 Full GC 但是不必然执行
2),老年代空间不足
3), 方法区空间不足
4), 通过 Minor GC 后进行老年代的平均大小大于老年代的可用内存
5), 由Eden 区, Survivor S0区向 S1区复制时,对象大小2大于 To 空间可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明: Full GC 是开发或者调优中尽量避免的,这样暂停时间会短一些
堆空间分代思想
为什么需要把Java 堆分代?不分代就不能正常工作?
为什么需要把Java 堆分代? 不分代就不能正常工作了?
内存分配策略 ( 或 对象提升(Promotion)规则)
如果对象在 Eden出生并经过 Minor GC 之后,仍然存活,并且能被 Surivor 容纳的话, 将被移动到 Surivor 中,将对象的年龄设置为1, 对象在 Surivor 中每经过一次 MinorGC 对象年龄就会加1,当年龄超过15岁时,就会晋升老年代
优先分配到Eden
大对象直接存放到老年代 -- 尽量避免程序中出现过多的大对象
长期存活的对象分配到老年代
动态对象年龄判断
空间分配担保
为对象分配内存TLAB
为什么要有TLAB (Thread Local Allocation Buffer)?
1), 堆区是线程共享区域,任何线程可以访问堆区中的共享数据
2), 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
3), 为避免多个线程操纵同一地址,需要使用加锁机制,进而影响分配速度
什么是TLAB?
1), 从内存模型而不是垃圾收集的角度,对Eden 区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
2), 多线程同时分配内存时, 使用TLAB 可以避免一系列的非线程安全问题,同时还能够提高内存分配的吞吐量, 因此我们可以将这种分配方式成为 快速分配策略
3), 据我所知所有 OpenJDK 衍生出来的JVM 都提供了TLAB 设计
TLAB 说明?
1), 尽管不是所有的对象实例都能在 TLAB 中成功分配, 但JVM 确实是将TLAB 作为内存分配的首选
2), 在程序中, 开发人员可以通过设置 -XX:UseTLAB 是否开启TLAB空间
3), 默认情况下, TLAB 空间的内存非常小, 仅占有整个 Eden 空间的 1%, 当然我们可以通过选项 -XX: TLABWasteTargetPercent 设置TLAB 空间所占用 Eden空间的 百分比大小
4), 一旦对象空间分配内存失败时, JVM就会尝试通过 加锁机制来 确保数据操作的原子性, 从而直接在Eden 空间中分配内存
堆空间的参数设置
-XX:+PrintFlagsInitial : 查看所有的参数默认值大小
-XX:+PrintFlagsFinal : 查看所有参数的最终值
-Xms : 堆初始默认值大小(默认物理内存的1/64)
-Xmx: 堆最大空间大小(默认物理内存1/4)
-Xmn: 设置新生代的大小
-XX:NewRadio: 配置新生代与老年代在堆结构的占比
-XX:SurvivoRatio: 设置新生代中 Eden 与 S0/S1空间的比例
-XX:MaxTenuringThreshold : 设置新生代垃圾的最大年龄
-XX:+PrintGCDetails : 输出详细的GC处理日志
-XX: HandlePromotionFailure : 是否设置空间内存担保
1), 发生 YGC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果大于,此次YGC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure 设置的值是否允许担保失败
如果大于则尝试进行一次YGC 这次YGC依然会有风险
如果小于 则改为一次FUllGC
如果大于,此次YGC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure 设置的值是否允许担保失败
如果大于则尝试进行一次YGC 这次YGC依然会有风险
如果小于 则改为一次FUllGC
2), 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Major GC 否则进行Full GC
堆是分配对象存储的唯一选择?
在<深入理解java虚拟机>关于java 堆内存有这样一段描述:
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,素有的对象都分配在堆上显得不是那么绝对了
在java 虚拟机中,对象是在java堆中进行分配的,这是一个普遍的尝试,但是有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,那么它可能被优化成栈上分配,这样就无需堆上分配内存,也无需进行垃圾回收,这就是常见的堆外存储技术
此外,基于OpenJDK 深度定制的 TaoBaoVM ,其中创新的GCIH(GC invisible Heap) 技术实现了 off-heap ,将声明周期长的java 对象从heap中移至 heap 外,并且 GC不能管理GCIH 内部的java对象, 以此达到降低GC的回收频率和提升GC回收效率的目的
逃逸分析
概述
如何将堆上的对象分配到栈,需要使用逃逸分析手段
这是一种可以有效减少java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
通过逃逸分析,Java HotSpot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配在堆上
逃逸分析的基本行为就是分析对象动态作用域
1), 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
2), 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其它地方中
参数设置
在JDK7以后, HotSpot 中默认开启了逃逸分析
如果使用的是较早版本,开发人员可以通过
1), -XX:+DoEscapeAnalysis 显示开启逃逸分析
2), -XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果
代码优化
1),栈上分配
将堆分配转为栈上分配,如果一个对象在子程序中分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
2),同步省略
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
3),分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
开发中能使用局部变量的,就不要使用在方法外定义
方法区(Method Area)
方法区在哪里?
<java 虚拟机>说明: 尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩, 但对于 HotSpot JVM 而言,方法区还有一个别名交 no-Heap (非堆),目的就是要与方法区分开,所以方法区看做是一块独立于java 堆的内存空间
概述
1), 方法区 与 java 堆一样,是各个线程共享的内存区域
2), 方法区在 JVM启动的时候创建完成,并且它的实际物理内存区域和 java 堆区一样都可以是不连续的
3), 方法区的大小,根堆空间大小一样,可以选择固定大小或者可扩展
4), 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出的话 JDK8之前报 OutOfMemoryError: PermGen space , JDK8之后报 OutOfMemoryError:Metaspace
5), 关闭JVM就会释放这个区域
HotSpot 方法区
在JDK7之前,习惯上把方法区称为永久代,JDK8开始把永久代改为元空间
在JDK8后的改变完全废除了永久代的概念,改用与 JRockit ,J9 一样在本地内存中实现的元空间(Metaspace) 来代替
元空间的本质和永久代类似, 都是对JVM 规范中方法区的实现,不过元空间与永久代最大的区别在于 元空间不在虚拟机设置的内存中,而是使用本地内存
永久代,元空间 二者不只是名字变了,内部结构也调整了
根据<Java 虚拟机规范> 的规定,如果方法区无法满足新的内存分配需求时, 将抛出 OutOfMemoryError:Metaspace 异常
设置方法区/元空间 内存大小
JDK8之后, 元空间可以使用惨 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间大小
默认依赖于平台, windows 下 -XX:MetaspaceSize 是21M; -XX:MaxMetaspaceSize是-1 即没有限制
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据发生溢出,虚拟机一样会抛出OOM异常
jvm 体系结构概述
类装载器 ClassLoader
负责加载 class文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且 ClassLoader 只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
双亲委派机制
当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类进去完成,每一个层次类加载器都是如此,因为所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候,(在它加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar包中的类 java.lang.Object,不管是那个加载器加载这个类,最终都是委托给顶层的启动器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object类。
Native Inteface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须调用C/C++程序,于是就在内存中专门开辟了一块区域登处理标记为 native 的代码,它具体做法是 在 native Method Stack 中登记 native方法,在 Execution Engine 执行时加载 native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java程序驱动打印机或者 Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用 web Service等等,不多做介绍
PC寄存器又叫程序计算器(类似于排班值日表)
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条命令,是一个非常小的内存空间,几乎可以忽略不计的。
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条命令需要执行的字节码指令
如果执行的是一个 native 方法,那么这个计数器的值为 空的
用以完成分支,循环,跳转,异常处理,线程恢复等基础功能,不会发生内存溢出 (OutOfMemory错误)
方法区 (MethodArea)
供各线程共享的运行时内存区域,它存储了每一个类的结构信息,例如运行时常量池(runtime Constant pool),字段,和方法数据,构造函数和普通方法的字节码内容,上面讲的是规范,在不同的虚拟机里头实现是不一样的,最典型的是(1.7)永久代和(1.8)元空间
栈 (Java stack)--栈管运行
栈 也叫栈内存,主管 Java程序的运行,是在线程创建时创建,它的声明周期是跟随线程的声明期,线程结束栈内存也释放
对于栈来说,不存在垃圾回收问题,只要一个线程结束该栈就over声明周期和线程一致,是线程私有的,
8中基本数据类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配
栈存储了什么?
本地变量(Local Variables) : 输入参数 和输出参数 以及方法内变量
栈操作 (Operand Stack) : 记录入栈和出栈的操作
栈帧数据 (Frame Data) : 包括类文件,方法等等
栈运行原理
栈中的数据都是以栈帧(stack Frame) 的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method) 和 运行期数据的数据集
当一个方法A被调用时就产生了一个栈帧F1,并被压力栈中, A方法又调用了B方法,于是产生栈帧F2 也被压入栈
B方法又调用了C方法,于是产生栈帧F3B也被压入栈...
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧...
遵循 先进后出,后进先出的原则
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的一个过程。
栈的大小和具体JVM实现有关,通常在 256-756之间,与等于1MB左右
Exception in thread "main" java.lang.StackOverflowError
在 main方法中调用一个方法,然后这个方法递归调用自己这个方法就会发生 SOF错误
堆 (Java Heap)--堆管存储
详细介绍
新生代是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生代又分为两部分,伊甸园区和幸存者区
所有的类都是在伊甸园区 new 出来的,幸存者区有两个 , 0区和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的GC将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其它对象引用的对象进行销毁
然后将伊甸园区中剩余对象移动到幸存者0区,若幸运者0区满了,则在对幸存者0区进行GC,然后将移动到幸存者1区,若幸存者1区也满了,在移动到养老区,若养老区也满了,那么这个时候将产生 MajorGC(FullGC) ,进行养老区的内存清理
若养老区执行了 Full GC之后依然发现无法进行对象的保存,就会产生和OOM异常 OutOfMemoryError
如果出现了 OOM 异常,说明Java虚拟机的堆内存不够,原因有2
Java虚拟机的堆内存设置不够,可以通过参数 -Xms , -Xmx 来调整
代码中创建了大量的对象,并且长时间不能被垃圾收集器收集(存在被引用)
新生代
伊甸园
幸存者0区
幸存者1区
养老区
1.8 元空间
在Java8中,永久代已经被移除,被一个称为 元空间 的区域取代,元空间的本质和永久代类似
元空间和永久代之间最大的区别是: 永久代使用 JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本地物理内存
因此默认情况下,元空间的大小受本地内存限制,类的元数据放入 native memory ,字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制
堆 参数调优入门
-Xms
设置初始分配大小,默认为物理内存的 1/64
-Xmx
最大分配内存,默认为物理内存的1/4
-XX: +PrintGCDetails
输出详细的GC处理日志
idea如何进行调优呢?
VM options : -Xms1024m -Xmx1024m -XX:+PrintGCDetails
堆物理内存分析
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
如何产生这错误
byte[] by=new byte[1024*1024*40];
:+PrintGCDetails
输出详细GC收集日志信息
GC
GC (Allocation Failure)
[DefNew{新生代发生GC}: 2694K{new GC新生代内存占用}->320K{new GC后新生代占用也就是说2694-320就是节约出来的内存}(3072K){新生代总共大小}, 0.0020741{总共耗时} secs] 2694K{new GC前JVM堆内存占用}->984K{new GC后JVM堆内存使用}(9920K), 0.0021174 secs]
[Times: user=0.00{用户耗时} sys=0.00{系统耗时}, real=0.00 secs{实际耗时}]
FullGC
Full GC (Allocation Failure)
[Tenured: 6156K->5796K(6848K), 0.0022554 secs] 6156K->5796K(9920K), [Metaspace: 233K->233K(4480K)], 0.0022868 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
总结: [名称: GC前内存占用->GC后内存占用 (该区内存总大小)]
GC
GC是什么(分代收集算法)
次数上频繁收集 new 区
次数上较少收集 old 区
基本不动 元空间
GC4大算法
GC 算法总体概览
4大算法
引用计数法
复制算法 (copying)
new 代中使用 minor GC,这种GC算法采用的就是 复制算法Copying
what
原理
子主题
动态演示
复制算法缺点
1),它浪费了一半的内存
2),如果对象的存活率很高,我们可以极端一点,假设是 100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重复一遍
复制这一工作所花费的时间,在对象存活率到一定程度时,将会变得不可忽视,所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
标记清除(Mark-Sweep)
老年代一般是由标记清除或者标记整理的混合使用
what
原理
算法分成 标记和清除两个阶段,先标记处要回收的对象,然后统一回收这些对象
用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作来便让应用程序恢复运行。
好处
节约内存空间
坏处
1),需要进行俩次扫描,耗时严重
2),内存不连续,会产生内存碎片
标记压缩(Mark-Compact)
老年代一般是由标记清除和标记压缩混合使用
好处
内存连续,没有碎片
缺点
效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址,从效率上说,标记/整理算法要低于复制算法
标记 -清除-压缩
标记清除
标记压缩
JMM(Java内存模型)
volatile是java虚拟机提供的轻量级的同步机制 (丐版的synchronized)
保证可见性
不保证原子性
禁止指令重排
JMM你谈谈
可见性
原子性
VolatileDemo代码演示可见性+原子性代码
有序性
0 条评论
下一页