JVM

JVM内存区域划分

JVM在启动的时候,会申请到一块很大的内存空间, JVM会将这块空间分成很多区域, 每个区域都有自己的功能和作用.

如下图:

29a55e195be04e648228462b3a64b992.png

3a1010c208eb4686b6eba37454b9d4d3.png

 本地方法栈是给调用JVM内部的native方法准备的空间.

823fc8c21bee4fcba52112c301632476.png

 这个部分是记录当前线程执行到哪个指令,每个线程都各自有一份.

c2ab788f2f7c467d8f1a91926bda3bd8.png

 虚拟机栈是给Java代码使用的栈,这里存储的是方法与方法之间的调用关系.

在虚拟机站中,有很多个元素,每个方法都代表一个元素,这个元素被称之为"栈帧".

在每个栈帧里面,会存储这个方法的入口地址、返回地址、局部变量、形参......

每个线程都各自有一个自己的栈。

9a4cbee8944b483094e7fb23227cafcf.png

 堆是JVM里面最大的区域,我们new出来的对象(对象的成员变量)都存放在堆中,并且所有的线程都共用一个堆。

2c1b74a0bc3c4b5b8b5419312840ac6e.png

 元数据区(方法区),类对象就是存放在这里的,这里面存储还有常量池、类方法(静态方法)

总结:局部变量存储在栈上;类的普通成员变量存储在堆上;静态成员变量存储在元数据区。

类加载机制

类加载流程

类加载就是将 .class 文件,从文件(硬盘)读取到内存(元数据区)中的过程。

如下图:

588045e1a4174379834d8be91e22bfed.png

加载:将 .class 文件找到,读取文件内容。

验证:根据JVM虚拟机规范,验证 .class 文件的格式是否符合要求。

准备:给类对象分配空间,此时内存初始化为全0,静态成员也就设置为0值了。

解析:针对字符串常量进行初始化,将 字符引用 转为 直接引用 。

初始化:真正将类对象里面的内容初始化,加载父类,执行静态代码块里面的代码。

解析中的字符引用和直接引用:

字符串常量需要一块空间来存储字符串的内容,同时也需要一个引用来保存它的其实地址。

在类加载之前,字符串常量是处于 .class 文件中的,此时 它的引用 记录的并非是字符串常量的真正地址,而是它在文件中的“偏移量”,这个偏移量就是上面说的符号引用

当类加载之后,才会真正的把这个字符串常量给放到内存中,此时才有“内存地址”,此时这个引用才能真正赋值成指定的内存地址。

类加载的时机

类加载属于懒汉模式,只有真正被用到的时候才会被真正加载。

1. 构造类的实例

2. 调用这个类的 静态方法/使用静态属性

3. 加载子类,在这之前会先加载父类

上述过程用到了才加载。并且,一旦加载过之后,后续再使用就不必再重复加载了。

双亲委派模型

双亲委派模型,描述的是在加载过程中,找 .class 文件的基本过程。

JVM里面默认提供了三个类加载器,并且他们存在“父子关系”:

BootstrapClassLoader(ExtensionClassLoader的父亲)  负责加载标准库中的类

ExtensionClassLoader(ApplicationClassLoader的父亲)  负责加载JVM扩展库中的类

ApplicationClassLoader  负责加载用户提供的第三方库/用户项目代码中的类

上述类加载器配合工作过程:

首先加载一个类的时候会先从ApplicationClassLoader开始。

但是ApplicationClassLoader会先将加载任务交给他的父亲ExtensionClassLoader

然后,ExtensionClassLoader同样会将任务交给它的父亲BootstrapClassLoader

当来到BootstrapClassLoader时,它的父亲是null,然后就由它进行加载,如果找到,就进行加载,如果没找到,就交给它的 子 类加载器

如果ApplicationClassLoader也没找到,由于当前已经没有子 类加载器了,此时就会抛出 类找不到 这样的异常

