整理有误烦请评论区提醒,及时改进~
一、JVM内存模型:
- JVM 作用:实现跨平台的基础,一次编译,到处运行。
-
JVM生命周期:随程序开始而创建,结束而销毁。
方法区:共享内存区域
1.存储已经被虚拟机加载的:类信息、常量、静态变量、即时编译器编译后的代码。
2.常量池:存放编译期生成的各种字面量(常量,如:int a = 1; String str = "abc")和符号引用量(类和接口的全限定名、字段名和描述符、方法名描述符),基本数据类型(Double、Float浮点数没有且没必要实现常量池技术)的常量池是-128到127之间,在这个范围中的基本数据类的包装类可以自动拆箱,比较时直接比较数值大小。注:装箱:自动将基本数据类型转换为包装器类型;拆箱:自动将包装器类型转换成基本类型,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx基本数据类型)。
编译期和运行期都可以将常量放入常量池中,内存有限,无法申请时抛出OOM异常。
1.好处:避免频繁的创建和销毁对象而影响系统性能、实现对象共享。堆:是JVM启动时创建的最大的一块内存区域(一个进程有且仅有一个Java堆),被线程共享,用于存放「对象实例」和「数组」,堆内存的空间在逻辑上是连续的,在物理地址上是不连续的,GC主要活动区域,同样也会抛出OutOfMemoryError。
1.JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;
2.JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。
3.默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
4.空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。-
Java虚拟机栈:为Java方法(字节码指令集)服务。
1.线程私有,生命周期同线程;
2.描述的是Java方法的内存模型,每个方法执行时都会创建一个栈帧,栈帧存储:「局部变量表」、「操作数栈」、「动态链接」、「方法出口」等信息。
3.方法从调用到执行结束,对应此栈帧在虚拟机栈中入栈到出栈的过程。
4.「局部变量表」存放方法参数和方法内部定义的局部变量:基本数据类型(boolean、byte、char、short、int、float、long、double)、reference类型(对象引用,操作堆上的具体对象,指向堆中句柄池里「到对象实例数据的指针」或「到对象类型数据的指针」来找到具体对象。注:对象实例数据:对象中各个实例字段的数据,对象类型数据:对象的类型、父类、实现的接口、方法等)
、returnAddress类型(指向一条字节码指令的地址)。注意:对于成员变量和定义在方法外的对象的引用则存储在堆中
5.容易出现的Error:超过栈最大深度:StackOverflowError(堆栈溢出:通过-Xss设置栈大小、递归、死循环容易造成此类问题)、无法申请足够内存:OutOfMemoryError(内存溢出)。
6.虚拟机内存有限,分配某一栈内存过大会挤占其他线程空间、GC不涉及虚拟机栈 本地方法栈:为本地Native方法服务。
同样会出现StackOverflowError(堆栈溢出)、OutOfMemoryError(内存溢出)异常。程序计数器:记录当前程序执行到哪一步了
1.特点:唯一无OutOfMemoryError情况的区域;每个线程都有自己的计数器(线程私有);内存空间很小可忽略不计,也是运行速度最快的存储区域;
2.前提:一个CPU每次只能处理一个程序,CPU会为每个程序分配时间片,多线程在特定的时间段内只会执行其中某一个线程中的一条指令。CPU不停的做任务切换,导致大量的中断和恢复,为了保证准确的记录各个线程正在执行的当前字节码指令地址,为每个线程分配独立的计数器以避免相互干扰。
2.作用:任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定值(undefned)。
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
二、垃圾回收机制
※判断对象是否可回收的机制:
-
引用计数法(reference counting):给对象添加引用计数器,缺点是对象循环引用则无法回收。JVM没有使用此算法,而是使用「根搜索算法」GC Roots。
-
可达性分分析(GC Roots):通过一系列名为 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称之为引用链,当一个对象到 GC Roots 没有任何引用链相连的时候说明对象没有被引用了。解决了对象循环引用的问题,即使a和b相互引用,但只要从GC Roots无法到达a或b则都不属于存活对象。缺点是在多线方程环境下其他线程可能更新了对象中的引用,导致误报(将引用设为null):会让此次GC无法回收此垃圾对象;导致漏报(将引用设为未访问过的对象):错误的回收了还在引用的对象,导致Java虚拟机崩溃。
好在判断对象真正死亡需要标记两次,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象就会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可。finalize() 方法只会被系统自动调用一次。
※主流的垃圾收集算法:
-
标记- 清除算法(mark-sweep):当堆中有效内存空间被耗尽时,会停止整个程序然后进行标记(mark)和清除(sweep)工作。
1.标记(mark)阶段:从引用根节点开始遍历,标记所有被引用的对象并在对象头(Object Header)中记录为可达对象。注:Object Header:包含对象基本信息,如布局、GC状态、同步状态、hash code(由JVM计算的hash code)、数组长度(前提是数组)
2.清除(sweep)阶段:对堆内存从头到尾进行 线性遍历,如果发现object header中没有标记可达对象,则将其回收。
3.缺点:效率低、GC进行需要停掉整个程序、清理出来的空间不连续,容易产生碎片。
-
复制-清除算法(copying):把内存分为 From 和 To 两个空间,对象一开始只在 From 空间分配,GC时把 From 空间中存活的对象复制到 To 空间,复制完成后 To 空间变为 From 空间,清除之前的 From 空间并变成 To空间,如此反复。
优点:1.只记录存活对象,对比「标记-清除算法」少了搜索整个堆内存时间,效率相对高。2.少了碎片化内存,空间连续。
缺点:需要空闲出一半的内存,内存浪费。
-
标记-压缩算法(mark-compact):分为标记和压缩两个阶段,标记阶段同「标记-清除算法」,然后遍历数次堆进行压缩,移动对象,把存活的对象重新填装,使对象紧挨着,避免内存碎片。
※JVM的垃圾回收策略:分代回收!合理划分内存区域并根据各个区域定制不同的回收算法。划分区域如下图:
分代回收机制流程:新创建的对象放入Eden区,当此区域的内存使用到达阈值时触发Young GC,然后将Eden区存活的对象复制到From区,下次再触发Young GC再将Eden区和From区存活的对象放入To区,From区和To区频繁的来回复制,存活次数过多的对象就会进入Old老年区。
※GC类型:Scavenge GC和Full GC两种类型。
- Scavenge GC:一般当新对象生成,并且在Eden申请空间失败时,就好触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor幸存区。然后整理Survivor的两个区(From、To)。
- Full GC:对整个堆进行整理,包括Young、Tenured(老年代)和Perm(永久代)。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC:1.Tenured被写满、2.Perm被写满、3.System.gc()被显示调用、4.上一次GC之后Heap的各域分配策略动态变化。
※垃圾回收器:串行收集器、并行收集器、并发收集器,选用哪种JVM会根据当前系统配置进行判断:
- 串行处理器:使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高,使用-XX:+UseSerialGC打开。
适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
缺点:只能用于小型应用 - 并行处理器:对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。
适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
缺点:应用响应时间可能较长 - 并发处理器:可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
※引用的回收:强>软>弱>虚
强引用:Object obj = new Object(); 创建的,只要强引用在就不回收。
软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为对象设置虚引用唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
※GC优化:
GC算法触发时,因为线程不安全,除GC所需的线程外,所有线程都进入等待状态(native 方法可以继续执行但也不能跟JVM交互),直到GC任务完成,这个阶段叫做「stop-the-world」!所以GC优化尽可能减少GC的触发时机。
GC回收的区域位于「堆」和「方法区」内的对象。「栈」里的数据在超出作用域后会被JVM自动释放掉。
三、类加载机制
概述:JVM把.class字节码文件加载到虚拟机内存中的过程。
- 我们编写业务的.java文件,Java编译器将其转化为.class文件,JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
- 运行时非全部一次性加载,而是按需加载。
-
一个class类通常只加载一次,之后从jvm的class实例的缓存中获取而免去文件系统中加载.class文件了(🌰:JVM在执行某段代码时遇到class A,然后去缓存中找,找不到会去相应的class文件找class A的类信息并加载到内存中)。
类加载过程:
类加载主要有三个过程:加载(loading)、连接(linking)、初始化(initializing)。其中连接又分三个步骤:校验、准备和解析。
- 加载(Loading):把.class文件转化成静态数据结构,存储在方法区内,并在堆中生成一个便用用户调用的Java.lang.Class类型的对象
- 连接(Linking):
- 校验(verification):校验加载进来的.class文件是否符合标准,不符合标准拒绝加载到内存。(这块主要验证的是元数据和字节码,为了确保此class文件是安全的,不会危害虚拟机的自身安全。)
- 准备(preparation):将.class文件的静态变量赋 初始值,初始值都为0,但被final修饰的初始值 = 默认值,即声明的值。(JDK8之前永久代实现方法区,存放类的元信息、常量池、静态变量等。JDK8改为元空间实现方法区,存放类的元信息,而常量池和静态变量则存在了堆中)
- 解析(resolution):把.class文件常量池中用到的符号引用转化成直接内存地址,可以访问到的内容。(此阶段会验证符号引用)
-
初始化(initializing):成功初始化,此阶段给静态变量赋予初始值,
双亲委派机制:
解释:当有一个类需要被加载时,首先要判断这个类是否已经被加载到内存,判断加载与否的过程是有顺序的,如果有自己定义的类加载器,会先到custom class loader 的cache(缓存)中去找是否已经加载,若已加载直接返回结果,否则到App的cache中查找,如果已经存在直接返回,如果不存在,到Extension中查找,存在直接返回,不存在继续向父加载器中寻找直到Bootstrap顶层,如果依然没找到,那就是没有加载器加载过这个类,需要委派对应的加载器来加载,先看看这个类是否在自己的加载范围内,如果是直接加载返回结果,若不是继续向下委派,以此类推直到最下级,如果最终也没能加载,就会直接抛异常 ClassNotFoundException,这就是双亲委派模式。如下图:
类加载顺序
父类的静态字段 -->父类的静态代码块 -->子类静态字段 -->子类静态代码块 -->父类成员变量(非静态字段) -->父类非静态代码块 -->父类构造器 -->子类成员变量 -->子类非静态代码块 -->子类构造器。