目录
1.JVM内存划分
2.JVM类加载过程
3.JVM垃圾回收机制GC
3.1.判断谁是垃圾
3.2.如何释放对应的内存
1.JVM内存划分
在一个Java程序运行起来之后,jvm就会从操作系统中申请一块内存,然后就会将该内存划分成多个部分,用于不同的用途。
(1)主要划分成五个部分
堆、栈、元数据区(方法区)、常数计数器(很少涉及)
图示:
像堆区中的各个部分划分,在后面的垃圾回收机制再拿出来鞭策一遍,在这里就不先做任何的赘述。
(2)每块内存区的功能
堆区:
整个内存区域中最大的区域,用于存放java代码中new出来的对象、和成员变量。
栈:
一般java是使用jvm虚拟机栈,这里保存了方法的调用关系、局部变量等。也就是每个方法怎么被调用,被谁调用等
元数据区:
元数据区,以前也成为方法区,用来存放类对象、类属性(静态成员)、常量
程序计数器:
属于内存中最小的区域,用来保存要执行的下一条指令的地址
(3)实战划分内存
看下嘛的一段代码,查看下面的变量和对象分别处于哪一块内存中
class Demo2 {
}
class Demo1 {
public int a;
Demo2 b = new Demo2();
public String c = "love";
public static int d;
}
public class Test {
public static void main(String[] args) {
Demo1 e = new Demo1();
}
}
对于变量:a,b,c,d,e 其中a,b,c都属于成员变量,存在于堆上;而e属于局部遍历,存在栈上;而d属于静态成员变量,属于类属性,存在于元数据区。它们只是属于变量,里面存有一个值,会指向另一块内存空间。
各对象内存分布和指向关系
以上就是对应的各种关系。
对于上述的四个区域,堆和元数据区,在整个进程中只有一份,而栈和程序计数器,是每个线程都有一份。
2.JVM类加载过程
一个.java文件变成.class文件的过程,也是从硬盘加载到内存中,得到类对象的过程。
(1)类加载的五个环节
加载、验证、准备、解析、初始化
(2)每个环节对应的作用
1.加载
在硬盘上找到对应的.class文件,并且读取.class文件内容
2.验证
检查.class文件中的内容,是否符合要求(如文件格式等)
3.准备
给类对象分配内存空间
4.解析
对字符串常量初始化,把刚才.class文件中的常量内容取出来放到元数据区
5.初始化
针对类对象进行初始化,给静态成员初始化,也就是执行静态代码块
(3)“双亲委派模型”
在第一步的加载环节,目的是打开.class文件,前提就是需要通过“全限定类名”找到文件才能进行打开,所以“双亲委派模型”就是寻找.class文件的一种机制。
在这个环节,涉及到一个概念:类加载器
在JVM中也会包含一些类,负责完成后续的类加载工作。其中JVM内置了三个类加载器,负责加载不同的类
分别是三个类:BootstrapClassLoader、ExtentionClassLoader、ApplicationClassLoader
1)三个类加载器作用
所以成为双亲委派模型,但是跟准确的说法为:父亲委派模型或者单亲委派模型
2)加载过程
那么双亲委派模型是如何找类的呢?我们举一个例子,假设我们自己写了一个java程序,会给定一个全限定类名
3)双亲委派模型应对的场景
如果自己的代码中写的类的名字和标准库/扩展库冲突了,JVM会确保加载的类是标准库的类(不会加载自己写的类),如果标准库中的类无法加载,那么Java进行就没有办法正常工作了。
这样还有一个好处,就是可以确保自己写的类肯定可以被加载到。
3.JVM垃圾回收机制GC
对于Java,回收垃圾采取的是自动回收策略,策略也称为GC。
对于GC来说,回收的其实是堆上的内存。而对于堆,保存的主要是对象,换句话说,也就是主要回收对象,那怎么回收对象呢?主要有两个步骤:判断谁是垃圾和如何释放其对应的内存。
3.1.判断谁是垃圾
在判断谁是垃圾这一步,Java是采取很保守的做法,也就是可以保证只会释放后续不会再使用的对象,后续仍会使用到的对象,是不会进行回收的,所以才用的策略是:判断某个对象是否存在引用指向,如果没有引用指向,就可以判断为垃圾,反之不行。
判断谁是垃圾,GC有两种策略:引用技术和可达性分析,而JVM采取的策略只有可达性分析,引用计数则不是。
(1)引用计数
1)策略:每创建一个对象,就在对象前面多开辟一块空间,用来计数使用,有一个引用指向该对象,计数变量就+1,如果计数器为0,则需要回收该对象。
2)代码举例
class TT {
}
public class GC {
public static void main(String[] args) {
TT a = new TT();
TT b = a;
}
}
对于实例化的TT对象,当前有两个对象指向它,所以计数值为2(分别是引用a和b),如果a=null或者b=null,则计数值-1,两个都置为null,则计数为0.
对于引用计数存在两个问题
问题一:会消耗额外的内存空间
如果对象本身的内存比较大,相比来说计数的空间就很小;但是如果对象内存空间很小,那么计数空间就会显得很大,就会浪费很大的空间
问题二:存在“循环引用”问题
这类问题就会让外部代码无法对 对象进行释放
代码举例:
class T {
T t;
}
public class GC {
public static void main(String[] args) {
T a = new T();
T b = new T();
a.t = b;
b.t = a;
}
}
这是一段有问题的代码
图示分析:
存在的问题:当将引用a和b置为null时,按理来说这两个对象是要被回收了,但是这里却不会,因为计数不为0,不能回收。所以这就是引用计数最大的一个问题。
(2)可达性分析
JVM采取的是可达性分析,既解决了空间问题,也解决了循环引用问题。
1)定义:JVM会把对象之间的引用关系,定义成树形结构,JVM可以不停的从根节点开始遍历,可以访问到的对象,成为“可达”,剩下的就是“不可达”。
标记为不可达的对象,也就被标记成垃圾。
2)举例
例如这样的结构,如果将c=null,那么c后面的两个对象f和g就变成不可达,就要成为垃圾。
访问这棵树的所有节点,都是要通过根节点a开始。
3)确定根节点
对于根节点,可能的情况有:栈上的引用局部变量、常量池中引用的对象、方法区中的静态成员。
3.2.如何释放对应的内存
当已经标记好垃圾之后,该怎么回收呢?下面介绍。一共四种,前面三种是铺垫,最后一种才是JVM真正使用的
(1)标记-清除
策略:直接把标记为垃圾对象的对应的内存回收,已经回收的内存,其他对象可以使用
缺点:这样做很存在内存碎片问题,后续就很难申请到一大块连续的空间
(2)复制算法
策略:要删除空间1中abcd中的ac,就直接将bd复制到另一块空间中
缺点:严重浪费空间。比如有8G内存,就只能使用4G
(3)标记-整理
策略:把不需要释放的内存空间覆盖到需要释放的空间上。可以解决内存碎片问题
缺点:时间开销很大
(4)分代回收
策略:根据不同的场景,采取不同的回收策略,这里的回收策略也就是上述介绍到的。
如何根据场景呢?就是要根据时间来,哪个时间呢?就是对象的年龄。
因为GC主要回收的地方就是堆区,所以堆区上会有对应的分区,不同的区代表对象的年龄
Eden:称为伊甸区,新创建的对象都处于这里。
这里的对象生命周期都很短,一般经过一轮GC就会成为垃圾。从伊甸区到达生存区,采取的是复制算法
s0:称为生存区,一般经过一轮还未被回收,就会到达下一个阶段
从生存区到达幸存区,也是通过复制算法
s1:称为幸存区
这里也称为生存区,到达这里也需要通过复制算法。
Old区:到达这里的对象都会成为老年代的对象,GC的扫描频率会大幅度降低。
对于老年代中的垃圾,就会通过标记-整理的方法进行回收
总结一下:伊甸区、生存区、幸存区都是通过复制算法回收垃圾很搬运到下一个区,对于老年区,则是通过标记-整理回收垃圾。