目录
3、加载一个类采用Class.forName()和ClassLoader.loadClass()区别
一、类加载机制
1、类加载的生命周期
Java中的类加载是一种懒加载的形式。只有当这个类需要被使用的时候才会去加载,这样做的好处就是避免一次性加载全部,从而占用很大的内存。
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
public class User{
public static final int a = 1; //静态常量 --->准备阶段赋值
public static int b = 2;//类变量 ---> 准备阶段赋值为null,初始化阶段赋值为具体的值
public int c = 2;//实例变量 ---> 创建对象的时候赋值
static{
... //类代码块 ---> 初始化阶段执行
}
{
... //实例代码块 ---> 创建对象的时候执行
}
public User{
... // 构造器 ---> 创建对象的时候执行
}
}
- 加载: 通过类加载器将class文件加载到jvm
- 连接
- 验证:验证class文件是否是合法的,保证jvm的安全
- 准备:为类变量赋默认的初始值(int为0,long为0L,boolean为false,引用类型为null,常量直接赋值)
- 解析:把类中的符号引用转换为直接引用
- 初始化:当我们要new一个对象,为类变量赋予正确的初始值,并执行其类代码块,实例代码块等
- 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据
- 卸载: 结束生命周期(1、该类的所有实例都被GC。2、加载该类的类加载器被GC。3、该类对象没有任何地方引用)
继承时,父子类的初始化顺序:
父类--->静态变量,父类--->静态代码块
子类--->静态变量,子类--->静态代码块
父类--->变量,父类--->代码块,父类--->构造器
子类--->变量,子类--->代码块,子类--->构造器
2、类加载器
-
启动类加载器: Bootstrap ClassLoader(负责加载Java-home/jre/lib目录下的jar包;被-Xbootclasspath参数指定的路径中的类库)
-
扩展类加载器: Extension ClassLoader(负责加载Java-home/jre/lib/ext目录下的jar包;由java.ext.dirs系统变量指定的路径中的类库)
-
应用程序类加载器: Application ClassLoader(负责加载用户类路径下的jar包)
-
自定义类加载器: 自定义加载器需要继承
ClassLoader
,重写findClass()方法或loadClass()
方法;-
findClass()方法:
重写该方法,此类加载器不会打破双清委派模型; -
loadClass()
方法:重写该方法,此类加载器会打破双清委派模型,不建议重写此方法;
-
3、加载一个类采用Class.forName()和ClassLoader.loadClass()区别
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行初始化
ClassLoader.loadClass()
:只干一件事情,将.class文件加载到jvm中,不会对类进行初始化
4、Tomcat的类加载机制
可以看到,在原来的Java 的类加载机制基础上,Tomcat 新增了3 个基础类加载器和每个Web 应用的类加载器+JSP 类加载器;(一个应用包含一个独立的WebAPP和JSP类加载器)
- 在Tomcat6之前:Common类加载器、Server类加载器、Share类加载器分别加载lib、Server、Share目录下的jar包;(在conf/catalina.properties中进行配置)
- 在Tomcat6之后:Common类加载器、Server类加载器、Share类加载器加载lib目录下的jar包,删除了Server目录和Share目录。
- WebAPP类加载器负责加载WebAPP目录下的jar包,该加载器打破了双亲委派模式,重写了loadClass方法;
- JSP类加载器负责加载JSP相关的文件;
4、双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
过程:
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
好处:
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
二、JVM的运行时数据区
JVM整个运行原理图:
JVM内存结构划分:
1、程序计数器
1、程序计数器是一块较小的内存空间;是私有的;
2、是唯一不会出现 OutOfMemoryError
的内存区域;
3、随着线程的创建而创建,随着线程的结束而死亡;不会出现垃圾回收 ;
4、在多线程的情况下,用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
2、Java虚拟机栈
1、线程私有的;方法是先进后出;
2、方法的执行会创建栈帧,栈帧中保存局部变量表、运行数据(操作数栈)、动态链接、方法返回地址、附加信息;
3、可能会出现异常:
- 栈内存溢出(tackOverflowError :线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量)
- 栈内存不足(OutOfMemoryError:在尝试扩展的时候无法申请到足够的内存或在创建新的线程时没有足够的内存去创建对应的虚拟机栈)
4、随着线程的创建而创建,随着线程的结束而死亡;不会出现垃圾回收 ;
5、可以通过参数-Xss
来设置线程的最大栈空间;默认为1M;
- 局部变量表:主要存放了基本数据类型、引用类型;
- 运行数据(操作数栈):用于存放方法执行过程中产生的中间计算结果和临时变量;
- 动态链接:指向元空间方法引用的地址
- 方法返回地址:方法正常退出或异常退出的地址
- 附加信息
3、本地方法栈
1、与虚拟机栈类似,区别是本地方法栈是为本地Native方法所服务的;
2、随着线程的创建而创建,随着线程的结束而死亡;不会出现垃圾回收 ;
4、堆
1、线程共享的区域;
2、虚拟机在启动时进行创建;
3、是虚拟机所管理的最大的一块区域;
4、存放所有的实例对象和数组;
5、是GC垃圾回收器主要负责的区域;
6、堆中可以分为新生代(新对象和没达到一定年龄的对象)和老年代块(被长时间使用的对象或大对象 )默认为1:2;新生代分为Eden、From、To,默认大小为8:1:1
7、可以通过-Xms和-Xmx调整堆得大小;
8、当堆空间不足,在向堆中存放对象时,会出现OutOfMemoryError
异常(堆内存溢出)
9、对象头中的Mark Word采用4个bit位来保存年龄,4个bit位能表示的最大数就是15!所以年轻代GC的最大年龄为15
JVM中对象如何在堆内存进行分配:
- 指针碰撞:内存规整的情况下
- 空闲列表:内存不规整的情况下;
堆内存中对象的布局:
- 对象头:由2部分组成;
- 存储对象自身运行时数据:哈希码、GC分代年龄、锁状态、偏向线程ID、线程持有锁
- 类型指针:对象指向他类的元数据指针,通过这个指针可以确定对象是哪个类的实例
- 实例数据:代码中所定义的成员变量类型的字段内容
- 对齐填充:不是必须存在,主要是占位,保证对象大小是某个字节的整数倍(HotSpot 虚拟机是8字节的整数倍)
5、方法区
1、在JDK1.8 开始才出现元空间的概念,之前叫方法区/永久代
2、元空间与 Java 堆类似,是线程共享的内存区域
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;XX:MetaspaceSize=20M ;-XX:MaxMetaspaceSize=20m(分别为初始值和最大值)
5、元空间很少有 GC 垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以 GC很少来回收;
6、元空间内存不足时,将抛出 OutofMemoryError;
方法区在 JDK6、7、8中的演进细节
6、字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
为什么要将字符串常量池移动到堆中
因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存
7、JVM内存异常
内存异常有两种:内存溢出和内存泄漏,JVM内存异常也存在这两种内存异常的情况。
- 内存溢出:分配内存时,发现内存不够用
- 内存泄漏:回收内存时,已经不被占用的内存无法被正常回收,造成闲置但无法被重新分配的情况
JVM内存指JVM的运行时数据区,包括程序计数器、堆、虚拟机栈、本地方法栈以及方法区。其中,程序计数器不会发生内存异常的情况。
JVM内存异常时,有两种错误提示类型,包括栈溢出(StackOverflowError)和内存溢出(OutOfMemoryError)。
JVM不区分虚拟机栈和本地方法栈的内存异常情况,一视同仁为栈
StackOverflowError
发生的区域:虚拟机栈、本地方法栈
发生的场景:
- 对于深度固定的栈,在其首次分配内存时,无法获取足够内存时,会报StackOverflowError
- 对于可扩展的栈,在首次分配内存时,无法获取足够内存时,会报StackOverflowError
OutOfMemoryError
发生的区域:虚拟机栈、本地方法栈、堆、方法区
发生的场景:
- 栈:对于非固定大小的栈,在其扩容时,如果没有办法获取到足够大小的内存,报OutOfMemoryError
- 堆:触发过垃圾回收后,仍然分配不到足够的内存空间,会报OutOfMemoryError
- 方法区:触发过垃圾回收后,仍然分配不到足够的内存空间,会报OutOfMemoryError
三、JVM 垃圾回收
1、内存分配策略
-
对象优先在 Eden 区分配:
-
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC;(对象经历一次Minor GC,对象的年龄就会+1)
-
JVM 会把存活的对象转移到S0或S1中;在S1或S0中的对象同样也会经历Minor GC;
-
如果执行完Minor GC后,对象还是无法存入Eden区,此时会将对象存放在老年代中;
-
如果老年代中没有足够的空间,会执行Full GC
-
-
大对象直接进入老年代:
-
大对象需要大量连续内存空间的对象(比如:字符串、数组)
-
大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
-
-
长期存活的对象将进入老年代:
-
对象在 S0 中每熬过一次 Minor GC,年龄就+1,当它的年龄增加到一定数量时,就会被存放到老年代中;(并行垃圾回收器默认为15,CMS垃圾回收器默认为6)
-
可以通过参数
-XX:MaxTenuringThreshold
来设置默认值。
-
2、如何判断一个对象是否可以回收
- 引用计数算法
- 给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
- 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
- 可达性分析算法
- 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收;
- 可以被作为 GC Roots 的对象:
- 虚拟机栈或本地方法栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁(synchronized关键字)持有的对象
对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;
可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize
方法。当对象没有覆盖 finalize
方法,或 finalize
方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
3、对象的引用类型
- 强引用(StrongReference):
- 使用 new 一个新对象的方式来创建强引用;
- 被强引用关联的对象不会被回收。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会回收强引用的对象来解决内存不足问题。
- 软引用(SoftReference):
- 使用 SoftReference 类来创建软引用。
- 如果内存空间足够,GC就不会回收软引用对象,如果内存空间不足了,就会回收软对象的内存。
- 在MyBatis的缓冲中使用;
- 弱引用(WeakReference)
- 使用 WeakReference 类来实现弱引用
- 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
- 在ThreadLoad类中使用;
- 虚引用(PhantomReference):
- 使用 PhantomReference 来实现虚引用。
- 一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被GC回收;
-
虚引用主要用来跟踪对象被垃圾回收的活动。
4、JVM对象动态年龄判断
虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold=15 才能晋升老年
代;
结论:动态年龄判断: Survivor 区的对象年龄从小到大进行累加,当累加到 X 年龄(某个年龄)时占用空间的总和大于 50% (可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是 50),那么比 X 年龄大的对象都会晋升到老年代6、
5、老年代空间分配担保机制
新生代 Minor GC后剩余存活对象太多,无法放入 Survivor 区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?
1、执行任何一次 Minor GC 之前,JVM 会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代 Minor GC 之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;
2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起次 Minor GC,因为 Minor GC 之后即使所有对象都存活,Survivor 区放不下了,也可以转移到老年代去;
3、如果执行 Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次 Minor GC 后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次 Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下 Minor GC,但是此时真的可能有风险,那就是 Minor GC 过后,剩余的存活对象的大小,大于 Survivor 空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”
4、如果 Full GC之后,老年代还是没有足够的空间存放 Minor GC 过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;
6、什么情况下对象会进入老年代
1、躲过 15 次 GC之后进入老年代,可通过JM 参数"-XX:MaxTenuringThreshold”来设
置年龄,默认为 15岁
2、动态对象年龄判断
3、老年代空间担保机制
4、大对象直接进入老年代
7、JVM内存相关核心参数
1、-Xms Java 堆内存的大小;
2、-Xmx Java 堆内存的最大大小;
3、-Xmn Java 堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小
4、-XX:MetaspaceSize元空间大小;
5、XX:MaxMetaspaceSize 元空间最大大小;
6、-Xss 每个线程的栈内存大小
7、-XX:SurvivorRatio=8 设置 eden 区 和survivor 区大小的比例,默认是 8:1:1;
8、-XX:MaxTenuringThreshold=5 年龄闻值;默认15
9、-XX:+UseConcMarkSweepGC 指定 CMS 垃圾收集器
10、-XX:+UseG1GC 指定 G1 垃圾收集器
4、垃圾回收算法
-
标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
- 标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
缺点:内存的使用率只有一半
-
标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法
这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法。
- 在新生代-复制算法:
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集
- 在老年代-标记整理算法:
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
5、Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)
- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 会有这种行为
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
6、触发Full GC的情况
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
- 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
- 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
- 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
7、HotSpot 虚拟机中的垃圾回收器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
CMS 收集器流程:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记: 进行 GC Roots 跟踪 的过程(查找并标记GC Root对象所有可达的对象),它在整个回收过程中耗时最长,不需要停顿。
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除: 开始清除为标记的对象,不需要停顿。
G1收集器流程:
- 初始标记:需要暂定所有线程,并记录下GC Roots能直接引用的对象,速度很快。与CMS的初始标记一样
- 并发标记:可以与应用线程一起工作,进行可达性分析,与CMS的并发标记一样
- 最终标记:需要暂定所有线程),根据三色标记算法修复一些引用的状态,与CMS的重新标记是一样的
- 筛选回收: