类加载过程
类加载:类加载将class文件加载到虚拟机的内存。
1.加载:在磁盘中查找并通过IO读入字节码文件。
2.连接:执行校验、准备、解析步骤。
1).校验:校验字节码文件正确性(-Xverifynone 关闭大部分的验证)
2).准备:给类的静态变量分配内存,并赋予默认值
3).解析:将符号引用替换为直接引用,该阶段会把一些静态方法替换为指向数据所存内存的指针或句柄等,这是所谓的 静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的符合引用替换为直接引用。
4.初始化:对类的静态变量初始化为指定的值,执行静态代码块。
5.使用
6.卸载
类加载器
#启动类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库。
#扩展类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
#应用程序类加载器:
负责加载ClassPath路径下的类包,主要是自己写的类。
#自定义加载器:
负责加载用户自定义路径下的类包。
自定义加载器:
package cn.learn;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @Author xxt
* @DATE 2022/5/1 16:56
*/
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
int available = fileInputStream.available();
byte[] data = new byte[available];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("d:/test");
Class<?> clazz = classLoader.loadClass("cn.learn.Link");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("test", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
双亲委派机制
加载某个类时会先委托父加载器寻找目标类,父加载器找不到再委托上层父加载器加载,如果所有的父加载器在自己的加载路径下都找不到目标类,则在自己的类加载路径中查找并加载目标类。
为什么要设计双亲委派机制
沙箱安全机制:Java核心类库的类不会被除了启动类加载器之外的其他加载器加载,这样可以防止核心api被随意篡改。
避免类重复加载:当父类加载器加载了目标类,子加载器就没有必要再重新加载,保证被加载类的唯一性。
内存模型
栈:
栈帧:每一个方法对应一个栈帧 栈帧z主要包含以下几部分:
-
局部变量表
保存局部变量
-
操作数栈
-
动态链接
-
方法出口
方法区:
jdk 1.8 之后使用的是直接内存。主要存的是常量、静态变量、类元信息。
本地方法栈:
本地接口的作用是融合不同的编程语言为Java所用。
堆内存
VisualVM
VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序(Java 应用程序)的详细信息。
启动命令:jvisualvm
默认是没有Visual GC 这个工具的,需要下载。
1.点击工具,然后点击插件。
2.之后选择可用插件。找到Visual GC 选中,点击安装即可。
例子:
对象逃逸分析
JVM 三种运行模式:
1.解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码。
2.编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有的JVM字节码一次编译为机器码,然后一次性执行所有机器码。
3.混合模式(Mixed Mode):(-Xmixed 设置JVM使用混合模式)依然使用解释模式执行代码,但是对于一些“热点”代码采取编译器模式执行,这些热点代码对应的机器码会被缓存起来,下次执行无需再编译。
JVM一般采用混合模式执行代码。 这就是我们常见的JIT(Just In Time Compiler)即时编译技术。
在即时编译过程中JVM可能会对我们的代码做一些优化,比如对象逃逸分析等。
JVM运行模式 | 优点 | 适用场景 |
---|---|---|
解释模式 | 启动快 | 只需要执行部分代码,且大多数代码只会执行一次的情况 |
编译模式 | 启动慢,但是后期执行速度快,比较占用内存(因为机器码的数量至少是JVM字节码的十倍以上) | 适合代码可能会被反复执行的场景 |
混合模式 | 一般JVM所默认的模式 |
什么是对象逃逸分析?
对象逃逸分析就是分析对象的动态作用域,当一个对象在一个方法中被定义之后,它很有可能被外部方法所引用,例如,作为调用参数传递到其他地方中。
public User test1(){
User user=new user();
user.setId(1)
user.setName("张三")
return user;
}
public void test2(){
User user=new user();
user.setId(1);
user.setName("张三");
}
这里有两个方法,第一个方法把对象作为返回值返回,第二个方法只在方法内部定义了一个对象test1方法内的对象的作用域不确定,第二个方法对象我们可以认为这个方法结束后就变为无效对象了,那么第二个方法内的对象如果存放到堆内的话,只能靠GC来回收这个对象了,但这个对象是在我们这个方法结束后就没用了,是没有必要存到堆内的,JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
关闭和开启逃逸分析
#开启
-XX:+DoEscapeAnalysis
#关闭
-XX:-DoEscapeAnalysis
JVM内存分配与回收策略
对象优先再Eden去分配
- 大多数情况下,对象在新生代中Eden区分配,当Eden区中没有足够的空间,JVM将发起一次Minor GC。
Minor GC 和 Full GC 区别
- Minor GC/Young Gc:指发生再新生代的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
- Major GC/Full GC:一般会回收老年代、年轻代、方法区的垃圾。Major GC 的速度一般会比Minor GC 的慢10倍以上。
对象进入老年代的四种情况:
1.存活对象达到年龄阈值(默认为15)
2.大对象直接进入老年代(超过了JVM中-XX:PretenureSizeThreshold参数的设置)
3.survivor 空间中年龄1 + 年龄2 +年龄3+ … + 年龄n (n>1)的这些对象大小的总和大于 survivor 空间的一半, 年龄大于或等于该年龄 n 的对象就会直接进入到老年代。
4.Minor GC后,S区空间不能容纳全部存活对象,直接进入老年代。
老年代空间分配担保机制
-
在每次年轻代Minor GC 之前JVM 都会计算下老年代剩余可用空间。
-
如果这个空间小于年轻代现有的所有对象之和(包括垃圾对象),就会看一个
-XX:-HandlePromotionFailure
(JDK 1.8 默认设置)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每次minor后进入老年代的对象平均大小。 -
如果上一次结果是小于或之前说的参数没有设置,那么就会触发一次Full GC 。
-
如果回收完还是没有足够的空间存放新的对象,就会好生
OOM
。
-
判断对象是否可以被回收?
-
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就+1,当引用失效,计数器就-1。计数器为0 的对象就是不可能在被使用的。
-
可达性分析算法
这个算法的基本思想是通过一系列成为GC RootS 的对象作为起始点,从这些节点向下搜索,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Roots根节点:线程栈的本地变量、静态属性、常量、本地方法栈的变量等。
四种引用类型
-
强引用
普通的变量引用
-
软引用
将对象用SoftReference软引用类型的对象包裹,正常情况下不会被回收,但是GC之后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉,软引用可以用来实现对内存敏感度不高的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
-
弱引用
将对象用WeaKReference弱引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用。
public static WeaKReference<User> user = new WeaKReference<User>(new User());
-
虚引用
虚引用也称为幽灵引用或幻影引用,它是最弱的一种引用关系,几乎不用。
finalize()方法最终判断对象是否存活
即使在可达性分析算法中不可达的对象,也并非是直接被回收,真正要宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1.第一次标记并进行一次筛选
对象没有覆盖finalize()方法,直接被回收
2.第二次标记
如果这个对象覆盖了finalize()方法,只需要重新与引用链上的任何一个对象建立关联即可。
怎么判断一个类是无用的?
类需要同时满足已下三种情况:
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
查看当前 JDK 使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version
GC常用的算法:
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
目前主流的JVM(HotSpot)采用的是分代收集算法。
标记-清除(Mark-Sweep)算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
复制(Copying)算法
复制算法是将内存分为大小相同的两块,每次只用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次性清理。
标记-整理(Mark-Compact)算法
根据老年代的特点,垃圾回收的过程与标记-清除算法一样,不过不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
当前所有商用虚拟机都采用“分代收集“(Generational Collection)算法,这种算法根据对象存活周期的不同将内存划分成几块。一般是把Java堆划分为新生代和老年代,这样就可以根据各个代的特点选择最合适的收集算法。
新生代:对象存活率低,适用”复制算法“。
老年代:对象存活率高且没有额外的空间提供分配担保,适用”标记 - 清除算法“或”标记 - 整理算法“。
垃圾收集器
如果说垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体体现。
Serial 收集器
(-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial收集器是最基础、历史最悠久的垃圾收集器,通过名字可以联想到他是单线程工作的收集器。这个单线程指的是在他工作是要停止所有用户程序,“Stop The World”,直到它收集完成为止。
新生代采用复制算法,老年代采用标记-整理算法。
ParNew 收集器
(-XX:+UseParNewGC)
ParNew 收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和Serial收集器完全一样,默认的收集线程数跟CPU核数相同,当然也可以使用参数(-XX:ParallelGCThreads)指定收集数,但是一般不推荐修改。
新生代采用复制算法,老年代采用标记-整理算法。
Parallel Scavenge收集器
(-XX:+UseParallelGC (年轻代)) (-XX:+UseParallelOldGC (老年代)) 指定在年轻代还是老年代使用
-
Parallel Scavenge 收集器类似于ParNew收集器,是Server模式(内存大于2G,2个CPU)下的默认收集器。
-
Parallel Scavenge 收集器关注点是吞吐量(高效利用CPU)。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
新生代采用复制算法,老年代采用标记-整理算法。
CMS 垃圾收集器(重点)
用于老年代。
CMS收集器(-XX:+UseConcMarkSweepGC (老年代))
CMS(Concurrent Mark Sweep)收集器是以最短回收停顿时间为目标的收集器,它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
优缺点:
#优点
1.并发收集、低停顿
#缺点
1.对CPU资源敏感,会与服务抢资源
2.无法处理浮动垃圾(在并发清理阶段,用户线程产生的垃圾,只能等到下一次GC清理)
3.它使用的是标记-清除算法会导致大量的空间碎片,可以通过-XX:+UseCMSCompactAtFullCollection JVM在执行垃圾清理后进行整理。
4.执行过程的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段出现,一边回收,系统一边运行,也许还没回收完就再次触发GC,也就是“concurrent mode failure”,此时会进入Stop The Word,用Serial Old 垃圾收集器来回收。
相关参数
-XX:+UseConcMarkSweepGC:启用CMS.
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection: FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是O,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比).
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阀值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor GC 目的在于减少老年代对年轻代的引用降低CMS GC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
G1垃圾收集器
-XX:UseG1Gc
使用的算法是复制算法
G1(Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多个处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
Humongous用来存放大对象。
G1收集器一次GC的运作过程大致分为以下几个步骤:
- 初始阶段(initial mark,STW):暂停所有的其他线程,并记录下GC Roots 直接能引用的对象,速度很快。
- 并发标记(Conocurrent Marking):同CMS的并发标记。
- 最终标记(Remark,STW):同CMS的重新标记。
- 筛选回收(CleanUp,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(默认是200ms,可以使用参数-XX:MaxGCpauseMills指定)来制定回收计划。
在这里插入图片描述