JVM(Java Virtual Machine),中文名叫做java虚拟机,可以将其理解为一台运行在电脑上的虚拟计算机,负责运行java程序。
一、 JVM的内存区域划分:
在真实的操作系统中,对于进程的地址空间进行了分区域的设计,JVM便仿照操作系统也进行了分区域的设计。
f二、JVM具体要怎么划分呢?
分为4个核心区域:
1.程序计数器:很小的区域,只是用来记录当前指令执行到哪个地址。
2.元数据区:保存当前类被加载好的数据~~
3.栈:保存方法的调用关系
写代码的时候肯定会有方法调用,而每次调用方法,都会进入方法内部执行,当方法执行完毕,返回到调用位置,继续往后走。
4.堆:用来保存new对象。
Test t = new test();
如果t是一个局部变量,t就在栈上。
如果t是一个成员变量,t就在堆上。
如果t是一个静态成员变量,t就在元数据区。
三、JVM类加载:JVM在程序运行时,把.class字节码文件读取进来,转换成jvm能使用的类,并完成初始化的过程
有三大阶段,其中第二阶段又分为5个步骤。
1.加载:找到.class文件
根据类的全限定名(包名+类名,形如java.lang.String),打开文件,读取文件内容到内存里。
2.验证:解析,校验.class文件读到的内容是否是合法的,并且把这里的内容转成结构化的数据。
3.准备:给类对象申请内存空间(此处申请的空间相当于是全0的空间)。
4.解析:针对字符串常量,进行初始化。字符串常量本身就包含在.class文件中,就需要.class文件里解析出来的字符串常量放到内存空间里(元数据区,常量池中)。
5.初始化:针对刚才谈到的类对象进行最终的初始化~~(执行类中的静态变量赋值和静态代码块)
针对类对象的的各种属性进行填充。
包括类中的静态成员。
ex:
class Demo { static int value = 10; static { System.out.println("类初始化"); } }四、类加载中的“双亲委派模型”(又称单亲委派模型或父亲委派模型):
类加载器(ClassLoader): JVM中有专门的模块负责类加载。
JVM默认提供了三种类加载器:
1.BootstrapClassLoader -->相当于爷爷 ----> java标准库的目录
2.ExtensionClassLoader -->相当于父亲 ---->java扩展库的目录
3.ApplicationClassLoader -->相当于儿子 --->java的第三方库
双亲委派模型的过程:进行类加载,通过全限定类名,找.class的时候,会从ApplicationClassLoader作为入口开始,然后把类加载这样的任务交给父亲(ExtensionClassLoader)来进行,ExtensionClassLoader也不会立刻进行查找,而是也委托给父亲(BootstrapClassLoader)来进行,于是BootstrapClassLoader根据类名,找标准库范围是否存在有匹配的.class文件,若其没找到,则再把任务还给儿子ExtensionClassLoader,接下来ExtensionClassLoader来负责找.class文件的过程,若其也没有找到,则把任务还给孩子ApplicationClassLoader.接下来ApplicationClassLoader负责找.class文件,若找到就加载,若没找到,则抛出异常
图示:
五、垃圾回收(GC)
定义:JVM 自动找出不再使用的对象,并释放它们占用的内存。
举一个简单的例子:
User user = new User(); user = null;当原来的User对象没有任何有效引用时,它就可能被 GC 回收。
工作过程:
一.找垃圾
1).引用计数(Python,PHP使用此方法)
每个对象在new的时候,都会搭配一个小的内存空间用来保存整数。
这个整数用来表示当前对象有多少个引用指向它,每次进行引用赋值的时候,都会自动触发引用计数的修改,通过引用计数来记录有多少个引用。若引用计数为0,则此对象可被视为垃圾
缺点:1.内存消耗的更多(尤其是对象本身小,引用计数消耗的空间比例就更大)
2.可能出现循环引用的问题
比如:创建两个对象,
a.t =b; b.t= a; a=null; b=null;此时虽然两个对象的引用不为0,但这两个对象都无法使用.
2).可达性分析(java使用):
过程:
1、以代码中的一些特定对象,作为遍历的“起点” =>GCRoots,比如:
1).栈上的局部变量(引用类型)
2)常量池引用指向的对象
3)静态成员(引用类型)
2、尽可能进行遍历,判断某个对象能否访问到。
3、每次访问到一个对象,都会把这个对象标记为“可达”,当完成所有的对象的遍历之后,未被标记为“可达”的对象就是“不可达”,随后便进行垃圾回收处理。
二.释放垃圾:
方法一:标记-清除,把垃圾对象的内存直接释放。
这样会产生内存碎片问题,如下图:
这些释放后的空闲空间的空间是不连续的,而申请内存都是申请连续内存,若内存碎片非常多,可能会导致申请内存失败的问题。
方法二:复制算法
申请出一段空间,将这段空间分成两个相等的部分,使用时只使用其中的一半,把不是垃圾的对象拷贝到另外一侧,让后再把这一侧给整体的释放掉,可以确保空闲的内存都是连续的。
缺点:
1.内存的空间利用率是很低的。
2.一旦出现垃圾对象比较少的情况,复制的成本就会变高。
方法三:标记-整理(将方法一和方法二相结合)
将带有标记的垃圾回收之后,类似于顺序表的搬运,将好的内存数据填补到空白空间
个类加载器收到加载请求后,会先交给父加载器;父加载器找不到时,子加载器才自己加载。这样可以避免自己写一个假的java.lang.String来替代系统类,也能减少类重复加载。
方法四:分代回收:据对象生命周期不同,年轻代和老年代采用不同策略。("代"指的是对象的年龄)GC 主要管理堆内存。而JVM通常把堆分为新生代和老年代。
分代回收过程:
新对象通常先进入 伊甸(Eden) 区。年轻代空间不足时,发生Minor GC或 Young GC:
- 找出仍然存活的对象;
- 把它们复制到 Survivor 区;
- 对象经历多次年轻代 GC 后,可能晋升到老年代。
老年代空间不足时,可能发生老年代 GC.
注:
类加载:把类放进 JVM 并准备使用。
对象创建:使用new在堆中创建对象。
类通常只会被某个类加载器加载一次,但只有在被主动使用时才可能触发初始化。