这个流程大致如下图:

7445b9f91ee24dc98745af90250728d3.png

 通过流程图可以发现,从ApplicationClassLoaderBootstrapClassLoader这个过程是没有进行查找操作的,而出现这种顺序的原因是JVM实现代码的逻辑是一个类似于递归这样的方式。

同时这个顺序还可以保证BootstrapClassLoader先加载,ApplicationClassLoader后加载

因为,如果用户给类起了一些奇形怪状的名,比如:用户起了一个java.long.String这个类名如果按照上述加载流程,加载的会是标准库里面的类,而不会加载到用户写的这个类。

这样就可以保证:即使出现上述问题,也不会让JVM已有代码发生混乱,最多就是用户自己写的类不生效罢了。

除了上述的这些类加载器,用户还可以自定义类加载器,用户自定义的类加载器页可以加入到上述流程中和现有的类加载器配合使用,同时自定义的类加载器也可以破坏这个双亲委派模型。

比如:Tomcat 在加载 webapp 的时候就时单独的类加载器,并且不会遵守双亲委派模型,因为webapp 的加载,其他的类加载器大概率是加载不到的,索性就直接自己加载。

GC垃圾回收机制

所谓垃圾,就是指不再使用的内存,而垃圾回收就是将不用的内存进行释放,而GC垃圾回收机制就是帮我们程序员自动释放这些不用的内存。

GC的优缺点

好处:方便省心,让程序员写代码更简单,不容易出错。

坏处:需要消耗额外的系统资源,和额外的性能开销,同时,GC还有一个STW问题。

STW问题:

如果在某一时段,内存中的垃圾非常的多,此时触发一次GC操作,开销可能会非常大,甚至会将系统资源吃掉很多,就会导致这次GC操作会让程序发生卡顿,这样的卡顿,在极端情况下可能会持续几十毫秒甚至上百毫秒。

GC如何寻找垃圾

在GC中,回收垃圾是以对象为基本单位的。

如果想释放掉垃圾内存,首先要先找到并判断哪个对象是垃圾,哪个对象不是垃圾。

而判断的思路就是:抓住一个对象,查看是否有“引用”指向它。

这个思路有两种典型实现。

方法1:引用计数(非Java做法)

引用计数就是给每个对象都分配了一个计数器(整数),每创建一个引用执行该对象,该对象的计数器就+1,反之,指向该对象的引用每销毁一个,计数器就-1。

举个例子:

public static void main(String[] args) {
    Test t1 = new Test();//计数器为+1 为 1
    Test t2 = t1;//        计数器为+1 为 2
    Test t3 = t1;//        计数器为+1 为 3
    Test t3 = null;//      计数器为-1 为 2
}
//大括号结束,上述三个引用超出作用域,失效,此时引用计数为0,此时 new Test() 就是垃圾了

引用计数的优点:简单有效。

引用计数的缺点:

1. 内存空间浪费(利用率低)

因为每个对象都需要分配一个计数器。

如果每个计数器都是4个字节,并且对象比较多,此时占用的内存空间就会很多,尤其是对象比较小的时候。

如果一个对象本身就只占了4个字节,那加上一个计数器4个字节,就相当体积于翻了一倍。

2. 存在循环引用的问题

如下:

class Test {
    Test t = null;
}

class T {
    public static void main(String[] args) {
        // a 指向的对象为对象1; b指向的对象为对象2
        Test a = new Text(); //对象1 计数器为1
        Test b = new Text(); //对象2 计数器为1
        a.t = b;//             对象2 计数器为2
        b.t = a;//             对象1 计数器为2

        a = null;//            a指向断开 对象1计数器-1 为1
        b = null;//            b指向断开 对象2计数器-1 为1
    }
}

上述代码,理论上 对象1和对象2都已经成为垃圾内存,但是他们的计数器却没有清0。

