JVM内存划分
1.堆,整个内存区域中,内存最大的区域,放的都是new出来的对象,new+类名这一部分存放在堆中,
而这个scanner是一个临时变量,这个scanner的地址存放在栈上,scanner里面存放的值是new+类名这个对象的首地址
2.栈,分为JVM虚拟机栈(Java代码),和本地方法栈(C++),这个栈包含了方法的调用关系
3.元数据区(以前叫做方法区),放的是类对象,代码中的每个类,在JVM上运行的时候,都会有对应的类对象,Text.class还存放了方法相关的信息,类有一些方法,每个方法都代表了一系列的"指令集合"(JVM指令集合)
4.程序计数器,是内存区域中最小的一个区域,只需要保存,当前要执行的下一条指令(JVM字节码)的地址,这个地址就是元数据区里面的一个地址,JVM的PC保存的地址是JVM字节码的地址
关于内存中的地址划分
内存存放基本原则:1)局部变量,堆上
2)成员变量,堆上
3)静态成员变量 方法区/元数据区
上述四个区域中,堆和元数据区,是整个进程只有一份,栈和程序计数器是每个线程都有,多个线程同享一份数据,每个线程的局部变量,不是共享的,每个线程有自己的一份
类加载的过程
当前写的Java代码,是.java文件(硬盘),一个Java进程要跑起来,需要执行cpu指令,通过字节码让JVM翻译出来,就需要把.java文件变为.class文件(硬盘),再加载到内存上,得到类对象
重要过程
1.加载:在硬盘上,找到对应的.class文件,读取文件内容
2.验证:检查.class里的内容,是否符合要求,把读取出来的内容,往这个格式里套,看能不能套进去
u4是unsigned int 无符号的int u2是unsigned short 无符号的short
3.准备:给类对象,分配内存空间(在元数据区中),类加载最终要得到的就是类对象,会先把这个空间里的数据全填为0
4.解析:针对字符串常量进行初始化,把刚才.class文件中的常量内容取出来,放到元数据区
5.初始化:针对类对象中的各部分进行初始化(不是针对对象初始化,与构造方法无关)
双亲委派模型
这是一个类加载的机制,根据代码中写的"全限定类名"(包名+类名,例如Java.lang.String)找到对应的.class文件
这个模型描述了JVM加载.class文件过程中,找文件的过程,这个模型中内置了三个类加载器,在JVM中包含了一个特定的模块/类,这个类负责完成后续类加载的工作
JVM中内置了三个类加载器(负责加载不同的类)
1.BootStrapClassLoader,负责加载标准库的类,这个类是Java官方给出的"标准类",
2.ExtentionClassLoader,负责加载JVM扩展库的类
3.ApplicationClassLoader,负责加载第三方库的类和你自己写代码的类
三者之间的关系
此处的父子关系,不是通过类的继承表示的,而是通过类加载器中有一个"parent"这样的字段,指向自己父亲的地址,类似于二叉树的三叉实现
工作过程如下:例如给定一个全限定类名,Java.Test,此时加载过程如下
1.工作从ApplicationClassLoader开始进行,这个类加载器并不会立即从第三方库/自己写的代码开始搜索,而是交给自己的父亲ExtentionClassLoader去处理
2.工作就到了.ExtentionClassLoader,这个类加载器也不会立即从JVM扩展库开始搜索,而是交给自己的父亲BootStrapClassLoader去处理
3.工作就到了BootStrapClassLoader,这个类加载器,也不会立即从标准库中开始搜索,而是继续交给自己的父亲,由于自己的父亲为null,只能自己来处理,BootStrapClassLoader尝试在标准库的路径上开始搜索,如果这个类找到了,搜索过程完成,然后打开文件进行后续操作,如果没找到则交给自己的儿子来进行处理,
4.工作回到了.ExtentionClassLoader这个类加载器尝试在JVM扩展库的路径上开始搜索,如果这个类找到了,搜索过程完成,然后打开文件进行后续操作,如果没找到则交给自己的儿子来进行处理,
5.工作回到了ApplicationClassLoader,这个类加载器尝试在第三方库/自己写的代码中的路径上开始搜索,如果这个类找到了,搜索过程完成,然后打开文件进行后续操作,如果没找到则交给自己的儿子来进行处理,由于它的儿子为null,所以会抛出一个异常ClassNotFoundException
上述工作流程,主要应对这个场景,如果自己写的一个类和标准库/扩展库中的类冲突了,此时JVM就会确保标准库/扩展库的类加载成功,
类加载器并非只有三个,还可以手动写更多的类加载器,添加到中间
JVM的垃圾回收机制GC
垃圾回收机制,是Java提供的对于自动回收的机制,自动回收相对于C++的手动回收来命名的,C++回收需要手动free函数,可能会遗漏这个操作,
GC需要消耗额外的系统资源,而且存在非常影响效率的"STW"(stop the world)问题,GC回收的是内存,更准确的说是"对象",回收的是堆上的内存
1)程序计数器(不需要额外回收,线程销毁,自然就回收了)
2)栈(不需要额外回收,线程销毁,自然回收)
3)元数据区(一般也不需要,都是加载类,很少有"卸载类")
4)堆(GC回收的主力军)
一定是一次回收一个完整的对象,把对象中的成员全都回收
JAVAGC机制有一个方法,GC机制有两个方法
GC的主要流程
1.找到谁是垃圾,不被继续使用的对象
使用对象都是通过引用的方式来使用,如果没有引用指向这个对象,意味着这个对象注定无法在代码被使用,被视为垃圾了,对于JVM来说不是实时的,JVM需要一定的时间周期
如何判断某个对象是否有引用指向呢?
1)引用计数(不是JVM的方案,是Python和PHP的方案)
当引用计数为0时,这个对象就是垃圾了
缺陷如下:
1)消耗额外的存储空间:如果对象的空间比较大,浪费的空间很小,但是如果对象的空间比较小,浪费的空间就会特别大了
2)存在"循环引用"的问题
2)可达性分析(是JVM采取的方案)
解决了空间问题,也解决了循环引用的问题,但是时间上效率变慢,通过"遍历",JVM把对象之间的引用关系,理解成一个"树形结构",JVM就会不停的遍历这个结构,把所有能够访问到的对象标记成"可达",剩下的就是"不可达"
2.释放对应的内存
由于可达性分析,需要消耗一定的时间,因此,JAVA垃圾回收,没法做到实时性,会周期性进行扫描(JVM提供了一组专门负责GC的线程,不停的进行扫描工作)
清理垃圾的策略:
1)标记-清除,直接把视为垃圾的对象对应的内存给释放掉,这样的做法会造成内存碎片化,后续很难申请到连续的空间,申请内存都是需要连续的
2)复制算法
3)标记-整理
上述这三个方法,都是铺垫,JVM中实际的方案,是综合上述的方案,更复杂的策略,分代回收(分情况讨论,根据不同的场景/特点选择合适的方案),根据对象的年龄(GC有一组线程,进行周期性扫描,没有成为垃圾的对象每进过一轮年龄+1)
Eden是伊甸区,S0 S1是新生区/幸存区