1.JVM的位置
2.JVM的体系结构
百分之99的JVM调优都是在堆中调优,Java栈,本地方法栈,程序计数器是不会有垃圾存在的
jvm
特点:
- 一次编译多处运行
- zid内存管理
- 自动回收功能
堆,栈,
JVM分为四个部分
- 类加载器
- 运行时数据区
- 执行引擎
- 本地方法接口
程序在执行前要先把java 文件转换成(Class文件),JVM首先要需要把字节码通过一定的方式类加载器(ClassLoader)把文件加载到内存中 运行时数据区,而字节码文件JVM的一套指令集规范并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎 将字节码翻译成底层操作系统指令再交由 CPU 去执行 ,而这个过程中需要调用其他语言本地库接口(Native Interface) 来实现整个程序的功能
我们常说的JVM值得是运行时数据区,
3类加载器
作用加载class文件
- class 存在于硬盘上
- Clas 加载到JVM中,被称为DNA元数据模板,放在方法中
- .Class–>JVM–>最终成为元数据模板,此过程就要有一个运输工具(ClassLoader) 扮演一个而快递员的角色
类加载的过程
加载:
链接 :
验证 验证被加载的类是否有正确的内部结构,并和其他类协调一致
准备
- 准备阶段负责为类的静态属性分配内存,并设置默认初始值
- 不包含Final 修饰的Static实例变量,在编译时进行初始话
- 不会为实例变量初始化
解析
- 将类的二进制数据中的符号引用替换成直接引用
初始化
类什么时候初始化?
1 )创建类的实例,也就是 new 一个 对象 2)访问某个类或接口的静态变量,或者对该静态变量赋值 3)调用类的静态方法 4)反射(Class.forName(“”)) 5)初始化一个类的子类(会首先初始化子类的父类)
类的初始化顺序
顺序是:父类 static –> 子类 static –> 父类构造方法- -> 子类构造方法
类的加载器
启动了加载器(引导类加载器)
- C/C 实现 嵌套在JVM内部,它用来加载JAVA核心类
扩展类加载器
- 上层类加载器 下载引用程序类加载器
- java.ext.dirs系统属性所指目录中加载类库,或者JDK系统安装目录
- jar包
引用程序类加载器
- 上层类加载器为扩展类加载器. 加载我们自己定义的类.
自定义加载器
4.双亲委派机制
java虚拟机堆class文件采用按需加载方式,就是需要时才会加载class对象,而且加载某个类的class时,java虚拟机采用的而是双亲委派模式,即把请求有父类处理,它是一种任务委派模式
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
如果均加载失败,就会抛出:classNotFoundException
异常
双亲委派优点
- 安全,可避免用户自己编写的类动态替换JAVA的核心类. java命名规范性
- 避免全限定名称的类重复加载
5沙箱安全机制
作用: 防止恶意代码污染java源码
比如上面我们定义了一个类名为 String 所在包也命名为 java.lang,因为这个类本来是属于 jdk 的,如果没有沙箱安全机制的话,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器.但是由于String 就是 jdk 的源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的 String,后面的一概不能使用,这就保证了不被恶意代码污染
5.1类的主动使用/被动使用
主动使用:
通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式, 他肯定会导致类的加载并且初始化 访问类的静态变量,包括读取和更新 访问类的静态方法 对某个类进行反射操作,会导致类的初始化 初始化子类会导致父类的的初始化 执行该类的 main 函数
被动使用:
其实除了上面的几种主动使用其余就是被动使用了
- final static 常量 在编译时初始化常量
- 构造某个类的数组是不会导致该类的初始化
Student[] students = new Student[10] ;
6.Native 本地方法栈
一个 Native Method 就是一个 java 调用非 java 代码的接口
会进入到本地方法栈
- 调用本地方法接口
- JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,最初: C、C
- Java诞生的时候C、C 横行,想要立足,必须要有调用C、C 的程序
- 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法 为什么要使用 Native Method 1.与 java 环境外交互 2.与操作系统交互(比如线程最后要回归于操作系统线程)
- 例如: java程序驱动打印机,管理系统, 调用其他接口 Socket webService http
java虚拟机定义了若干种程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的.这些与线程对应的区域会随着线程开始和结束而创建销毁
红色:多线程共享,灰色的为单个线程私有的
线程间共享: 堆,对外内存
每个线程: 独立包括程序计数器,栈,本地方法
7:PC寄存器
程序计数器: Program Counter Register
每个程序都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的代码),在执行引擎读取下一条指令,是一条非常小的内存空间
特点 :
- 程序用来储存下一条指令的地址,也是即将要执行的指令代码.
- 所在内存小,几乎可以忽略不计,也是运行速度最快的存储区域.
- 每个线程都有自己的线程,每个线程都有自己程序计数器,…线程是私有的.生命周期于线程的生命周期保存一致
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成
- 它是唯一个在java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域 - 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.
面试题
使用程序计数器存储字节码指令地址有什么用?
为什么使用程序计数器记录当前线程的执行地址
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿到哪儿,开始继续执行
- JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令
2.程序计数器为什么被设定为线程私有的
我们都知道所谓的多线程子啊一个特定的时间段只会执行其中某一个线程的方法,cpu会不停地做任务切换,这样必然导致经常终端或者恢复,
为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个程序计数器,这样一来各个线程之间便可以独立计算
8执行引擎
一个 Java 程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
区分:
- 前端编译: 从程序员—字节码文件的这个过程叫前端编译
- 执行引擎这里有两种行为: 一种是解释执行,一种是编译执行(这里是后端编译)
什么是解释器=====什么是JIT编译器
解释器: 当JAva 虚拟机启动时 会根据预定义的而规范对字节码采用逐行解释的方式执行,将每条字节码文件中的类容"翻译"为对应的本地机器指令执行
JIT(Just In Time Compiler)编译器: 就是虚拟机将源码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行
5.3 为什么 Java 是半编译半解释型语言?
不常用的
解释
常用的编译
(热点代码)
起初将Java语言定为"解释执行" 还是比较准确的…再后来…,java也发展出可以直接生成本地代码的编译器,现在JVM在执行java代码的时候,通常都会将解释执行与编译执行二者结合起来进行
原因: 解释器真正意义上所承担的角色就是一个运行时“翻译者” 将字节码文件中的 内容“翻译”为对应平台的本地机器指令执行,执行效率低。 JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区 的 JIT 代码缓存中(执行效率更高了) 是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要 根据代码被调用执行的频率而定。 JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其 直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。 一个被多次调用的方法,或者是一-个方法体内部循环次数较多的循环体都可以 被称之为“热点代码”。 目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。
JIT 编译器执行效率高为什么还需要解释器?
- 当程序启动后,解释器可以马上发挥作用,响应速度块,省去了编译的时间,立即执行
- 编译器想要发挥作用,把代码编程成本地代码,需要一定的执行时间,但编译后为本地代码后,执行效率高,就需要采用解释器与及时编译器并存的架构来换取一个平衡点
9.栈
栈是运行时的基本单位,堆是储存的基本单位
java虚拟机栈
java虚拟机栈:每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次调用方法
主管程序的运行,生命周期和线程同步 ::::线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就会over
栈: 数据共享
栈: 8大基本类型 对象引用 实例方法
栈运行的原理: 栈帧
栈帧
栈帧: 局部变量表 操作数栈
每执行一个方法,就会产生一个栈帧,程序正在运行的方法永远在栈的顶部
栈帧的内部结构
- 局部变量表 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
- 操作数栈 栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
- 动态链接 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量
- 返回地址 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
- 附加信息
本地方法栈:
区别: java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用
本地方法栈也是线程私有的
允许被现实固定或者是可扩展的内存大小,内存溢出方面也是相同的
StackOverflowError.
如果线程请求分配的栈容量超过本地方法栈允许的最大容量OutOfMemoryError
如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出
本地方法是用 C 语言写的.
10.三种JVM
- Sun公司的
HotSpot Java Hotspot™ 64-Bit Server VM (build 25.181-b13,mixed mode)
常用 - BEA
JRockit
- IBM
9JVM
11. 堆:
特点
- heap,一个JVM只能有一个堆内存,堆也是JAVA内存管理的核心
- 堆内存的大小是可以调节:
-Xms1024m (起始堆大小) -Xmx1024m(堆最大内存大小)
一般情况起始值和最大值设置为一致,这样就会减少垃圾回收之后堆内存重新分配大小的次数,提高效率
- 《Java 虚拟机规范》规定 : 堆可以处于物理上连续不内存空间,但是逻辑上他是连续的
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
- 所有的对象实例都应当运行时分配在对上堆,是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 类加载器读取类文件之后,一般会把什么东西放在堆中? 类 方法 常量 变量~~ 保存我们所有的引用类型性的真实对象
堆内存 中还会细分为三个区域
- 新生区(伊甸园区) Young/New
- 养老区 old
- 永久区 Perm
GC:Garbage recycling
轻GC:轻量垃圾回收,主要在新生区
重GC:重量级垃圾回收,主要在养老区,重GC就说明内存都爆了
GC垃圾回收,主要是在伊甸园区和养老区~
假设内存满了,OOM(Out Of memory) 堆内存不够!java.lang.OutOfMemoryError: Java heap space
在JDK以后,永久存储区改了个名字(元空间)
为什么区分代(代)
将对象根据存活概率进行分类 ,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间以及GC频率,针对分类进行不同的而垃圾回收算法,对算法进行扬长避短
创建对象在内存分配过程
- new的新对象先放到伊甸园,此区有大小限制
- 当伊甸园的空间填满是,程序需要创建对象,JVM垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不在被其他对象所引用的对象进行销毁,在加载新的对象放到伊甸园区
- 然后将伊甸园区中的剩余对象移动到幸存者 0 区.
- 如果再次出发垃圾回收,此 时上次幸存下来存放到幸存者 0 区的对象,如果没有回收,就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
- 什么时候去养老区呢?默认是 15 次,也可以设置参数
-XX:MaxTenuringThreshold=<N>
- 在老年区,相对悠闲,当养老区内存不足时,再次触发GC:MajorGC,进行养老区的内存清理
- 若养老区的
MajorGC
之后发现依然无法进行对象保存,就会产生OOM异常
12.新生区.老年区
新生区
- 类:诞生和生长的地方,甚至死亡
- 伊甸园: 所有的对象都是在伊甸园new出来的
- 幸存者区(0,1)
伊甸园满后就触发轻GC,经过轻GC存活子啊来到了幸存者区,幸存者区满之后意味着新生区也满了,则触发重GC,经过GC之后存活下来的就到了养老区
真理: 经过研究,99%的对象都是临时对象
配置新生代与养老代在堆结构占的比例(一般不会调用)
- 默认新生代占1 老年代占2
- 当发现整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
在HotSpot
Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1
新生区的对象默认生命周期超过15,就会区养老区
12.1分代收集思想MinorGC,Major GC , Full GC
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾.
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.
整堆收集出现的情况
System.gc() 时 老年区空间不足 方法区空间不足 开发期间尽量避免整堆收集.
12.2TLAB机制
为什么要有TLAB
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在开发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
什么是TLAB?
- TLAB即线程本地分配缓存区,这是一个线程专用的内存分配区域
- 设置了虚拟机参数
XX:UseTLAB
,在初始化时,同时也会申请一个指定大小的内存,只给当前线程使用,这样每个线程都有一个空间,如果需要分配内存,就在 自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。 - JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以避免线程同步,提高了对象分配的效率
字符串常量池为什么要调整位置?
JDK中将字符串常量池放到了堆内存中,因为永久代的回收效率低,在FUll GC的时候,才会执行永久的垃圾回收,而 Full GC 是老年代的空间不足、永久代不足时才会触发 这就导致 StringTable 回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
方法区
方法区的基本理解
方法区是一个被线程共享的内存区域,其中主要储存加载的类字码…class/method/field 等元数据、static final 常量、static 变量 即时编译器编译后的代码等数据,另外,方法区包含了一个特殊的区域"运行时常量池"
Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开. 所以,方法区看做是一块独立于 java 堆的内存空间.
方法区的内部结构
方法区用于储存
方法区用于储存已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存,运行常量池等
方法区的垃圾回收
方法区的垃圾收集主要回收两个部分内容: 运行时常量池中废弃的常量的不在使用的类型
回收废弃常量与回收 Java 堆中的对象非常类似。(关于常量的回收比较简单, 重点是类的回收) 下面也称作类卸载
13永久区
**这个区域常驻内存的,用来存放JDK自身携带的CLass对象,InterFace元数据,存储的是java运行时的一些环境,**这个区域不存在垃圾回收,关闭虚拟机就会释放内存
- JDK1.6之前:永久代…常量池是在方法区
- JDK1.7: 永久代,但是慢慢退化了,去了永久代,常量池在堆中
- JDK1.8之后,无永久代,常量池在元空间
元空间**:逻辑上存在,物理上不存在 (因为储存在本地磁盘内)** 所以最后并不算在JVM虚拟机内存中
14.堆内存调优
可以通过调整参数(Edit Configuration—>VM options
)控制JAVA
虚拟机初始内存和分配的总内存的大小
默认情况下
分配的总内存是电脑的内存的1/4 而初始化的内存是 1/64
报错OOM:
1 尝试扩大堆内存看结果 2 分析内存,看一下哪里出了问题(专业工具) //
-Xms1024m -Xmx1024m -XX : PrintGCDetails
当新生代,老年代,元空间内存满了之后才会报OOM
在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错
- 能够看到代码第几行出错:内存快照分析工具,MAT,
Jprofiler
- Dubug, 一行行分析代码!
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄漏的问题
- 获取堆中的数据
- 获得更大的对象
Jprofile使用
- 在IDEA中下载
jprofile
插件 - 联网下载
jprofile
客户端 - 在idea中VM参数中写参数
-Xms1m -Xmx8m -XX: HeapDumpOnOutOfMemoryError
- 运行程序后在
jprofile
客户端打开错误,告诉哪个位置的错误
命令参数详解析
-Xms 设置初始化内存分配大小/164 -Xmx 设置最大分配内存,默以1/4 -XX: PrintGCDetails // 打印GC垃圾回收信息 -XX: HeapDumpOnOutOfMemoryError //oom DUMP
15.0 垃圾回收
垃圾
垃圾是指在运行中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
如果不回 收可能会导致内存泄漏
为什么需要 GC?
- 内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
- 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片,碎片整理将所占用的将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
- 随着应用程序所应付的额业务越来越大,复杂,用户用来越多,,没有GC就不能保证应用程序的正常进行
早期的垃圾回收
在早期的C/C 时代,垃圾回收基本上是手工进行的。
好处: 可以灵活的控制内存释放 坏处: 会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏
15.0.1 JAVA垃圾回收机制
- 自动内存管理,无需开发人员手动参与内存的分配和回收,这样降低了内存泄漏和内存溢出的风险
- 开发人员更专心地专注于业务开发.
15.0.2垃圾标记阶段算法
15.0.2.1标记阶段的目的
垃圾标记阶段:主要是为了判断对象是否存活
- 在堆里存放着几乎所有的JAVA对象实例,在GC执行垃圾回收之前**,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象**.只有被标记已经死亡的对象,GC才会执行垃圾回收,释放掉其所占 的内存空间,因此这个过程我们成为垃圾标记阶段
- 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
- 判断对象存活一般有两种方式:计数算法和可达性分析算法
15GC
GC的作用区
JVM在进行GC时,并不是对这三个区域统一回收,大部分都是新生代
- 新生代
- 幸存区(From,to)
- 老年区
GC两种类:轻GC(普通类) 重GC(全局GC)
15.1引用计数算法
- 每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况
- 对于一个对象,引用 计数器 加1,否则减1 只要对象的引用计数器值为0.即表示 不可能再被使用,可进行回收。
优缺点
优点:实现简单,垃圾对象便于辨识,判断效率高,回收没有延迟性
缺点:
- 需要单独的字段储存计数器,这样的作法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加减法操作**,这样增加了时间开销**
- 一个严重的问题,既无法 处理循环引用的情况,这是一条致命的缺陷,导致java的垃圾回收器重没有使用这类算法
15.2可达性分析算法
可迭代分析算法: 也可以成为根算法,追踪性垃圾回收
- 相对于引用计数器算法来说,可达性分析算法不仅具备实现简单和执行高效等特点,更重要的是该算法有效的解决了引用计数算法中循环引用的问题,防止内存泄漏发生
- 相对于引用计数算法,这里可达性分析就是 JAVA,C# 选择的,这种类型的垃圾回收集通常也叫追踪垃圾收集
可达性分析实现思路
所谓"GCRoots
”根集合就是一组必须活跃的引用
- 可达性分析算法是以根对象集合(
GCRoots
)为起点,按照从上到下的方式搜索被根对象集合所链接的目标是否可达 - 使用可达性分算法后,内存中存活对象都会被跟对象集合直接或间接链接着,搜索所走过的路径称为
链引用
- 如果目标对象没有被任何引用链相连,则不可达,就意味着该对象已经死亡
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GCRoots可以是哪些元素?
- 虚拟机栈中引用的对象
- 本地方法栈内JNI(本地方法)引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。
1.简单一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为GCRoots进行可达性分析。 2.除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GCRoots集合。
比如:分代收集和局部回收。
15.2.1.4对象的finalization机制
finalize()方法机制
- 对象销毁前的回调函数:
finalize();
- java语言提供了对象终止机制,允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收对象之前,总会调用这个对象的
finalize()方法。
finalize()
方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中,进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。
15.2.1.5生存还是死亡?
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
可触及的:从根节点开始,可以到达这个对象。 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。 以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
具体过程
判定一个对象obJA是否可回收,至少要经历两次标记过程
- 如果对象obja到
GCRoots
没有引用链,则进行第一次标记。 - 进行筛选,判断此对象是否有必要执行finalize()方法 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。 finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。
GC常见的面试题:
- JVM的内存模型和分区----详细到每个区放什么 JVM内存模型和分区
2 堆里面的分区有哪些
Eden from to 老年区
3 GC的算法有哪些
标记清楚发法 , 标记整理 , 复制算法 引用计数器
16.回收算法
标记-清除算法
执行过程:
- 当堆空间有效内存被耗尽的时候,就会停止整个程序,然后经行两项工作,第一项就是标记,第二项清除
标记:
Collector
从引用根节点开始遍历, 标记所有被引用的对象,一般时子啊对象的header
中记录为可达对象(注意: 标记的是被引用的对象,也就是可达对象,,非标记的是即将被清除的对象垃圾
) 清除:Collector
对堆内存从头到尾进行线性的遍历,如果发现某个对象在其中header中没有标记可达性对象将其回收
标记清除优点:
非常基础和常见的垃圾收集算法容易理解
标记清除缺点:
标记清除算法的效率不算高 在经行GC的时候,需要停止整个应用程序,用户体验度差 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空间列表(空闲列表-记录垃圾对象地址)。
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的 地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够, 就存放(也就是覆盖原有的地址)。
复制算法
为了解决标记-清除算法在垃圾收集效率方面的缺陷,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
复制算法的优缺点
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
复制算法的应用场景
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,效率较高 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低 在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
标记-压缩算法
- 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记–压缩算法的最终等同于标记清除算法执行后,在进行一次内存碎片整理,因此,可以把他称为标记–清除–压缩算法
二者的本质差异在于
- 标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),
- 标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记-压缩算法的优缺点
优点:
- 清除了标记清除算法中,内存区域分散的缺点,,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
垃圾回收算法小结
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段
总结:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度的问题)
内存整齐度:复制算法=标记压缩算法>标记清除算法
利用率:标记压缩算法=标记清除算法>复制算法
思考:难道没有更优的算法吗?答案:没有,没有最好的算法,只有最合适的算法
分代收集算法
为什么要使用分代收集算法
GC—>分代收集算法
年轻代:
- 存活率低
- 复制算法!
老年代:
- 存活率高,区域大
- 标记清除(内存碎片不是太多的时候) 标记压缩混合实现
本文来源面相薪水编程,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处