这就是循环引用问题,它的解决需要搭配其他的机制去配合(此处不说明)。

方法2:可达性分析(Java做法)

Java中的对象都是通过引用来指向并访问的,而又经常会有:一个对象里面的成员又指向了其他对象,类似于一个链式/树形的结构。

而可达性分析,就是把这些对象组织起来的结构视作为树,然后从根节点出发,遍历这颗树,将所有能被访问到的对象标记为 “可达” ,不能被访问到的就是不可达。

重复上述的做法将所有对象全部遍历一遍(如果该对象已经被标记可达就不会继续向下遍历),将不可达的对象进行回收,这就是可达性分析。

可达性分析的优缺点:

优点:节省空间。

缺点:速度慢。

但是上述的可达性分析遍历操作,不需要一直执行,只需要每隔一段时间,分析一遍就可以了,可以在一定程度上缓和速度慢的缺点。

延展:

进行可达性分析遍历的起点成为GCroots

GCroots可以为:

1. 栈上的局部变量

2. 常量池中的对象

3. 静态成员变量

一个代码中又很多这样的起点,把每个起点都往下遍历一遍就完成了一次扫描

GC如何清理垃圾

GC清理垃圾主要有三种基本做法

标记清除

如下图:

fe4d1b2b63ef4ea58f2c885b405af20b.png

 这个方法,简单粗暴,效率很快。

但是缺点也很明显:

被释放的空间是零散的,得到的空间也是零散的,不是连续的。

复制算法

复制算法是现将空间平均分为两大块,一块使用,另一块不使用。

当使用区有回收的空间时,会先将不被回收的对象放入另一块空间,然后再将原来的空间清空。

7882dea3f0b44a6eb43762abec5642e8.png

 这样的方法优点是:回收后的剩下的空间也是连续的。

缺点:(1)空间利用率低,原本一整块空间被分为两块

(2)效率可能低,因为只要有内存被回收,就会进行复制,如果回收的内存是很少一部分,那么效率就会变得很低,并且如果被复制的内容量很大,那么效率也会很低。

标记整理

标记整理,解决了复制算法的空间利用率低的问题,它的解决方法类似于顺序表删除节点的空间搬运。

如下图:

c7b5a5cb3d604fd8b1f829b0726c5391.png

标记整理,解决了复制算法的空间浪费,但是复制过去的效率还是比较低。

所以,就出现了一种回收算法,将上述的三种算法进行了优点整合。

 分代回收

分代回收根据了一个经验规律:如果一个东西,存在的时间比较久,那么大概率还会继续长时间的存在下去。

上面的这个规律对于Java的对象也是有效的(又一系列的实验和论证)

所以,就给对象引入了一个“年龄”的概念,它的单位为 熬过GC的轮次

每经历一次GC,没有被回收,就会 +1岁

根据上面的规律,分代回收会将堆分为一系列区域。

首先是分为两个区域,一块是新生代默认占比1/3,一块是老年代默认占比2/3。

新生代又非分为一个伊甸区和两个幸存区,其中伊甸区占有空间较多,幸存区占有空间较少,并且两个幸存区的空间大小相同,默认占比为8:1:1。

如下图:

ee2c3f1019c147dbac807e1b621eeafe.png

 在伊甸区中,存放的是新进来的对象,也就是年龄为0的对象。

当伊甸区的对象经历了一次GC后,没有被回收掉,此时没被回收的对象就进入了幸存区。这个过程使用的是复制算法。

在两个幸存区中,也是使用复制算法来筛选,一个幸存区放对象,另一个滞空。

每当幸存区的对象经过一次GC,存活下来的对象就会转移到另一个幸存区,容纳后来回转换,当达到一定次数后(默认对象到达15岁),就会进入老年区。(复制算法)

在老年代,GC的的频率就比较慢了,如果在老年代被回收了,此时使用的是标记整理的方法进行释放。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追梦不止~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值