JVM原理
JVM类加载流程
源码——》javac——》类加载器——》内存
主动加载的方式?(4种)
- Student student = new Student();
- 反射、clone
- 初始化子类的时候、父类会被初始化
- 调用一个静态方法
加载过程
- 加载
- 1)通过类的全路径名,获取类的二进制流
- 2)解析流,将类的信息加载到方法区中
- 3)创建这个类的实例
- 验证
- 验证这个字节码是否合法(格式、语义、符号引用)
- 准备
- jvm为这个类分配相应的内存空间
- 解析
- 将符号引用转化为直接引用
- 初始化
- 说明已经将类加载到系统中,这时类才会开始执行java字节码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wgp5DF8F-1629195522503)(/Users/apple/Library/Application Support/typora-user-images/image-20210722174844248.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kDjGiDJ2-1629195522506)(/Users/apple/Library/Application Support/typora-user-images/image-20210722175510262.png)]
逃逸分析
逃逸分析就是分析Java对象的动态作用域。当一个对象呗定义之后,有可能被外部的对象引用,称之为方法逃逸。也有可能被其他线程引用,称之为线程逃逸。如果经过逃逸分析之后,对象并没有逃逸出方法,那就不用在堆上分配,可以优化先在栈上分配内存,这样就不用在堆上分配内存,也不用进行垃圾回收了。
对象头
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。
HotSpot对象头包含两部分,一部分用于存储对象自身的运行时数据,如:哈希吗(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位虚拟机中分别为32个和64个bit,官方称之为Mark Word。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wz4qdqlT-1629195522508)(/Users/apple/Library/Application Support/typora-user-images/image-20210722180525109.png)]
对象头另一部分,是类型指针,该指针指向他的类元数据,JVM通过这个指针确定对象是哪个类的实例。
锁标志位:锁标志位与是否偏向锁对应到唯一的锁状态
Synchronized锁的是什么,锁的是代码还是对象。
答:锁的是对象
synchronized有4种锁状态,从低到高为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
锁可以升级但不可以降级。
偏向锁,就是在锁对象的对象头中有一个ThreaddId的字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的TreadId字段内,将锁头内的是否偏向锁的状态位置成1,这样下次获取锁的时候,直接检测ThreadId是否和自身的线程id一致,如果一致,则认为当前线程已经获取了锁,因此不需要再次获取锁了,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
例:
在HotSpot虚拟机中,32位机器侠,Integer对象的大小是int的几倍?
Integer中只有一个int型的成员变量value,所以其对象的实际数据部分大小是4个字节,Mark Work 大小4个字节,类型指针 4个字节,再加上对齐补充的 4个字节,一共是16个字节。
内存管理
-
线程私有
- 程序计数器
- 本地方法栈
- 虚拟机栈
- 栈帧
-
线程公有
- 方法区
- java堆(重点)
JMM
程序计数器
- 有一个较小的内存空间
- 正常的非native方法:字节码的行号指示器。native方法:空
- 内存是私有的
- 唯一的没有规定任何OutOfMemeryError的区域
本地方法栈
- 为native方法服务的
- 内存私有
虚拟机栈
方法本身,和方法内的变量(局部变量)
- 内存是私有的
- 描述方法执行的内存模型(栈帧)
- 方法开始执行——》结束执行 == 栈帧的入栈出栈
方法区
- 内存公有
- 方法区
- 逻辑上的概念
- 类的信息、常量池、方法数据
- 永久代(JDK7)
- 永久保存的内存信息,不会被GC的一块区域
- 加载了类信息,和元数据信息
- 如果空间被加载的类信息沾满了,就会发生溢出异常
- 元空间(JDK8)
- 永久代被移除了
- 不在jvm中,直接使用本地内存。受限于本地内存的大小
- HopSpot、JRrockit(没有永久代)
堆
- 内存公有
- 创建对象、数组保存的地方
- GC发生的地方
- 新生代(Eden、s0、s1)和老年代
栈帧操作
双亲委派机制
在虚拟机中并不是一次性把所有文件都加载到,而是一步一步的,按照需求来加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KOTwucQE-1629195522509)(/Users/apple/Library/Application Support/typora-user-images/image-20210722183000771.png)]
虚拟机提供了3种类加载器,根类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)
-
根类加载器(C++实现,没有父类)
跟类加载器主要加载的是JVM自身需要的类,由C++实现,是虚拟机自身的一部分,它负责将/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,如:rt.jar。如果文件名不被虚拟机识别,即使把jar包丢到/lib下也没用,(出于安全考虑,根类加载器只加载报名为java、javax、sun开头的类)。
-
扩展类加载器(java实现,父类加载器null)
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由java实现,是Launcher的静态内部类,它负责加载/lib/ext目录下或由系统变量-Djava.ext.dir指定位路径下的类库,开发者可以直接使用标准扩展类加载器。
-
应用类加载器(Java实现,父类加载器ExtClassLoader)
它负责加载系统路径java -classpath或者 - D java.class.path指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用应用类加载器,一般情况下,该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
这3中类加载器之间存在这父子关系,子类加载器保存着父加载器的引用。当一个类加载器需要架子啊一个目标类时,回先委托父加载器区加载,然后父加载器会在自己的加载器路径中搜索目标类,父加载器在自己的加载范围找不到时,才会交还子加载器加载目标类。
双亲委派机制的优点:
采用双亲委派机制可以避免类加载混乱,并且将类分类了,例如java的lang包下的类在虚拟机启动时就别跟加载器加载了,用户的以下代码类要应用类加载器加载,基于双亲委派机制,就算用户定义了和lang包下一样的类,也不会生效,因为应用类加载器回去委派跟加载器加载,这时,应用类加载器发现已将lang包下的类加载了,就不会加载自己写的类了,就算用户自定义类加载器强行打破双亲委派机制,也不会成功,java安全管理器会抛出java.lang.SecurityException异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PrqNem51-1629195522512)(/Users/apple/Library/Application Support/typora-user-images/image-20210722185619117.png)]
实现原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IafxXwtI-1629195522512)(/Users/apple/Library/Application Support/typora-user-images/image-20210722185710276.png)]
loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派机制实现的,顶层的类加载器是ClassLoader类,它是一个抽象类,其后搜有的类加载器都继承自ClassLoader(不包括跟类加载器)。
-
loaderClass(String name,boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
ected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先从缓存中查找该class对象,找到就不用重新加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果找不到,则委派给父类加载器去加载 c = parent.loadClass(name, false); } else { //如果没有父类,则委托给根类加载器去加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // 如果都没有找到,则通过自定义实现的findClass去查找并加载 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {//是否需要在加载时进行解析 resolveClass(c); } return c; } }
-
findClass()方法,该方法是在loaderClass方法中被调用的,当loadClass()方法中父加载器架子啊失败后,则会调用自己 的findClass()方法来完成类加载,这样就可以保证自定义的类加载器页符合双亲委派模式,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器是描绘直接覆盖ClassLoader的findClass()方法并比那些加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法,生成类的Class对象。
protected Class<?> findClass(String name) throws ClassNotFoundException { // 获取类的字节数组 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //使用defineClass生成class对象 return defineClass(name, classData, 0, classData.length); } }
-
resolveClass(Class<?> c)
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说连接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转化为直接引用。
双亲委派机制模型的好处:
1、主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如:String
2、同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:
1、类的完整类名必须一致,包括包名。
2、加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
自定义类加载器
应用场景
-
加密
Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这是就需要自定义类加载器在加载类的时候先解密类,然后再加载。
-
从非标准的来源加载代码
如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
-
以上两种情况在实际中的综合应用
比如你的应用需要通过网络来传输Java类的字节码,为了安全性,这些字节码经过加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类
如何自定义类加载器:
1、如果不想打破双亲委派模型,只需要重写findClass方法即可。
2、如果要打破双亲委派模型,就要重写整个loadClass方法。
Demo
Class HClassLoader extends ClassLoader {
private String classPath;
public HClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 获取.class字节流
*
* @param name
* @return
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
//字节流解密
data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);
return data;
}
}
测试类
@Test
public void testClassLoader() throws Exception {
HClassLoader myClassLoader = new HClassLoader("e:/temp/a");
Class clazz = myClassLoader.loadClass("com.demo.Car");
Object o = clazz.newInstance();
Method print = clazz.getDeclaredMethod("print", null);
print.invoke(o, null);
}
实体类
public class Car {
public Car() {
System.out.println("Car:" + getClass().getClassLoader());
System.out.println("Car Parent:" + getClass().getClassLoader().getParent());
}
public String print() {
System.out.println("Car:print()");
return "carPrint";
}
}
双亲委派模型的破坏者——线程上下文类加载器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRw6y7ww-1629195522514)(/Users/apple/Library/Application Support/typora-user-images/image-20210722195413835.png)]
垃圾回收GC
什么是垃圾回收?
- 清除掉内存中不会被再使用的对象
标记一个垃圾对象(可触及性)
- 可触及性:确定一个对象是否可以被回收
- 从根节点出发,访问某个对象,如果这个对象可以被访问到,说明可用,反之亦然
- finlize
引用级别
- 强引用
- 程序中的应用。Student student = new Student()
- 软引用
- 当我们堆空间不足时,才会被回收
- 弱引用
- 当Gc发生时,它只要发生了弱引用
- 虚引用
- 跟没有一样
槽位服用
对象的分配
-
是否可以在栈上分配(栈上分配)
-
逃逸分析
Student s1 = new Student();//逃逸了 public void GC1(){ Gc1(); system.gc(); }
-
标量替换
- 标量:不可进一步分解的量(基本数据类型)
- 聚合量:可以进一步分解的量
- 替换
- 通过逃逸分析,确定这个对象不会被外部访问
- 会对这个对象进行分解(若干个变量锁代替。用标量替换聚合量)
-
-
是否可以再TLAB分配(TLAB分配)
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
- 作用:避免多线程之间的冲突
- 知识一个缓冲区:空间较小,默认占eden的百分之一(大的对象无法分配)
-
是否可以直接进入老年代(老年代分配)(堆分配)
- 绝大对象的分配方式
-
新生代分配
主要的垃圾回收算法
-
引用计数法
对于一个对象A只要有任意一个对象引用了它,计数器+1,引用失效时-1
严重问题:
1)不法解决引用循环的问题
2)老是+1-1效率低下
-
标记清除算法
问题:内存碎片多,对于大对象的内存分配,不连续的空间分配效率低于连续空间
分为两个阶段:标记和清除
-
复制算法
问题:为了解决上边算法效率低的问题,只能用一半的内存空间
将原有内存分为2块(A、B)每次只是用其中一块,A内存GC将存活的对象复制到B内存中。
-
标记压缩算法(标记整理)
老年代的回收算法
标记存活的对象,将存活的对象压缩到内存的一端,将存活的对象之外的所有地方清除
-
分代算法
将堆空间分为新生代和老年代,根据他们直接不同的特点,执行不同的回收算法
-
分区算法
将堆空间划分成连续不同的小区间,每个区间独立使用。回收,由于堆空间大时,一次Gc的时间会非常耗时,那么每次可以控制回收多少个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿
JVM垃圾收集器
-
串行回收器-Serial(比较古老)
- 只是用单线程进行GC
- 独占式GC(STW。stop the world)
- 串行收集器是JVM Client模式下默认的垃圾收集器
-
并行回收器-ParNew、ParallelGC、ParallelOldGC
-
将串行回收器多线程化
CMS
回收步骤:
初始标记:标记根对象
并发标记:所有的对象
预清理:中间值重新标记(可有可无):最后一次标记
并发清理:清理
并发重置:整理
-
-
G1回收器(负责新生代和老年代)
-
优先回收垃圾比例最高的区域
第一阶段:新生代GC
第二阶段:并发标记周期
第三阶段:混合收集
第四阶段:Full GC(非必须)
并发标记周的回收步骤:
Application Threads
初始标记:根对象
根区域扫描:
并发标记:
重新标记:最后一次标记
独占清理:计算存活/清理的比例
并发清理:清理完全空闲的区域
-