了解JVM
目录
一、内存区域划分
1.方法区(元数据区)
2.堆
3.栈
4.程序计数器
5.本地方法栈
总结:
二、类加载
1.加载
2.验证
3.准备
4.解析
5.初始化
三、双亲委派模型
四、垃圾回收
1.找到垃圾
1)引用计数
2)可达性分析
2.回收垃圾
1)标记清除
2)复制算法
3)标记整理
分代回收
JVM内部涉及到的内容非常广泛,这里主要讨论三个方面的主题。
- JVM内存区域划分
- JVM中类加载的过程
- JVM中的垃圾回收机制
一、内存区域划分
JVM运行时数据区域也叫内存布局,但需要注意的是它和java内存模型(java memory model,简称JMM)完全不同,属于两个不同的概念。
- 内存区域划分解决的是“数据”存哪里的问题。
- JMM解决的是“多线程下保证数据读写安全”的问题。
一个运行起来的java进程就像是一个JVM虚拟机,就需要从操作系统申请一大块内存,把这块内存划分成不同的区域,每个区域都有不同的作用。
JVM会把这块内存划分成下面区域。
1.方法区(元数据区)
用于存储被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等数据的。
补充:
JDK1.8元空间的变化:
JDK8中把字符串常量池移动到了堆中。
运行时常量池是方法区的一部分,存放字面量与符号引用。
- 字面量:字符串(JDK8d+移动到了堆中)、final常量、基本数据类型的值。
- 符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
2.堆
堆的作用:程序中创建的所有对象都在保存在堆中。
我们常见的JVM参数设置 -Xms10m最小启动内存是针对堆的,-Xms10m最大运行内存也是针对堆的。
ms是memory start 简称,mx是memory max的简称。
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定GC次数之后还存活的对象会放入老生代。新生代还有3个区域:一个Endn +两个Survivor(S0/S1)。
垃圾回收的时候会将Endn中存活的对象放到一个未使用的Survivor中,并把当前的Endn和正在使用的Survivor清除掉。(后面的内容会讲到垃圾回收机制)
3.栈
java虚拟机栈的作用:java虚拟机栈的生命周期和线程相同,java虚拟机描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
java虚拟机栈中的栈帧中包含了一下4个部分:
1.局部变量表:存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要再帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
2.操作栈:每个方法会生成一个先进后出的操作栈。
3.动态链接:指向运行时常量池的方法引用。
4.方法返回地址:PC寄存器的地址。
4.程序计数器
程序计数器的作用:用来记录当前下一条执行的指令在内存中的哪个地方。
刚开始调用的方法,程序计数器记录的就是方法的入口地址,随着一条一条的执行指令,每执行一条指令,程序计数器的值都会自动更新指向下一条指令。
5.本地方法栈
本地方法栈和虚拟机栈类似,只不过java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的。
总结:
每个进程有自己的程序计数器,栈空间和本地方法栈,而这些线程之间共用同一份堆和方法区。
二、类加载
对于一个类来说,它的生命周期是这样的:
其中前5步是固定的顺序并且也是类加载的过程,其中中间的3步我们都属于连接,所以对于类加载来说总共分为以下几个步骤:
1.加载
2.连接
1)验证
2)准备
3)解析
3.初始化
1.加载
所谓的加载,就是类加载器通过全限定名定位 .class文件或其他资源,读取字节码数据,找要找的类的信息并加载到内存中并生成Class对象,并且在同一个类加载器内一个.class文件所生成的Class对象是唯一且对应的。
补充:
Class对象并非是实例new后的对象,而是java中
java.lang.Class
类的实例(Class对象存储在堆区),用于描述存储类的元信息(如方法,属性,继承结构等)。
比如在执行代码中遇到class A,但是内存中没有class A的相关信息,于是就会从.class文件或其他资源中去找到class A的信息并加载到内存中。
加载loading的阶段,jvm需要完成一下三件事情:
- 通过一个类的全限定名例如:java.lang.A 来读取.class文件或其他资源,找到类A,获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 方法区创建类A的元数据(类型信息、方法代码等),并在堆中生成一个代表这个类的java.lang.Class对象,作为访问入口。
2.验证
验证这一阶段的目的是确保Class文件的字节流中包含的信息符合 《java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身安全。
验证选项:
- 文件格式验证
- 字节码验证
- 符号引用验证....
3.准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
这里只是分配内存空间,还没有初始化,此时这个空间上的内存的数值就是全0。
4.解析
解析阶段是java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
5.初始化
初始化阶段,java虚拟机真正开始执行类中编写的java程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程,即针对类对象进行初始化,把类对象中需要的各个属性都设置好。
三、双亲委派模型
双亲委派模型属于类加载第一个步骤,“加载”过程中的这一个环节。
双亲委派模型中,类加载器通过全限定名(如java.lang.String)定位类资源,并依据层级委派规则并创建对应的类对象。
常见定位的类资源来源包括:
- 本地文件系统的.class文件(如用户编写的类);
- jar包中的.class文件(如第三方依赖);
- 网络传输的字节流;
- jvm核心类库。
类加载器:
是jvm中的一个模块,JVM中,内置了三个类加载器
1.BootStrap ClassLoader
2.Extension ClassLoader
3.Application ClassLoader
双亲委派模型原理:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类)传递给子加载器,子加载器才会尝试自己去完成加载。
类加载过程:
- 给定一个类的全限定类名,形如java.lang.String。
- 从Application ClassLoader 作为入口,开始执行查找的逻辑。
- Application ClassLoader,不会立即去扫描自己负责的目录,而是把查找的任务交给它的父亲,即Extension ClassLoader。
- Extension ClassLoader也不会立即去扫描自己负责的目录,而是把查找的任务,再交给它的父亲,即BootStrap ClassLoader。
- BootStrap ClassLoader,也不想立即扫描自己负责的目录,也想要把任务交给它的父亲,结果发现自己没有父亲,因此BootStrap Loader只能亲自扫描标准库的目录。
- 如果找到了,就执行后续 的类加载操作,此时查找过程结束,如果没有找到,还是把任务交给其孩子(Extension ClassLoader)执行。
- 如果还是没有扫描到,就会回到Application ClassLoader,Application ClassLoader就会负责扫描当前项目和第三方库的目录。
- 如果还是没有找到,就会抛出一个ClassNotFoundException。
四、垃圾回收
垃圾回收(Garbage Collection GC)是一种自动内存管理机制,由虚拟机(JVM)在程序运行时动态识别并回收不再被引用的对象的内存,防止内存泄露和溢出,降低开发者手动管理内存的负担。
垃圾指的是内存中没有引用指向的对象。
垃圾回收机制可以理解成两大步骤:
1.找到垃圾
2.回收垃圾
1.找到垃圾
在GC的圈子中,有两种主流的方案:引用计数;可达性分析。
1)引用计数
在java中,引用与对象关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以被回收。如果一个对象被引用一次,引用计数+1,反之没有引用计数归零了,该对象不被引用,则被视为垃圾,并且被GC回收利用。
然而引用计数并不被java所引用,原因存在两种:
- 比较浪费内存:
对象占据内存空间越小,计数器占比的空间就越大,这样计数器占据的空间就难以忽视。
- 引用计数机制,存在“循环问题”会导致对象清理不掉
2)可达性分析
可达性分析,本质上是时间换空间这样的手段。
有这么一个或一组线程,周期性的扫描我们代码中的所有对象,从一些特定的对象出发,尽可能的进行访问的遍历,把所有能够访问到的对象,都标记“可达”,反之,经过扫描后,未标记的对象就是垃圾了。
2.回收垃圾
回收垃圾有三种基本思路:标记清除;复制算法;标记整理。
1)标记清除
把对应的对象直接释放掉,就是标记清除的方案。
注:灰色标记
把标记的对象直接释放掉,就是标记清除的方案。
但是这个方案其实非常不好,会产生很多的内存碎片,当我们在申请内存的时候,申请到的都是“连续”的内存空间。随着时间的推移,内存碎片的情况就会越来越多,导致后面申请内存变得困难。
2)复制算法
通过复制的方式,把有效的对象归类到一起,然后再统一释放剩下的空间。
把内存分成两份,一次只用其中的一半,这个方案可以有效解决内存碎片的问题,但是,缺点也很明显:
1.内存要浪费一半,利用率不高。
2.如果有效的对象非常多,拷贝开销就很大。
3)标记整理
既能够解决内存碎片的问题,又能够处理复制中算法利用率的问题。
方法是先标记删除,然后再整理内存,类似与顺序表中删除元素的搬运操作,但是搬运的开销仍然很大。
实际上,JVM采用的释放思路,是上述基础思想的结合体,叫做“分代回收”机制。
分代回收
分代回收的其目的是识别并回收不被引用的对象,从而释放器占用的内存空间,是JVM中主要的回收的思想方法。
JVM将内存分为不同区域,根据对象生命周期差异化处理。
- 年轻代:存储新创建的短期对象,通过可达性分析扫描后,大部分对象会变成垃圾。
- 老年代:存储长期存活的对象,是使用标记-清除或标记-整理算法释放内存。
具体回收流程:
1.标记阶段:标记所有可达对象;
2.清除/整理阶段:
- 对年轻代使用复制算法,将存活的对象复制到年轻代的一块Survivor区,直接释放,然后再直接释放原区域内存,经过多次GC扫描之后,若有对象短时间内释放不掉就会把这个对象拷贝到老年代中。
- 对老年代使用标记-清除(释放离散垃圾内存)或标记-整理(压缩内存消除碎片)。