赞
踩
目录
3.程序计数器(Program Counter Register)
正如一个公司的运作模式,首先公司需要一块场地,再接着需要根据公司的各个部分,将这块场地划分成一块一块的部门,每个部门都有独立的功能。
JVM的内存区域划分也是如此,JVM启动的时候会申请一整个很大的内存区域(JVM是个应用程序,要从操作系统中申请内存),接着根据需求,将整个空间分成几个部分。
native表示JVM内部的C++代码(JVM是用C++写的),所以本地方法栈就是给调用native方法(JVM内部方法)准备的栈空间
问题:C++代码是不是不用跑在虚拟机上 答:像C、C++、Go、Rust都是把代码变成成native code,也就是可以直接被cpu识别的机械指令 而Java、Python、PHP为了跨平台,都是统一翻译成指定的字节码(字节码都是一样的),然后由对应的虚拟机转换成机械指令
给Java代码使用的栈
注意:stack栈,在之前数据结构中也学习过,数据结构中的“栈”是“后进先出”的,和这里所说的栈不是一个东西。
这里所提的栈,是JVM中的一个特定的空间。
整个栈空间内部,可以认为是包含很多个元素,每个元素表示一个方法,把这里的每个元素,称之为“栈帧”,这一个栈帧里,会包含这个方法的入口地址、方法参数、返回地址、局部变量.....
但是由于函数调用中,也有“后进先出”的特点,因此此处的栈也是后进先出的,只是数据结构中的栈是一个更通用的,更广泛的概念,而JVM中的栈是特指JVM上的一块内存空间。
从内存划分图中也可以看出,这里的栈有多个,每个线程有一个(线程是一个独立的执行流),使用jconsole(jdk下bin目录中)可以查看java进程内部的情况,点击线程就可以看到线程调用栈的情况。
问题:这些线程每次创建都分相同空间,那么栈会不会被很快就用完了? 答:栈的整体大小一般都不会很大,但是每个栈帧的大小也是比较小的,遇到无限递归的代码会遇到栈溢出的情况,就正常写代码,创建销毁线程一般是不需要担心这个问题的。
记录当前线程执行到哪条指令(很小的一块存一个地址),也是每个线程有一份。
堆是整个JVM空间的最大的区域,new出来的对象,都是在堆上的,类的成员对象也就在堆上了
堆 是一个进程只有一份的
栈 是每个线程有一份,一个进程有多个线程
堆,多个线程用的都是同一个堆
栈,每个线程用自己的栈
每个jvm就是一个java进程
以前这个区域叫做方法区,java8开始改为了元数据区 这里存储的有类对象、常量池、静态成员
元数据区也是一个进程一份,多个线程共用这一份
问题:final修饰的变量在这里存储吗? 答:不一定,有一定概率被优化成字面值常量,也有可能没优化
总结
准确的来说,类加载就是将.class文件,从文件(硬盘)被加载到内存中(元数据区)这样的过程。
但是对应上述的要求并不是具体的,JVM的实现过程和应用是比较灵活的,比如获取类的二进制字节流,并没有说明如何获取,所以就有了从压缩包中获取(jar、war、ear),从网路中获取(Applet),运行时计算生成(动态代理)
对于不是数组的类的加载我们可以自定义去控制字节流的获取方式,而数组类就不一样了,因为数组类本身不是通过类加载进行创建的,而是由JVM直接创建
根据jvm虚拟机规范,检查.class文件的格式是否符合要求
给类对象分配内存空间(此时内存初始化成全0),在这个阶段里,为静态变量分配内存并设置静态变量初始值。这里说的初始值通常情况下,不是代码中写的初始值,而是数据类型的零值。代码中写的初始值,是在初始化阶段赋值的。如果是静态常量(被final修饰),这个阶段就会被直接赋值为代码中写的初始值
针对字符串常量进行初始化,把符号引用转为直接引用
字符串常量,需要有一块内存空间存这个字符的实际内容,还需要一个引用,来保存这个内存空间的起始地址。 在类加载之前,字符串常量是处在.class文件中的,这个时候没有地址这个概念,此时这个"引用"记录的并非是真正的地址,而是他在文件中的"偏移量"(或者是个占位符)。 在类加载之后,才真正把这个字符串常量给放到内存中,此时才有"内存地址",这个原因才能真正赋值成指定的内存地址。
真正针对类对象里面的内容进行初始化,加载父类、执行静态代码块的代码.....
总结
问题:一个类,什么时候会被加载呢? 答:不是java程序一运行就将所有的类都加载了,而是真正用到的时候才加载(赖汉模式),比如构造类的实例、调用这个类的静态方法/使用静态属性、加载子类,就会先加载父类。 而一旦加载过后,后续再使用就不需要重新加载了。
JVM 默认提供了三个类加载器
而上述三个类存在"父子关系",BootstrapClassLoader为最大,ApplicationClassLoader为最小。
此处的"父子关系",不是父类、子类,而是每个ClassLoader中都有一个parent属性指向自己的父 类加载器
上述三个类加载器的工作模式:
首先加载一个类的时候,先从ApplicationClassLoader开始
但是ApplicationClassLoader会把加载任务交给父亲,于是ExtensionClassLoader就会去加载,但是也不是真加载,而是再委托自己的父亲去加载
于是BootstrapClassLoader就去加载,他也想委托给父亲,但是发现父亲为null,所以就由自己加载,那么BootstrapClassLoader就会搜索自己负责的标准库中的类,如果找到就加载,如果没找到就交给自己的子类进行加载
于是ExtensionClassLoader就去加载,搜索自己负责的JVM扩展库中的类,找到则加载,找不到就交给子类
于是ApplicationClassLoader进行加载,搜索用户用的第三方库和项目中的类,找到则加载,找不到的话由于自己没有子类,就只能抛出类找不到这种异常
使用这个顺序进行,主要的目的据说为了保证Bootstrap能够先加载,Application能够后加载,这就避免了用户创建一些奇怪的类而导致的bug
比如用户写了一个java.lang.String这个类,此时能保证JVM加载的还是标准库中的类,而不会加载到用户写的这个类,这样就能保证即使出现上述问题,也不会让jvm的代码混乱,最多就是用户写的代码不生效
首先要知道的是在JVM中进行垃圾回收的是"堆",并且GC是以"对象"为基本单位进行回收的
GC回收的是整个对象都不再使用的情况,而那种一部分使用,一部分不使用的对象先不回收了(一个对象里有很多属性,可能其中10个属性后面要用,10个属性后面再也不用了)
关键思路在于看看到底有没有"引用"指向它,Java中使用对象只有一条路,通过引用来使用,如果一个对象有引用指向它那么就有可能被用到,如果没有引用指向它那么就不可能被用到
那么怎么知道对象是否有引用指向?
给每个对象分配一个计数器(整数),每次创建一个引用指向该对象,计数器就+1,每次该引用被销毁了计数器就-1
问题:这种方法很好理解,但是为什么Java不使用呢?
答: 1.引用计数内存空间浪费的多,每个对象都要分配一个计数器,计数器按照4个字节算,代码中对象非常少无所谓,怕的就是对象特别多,并且每个对象都比较小,那么这个时候占用的额外空间就会很多(一个对象的体积是1k,多4个字节无所谓,一个对象体积是4个字节,再多4个字节相当于体积扩大一倍)
2.存在循环引用问题
Java中的对象,都是通过引用来指向并访问的,经常是一个引用指向一个对象,这个对象里的成员又指向别的对象
整个Java中的所有对象,就通过类似这种树形/链式结构,整体串起来
可达性分析,就是把所有这些对象组织的结构视为树,从根节点出发,遍历树,所有能被访问到的对象就标记为"可达"(不能被访问到的,就是不可达)
此处的图中虽然只有root引用,但是上述6个对象都是可达的
小结:可达性分析需要进行类似于"树遍历"整个操作相比于引用计数来说肯定要慢一些的,但是速度慢没关系,上述遍历操作,并不需要一直执行,只需要每隔一段时间分析一遍即可
进行可达性分析遍历的起点,称为GCroots
1.栈上的局部变量
2.常量池中的对象
3.静态成员变量
一个代码中可能有很多这样的GCroots,把每个起点都往下遍历一遍就完成了扫描过程
标记清理的特点是简单粗暴,但是会导致内存碎片化问题,被释放的空闲空间是零散的,不是连续的
但是我们申请内存的要求是需要连续的空间,总的空闲空间可能很大,但是每个具体的内存空间可能很小,可能导致申请大一点的内存就失败了
例如总的空闲空间是10k,分成1k一个,一共十个,此时如果需要申请2k的空间,那么就会申请失败
复制算法解决了内存的碎片化问题
复制算法把整个内存分为两半,用一半丢一半
把不是"垃圾"的对象复制到另外一半内存空间,然后把之前的一半整个空间删掉
若是后续再触发GC,那么就在右边这一半进行复制算法到左边即可
缺点:空间利用率低下,如果垃圾少,有效对象多,那么复制成本就比较大了
标记整理解决了复制算法的缺点
类似于顺序表中的删除中间元素,有元素搬运的操作,这样既保证了空间利用率,也解决了内存碎片化问题
但是,很明显,这种做法,效率也不高,元素搬运如果需要搬运的空间大,那么开销也大
上述的三种方法都有其对应的缺点,那么有没有比较完美的办法呢?
把垃圾回收,分成不同的场景,对应不同的场景使用上述不同的办法
那么如何定义上述的不同场景呢?也就是分代是如何分的?
这里的分,是基于一个经验规律:如果一个东西(对象),存在的时间长了,那么大概率还会继续的长时间存在下去(比如C语言从197x年就开始诞生了,到如今还依然存在并且活跃,那么我们就有理由相信他还会存在很长时间),而上述规律对应Java中的对象也是有效的,java对象要么就是生命周期特别短,要么就是特别长
根据上方的规律,我们给对象引入一个概念"年龄"(这个年的单位是熬过的GC的轮次),年龄越大那么存在的时间就越久
刚new出来的对象,年龄是0的对象,放到伊甸区(出自圣经,上帝在伊甸园造小人)
熬过一轮GC,对象就要被放到幸存区了,那么伊甸区-->幸存区就使用复制算法(将有效对象复制到幸存区,伊甸区整体释放)
到幸存区后,也要接受GC的考验,如果变成垃圾就要被释放,如果不是垃圾,那么就拷贝到另一个幸存区(这两个幸存区,同一时刻值用一个,在两个幸存区反复横跳),所有这里也是使用复制算法(由于幸存区的体积不大,此处浪费的空间也能接受)
如果这个对象在幸存区中GC了很多次了,那么这个时候就进入到老年代,老年代都是年龄大的对象,生命周期普遍更长,那么针对老年代进行的GC扫描频率就低了,如果老年代的对象是垃圾了,那么就使用标记整理的方式进行释放
小结:
上述的GC中典型的垃圾回收算法:如何确定垃圾、如何清理垃圾,这里的策略,实际上在JVM实现的时候,会有一定的差异,JVM有很多的"垃圾回收实现"称为"垃圾回收器",回收器的具体实现做法,会按照上述算法思想展开,不同的垃圾回收器侧重点不同,有的追求GC扫描快、有点追求扫描好、有点追求用户打扰少(STW短).....
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。