文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容
软件使用文件的头几个字节(字节头)去校验文件的类型,若软件不支持该种类型就会出错
文件类型 | 字节数 | 文件头 |
---|---|---|
JPEG(jpg) | 3 | FFD8FF |
PNG(png) | 4 | 89504E47 |
bmp | 2 | 424D |
XML(xml) | 5 | 3C3F786D6C |
AVI(avi) | 4 | 41564920 |
Java字节码文件(.class) | 4 | CAFEBABE |
Java字节码文件种,将文件头称为Magic魔数
主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号(主版本号-44 = 当前的JDK版本)
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容
新建文件夹 -> 执行curl -O https://arthas.aliyun.com/arthas-boot.jar ->运行程序,进入cmd窗口输入java -jar arthas-boot.jar -> 找到正在运行的程序输入对应的编号
描述了一个类加载、使用、卸载的整个过程
类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中
生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息
Java虚拟机会在堆中生成一份与方法去中数据类似的java.lang.Class对象
作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8之后)
Java虚拟机就能很好的控制开发者访问数据的范围
因为方法区中的一些方法是使用C++编写的,而Java并不能访问,所以就将方法区InstanceKlass中我们所需要用到的一些数据复制到堆区中,而开发者就只需要访问堆区中java.lang.Class的对象,从而不再直接方法区中的对象,提高了数据安全性
使用位于JDK安装目录下的lib文件夹中的sa-jdi.jar中自带的hsdb工具
启动命令java -cp sa-jdi.jar sun.jvm.hotspot.HSDB -> 输入对应的进程号,查看相关对象信息
验证主要是检测Java字节码文件是否遵守了Java虚拟机规范中的约束
主要包含以下四部分:
文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求
元信息验证,比如类必须有父类,默认有父类Object
验证程序执行指令的语义,比如方法内的指令执行中跳转到其他方法总
符号引用验证,比如是否访问了其他类中private的方法等
准备阶段只会为静态变量(static)分配内存并设置初始值,其初始值根据不同的基本数据类型和引用数据类型的初始值再区分
final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值
初始化阶段会执行静态代码块中的代码,并为静态变量赋值
初始化阶段会执行字节码文件中clinit(class init)部分的字节码指令
clinit方法中的执行顺序与Java中编写的顺序是一致的
以下几种方式会导致类的初始化
添加-xx:+TraceClassLoading 参数可以打印出加载并初始化的类
以下几种方式不会对类进行初始化
直接访问父类的静态变量,不会触发子类的初始化
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
数组的创建不会导致数组中元素的类进行初始化
final修饰的变量若赋值的内容需要执行指令才能得出结果,则会执行clinit方法进行初始化
类加载器(ClassLoader)是Java虚拟机提供给应用程序区实现获取类和接口字节码数据的技术
类加载器只参与加载过程中的字节码获取并加载到内存这一部分
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供,由C++编写的类加载器
默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等
位置在C:\Program Files\Java\jdk1.8.0_361\jre\lib
若想要运行Java程序,则需要在Java的基础运行环境的jre中运行
当运行Bootstrap类加载器时,因为虚拟机是偏向底层应用的,而Java程序是偏向上层应用的,所以当运行该段程序时,会打印出null。(路径在 External Libraires -> <1.8> -> rt.jar -> java -> lang -> String
找到String类)
public class ClassLoaderDemo {
public static void main(String[] args) throws IOException {
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
//使程序运行完不再退出
System.in.read();
}
}
扩展类加载器(Extension Class Loader)是JDK中提供的使用Java编写的类加载器
默认加载Java安装目录/jre/lib/ext下的类文件
位置在C:\Program Files\Java\jdk1.8.0_361\jre\lib\ext
放入/jre/lib/ext下进行扩展(不推荐,尽可能不要更改JDK安装目录中的内容)
使用参数进行扩展(推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,但是会覆盖掉原始目录
windows:“-Djava.ext.dirs=原始jar包目录;自定义jar包目录”
macos/Linux:“-Djava.ext.dirs=原始jar包目录:自定义jar包目录”)
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
双亲委派机制让一个类只能被同一个类加载器加载,就可以避免同一个类被多次加载,减少加载过程中的性能开销
当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
向下委派加载起到了加载优先级的作用
当类加载器发现一个类在自己的加载路径中就会去加载这个类,若没有发现,则从下往上继续查找是否被加载过,若发现被加载了则直接将这个类进行加载,若一直到最顶层的类加载器都没有被加载,则由顶向下进行加载。
若所有类加载器都无法加载该类,则会抛出类无法找到的错误
//获取main方法所在类的类加载器,应用程序类加载器
ClassLoader classLoader = Demo.class.getClassLoader();
System.out.println(classLoader);
//使用应用程序类加载器加载 com.aaa.bbb.CCC
Class<?> clazz = classLoader.loadClass("com.aaa.bbb.CCC");
System.out.println(clazz.getClassLoader());
每个java实现的类加载器中保存类一个成员变量叫"父"(Parent)类加载器,可以理解为是其上级而不是继承关系
应用程序类加载器的父类加载器是扩展类加载器
扩展类加载器的父类因为启动类加载器使用C++编写,获取不到,所以为null
启动类加载器使用C++编写,没有父类加载器
public Class<?> loadClass(String name)
类加载的入口,提供了双亲委派机制。内部会调用findClass
public Class<?> findClass(String name)
由类加载器子类实现,用来加载字节码文件中的信息,获取二进制数据调用defineClass
URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected final Class<?> defineClass(String name,byte[] b,int off,int len)
做一些类名的校验,调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
执行到这,类加载阶段执行完毕
protected final void resolveClass(CLass<?> c)
执行类生命周期的连接阶段
核心代码
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if(parent != null){
c=parent.loadClass(name,false);
}else{
//调用启动类加载器加载类
c=findBootstrapClassOrNull(name);
}
//若父类加载器都未加载成功,则由本类加载器加载
if(c==null)
c=findClass(name);
自定义类加载器不手动设置parent的话,则默认父类是由getSystemClassLoader()方法设置,该方法返回一个应用程序类加载器AppClassLoader
重写private ClassLoader(Void unused, ClassLoader parent)方法完成自定义父类
两个自定义类加载器加载相同限定名的类,不会冲突,因为在同一个java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类
JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动
DriverManager类位于rt.jar包中,由启动类加载器加载
依赖中的mysql驱动对应的类,由应用程序类加载器加载
DriverManage使用SPI机制
(Service Provider Interface,是JDK内置的服务提供发现机制
工作原理:
SPI中使用了线程上下文中保存的类加载器进行类的加载,而且该类加载器一般是应用程序类加载器
使用自定义类加载器加载:Thread.currentThread().setContextClassLoader(自定义类加载器名)
? 因为这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
? 因为JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,依旧是通过启动类加载器向下委派直至委派给应用程序类加载器去加载类,类的加载依然遵循双亲委派机制
JDK8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java
JDK9引入了module的概念,类加载器在设计上发生了变化
启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中
Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一
扩展类加载器被替换成了平台类加载器(Platform Class Loader)
平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件
平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区
采用栈的数据接口来管理方法调用中的基本数据,先进后出,每一个方法的调用都使用一个栈帧来保存
若抛出异常,则会在控制台打印出当前栈中所保存的各个栈帧的情况,获取到栈帧里面保存的对应的方法名
Java虚拟机栈是随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈
若不指定栈的大小,JVM将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构
若要修改Java虚拟机栈的大小,可以使用虚拟机参数-Xss
语法:-Xss栈大小
单位:字节(默认,必须是1024的倍数)、K或者K(KB)、m或者M(MB)、g或者G(GB)
与-Xss类似,也可以使用-XX:ThreadStackSize调整标志来配置堆栈大小
格式为:-XX:ThreadStackSize=1024
HotSpot JVM对栈大小的最大值和最小值有要求
Windows(64位)下的JDK8测试最小值为180k,最大值为1024m
局部变量过多、操作数栈深度过大也会影响栈内存大小
一般情况下,即便使用了递归,栈的深度最多只能到几百,不会出现栈的溢出。
参数可以手动指定为-Xss256k节省内存
Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象,在类的加载阶段完成
方法区除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容
字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池
当常量池加载到内存之中后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池
方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同
HotSpot设计如下:
-XX:MaxPermSize=值
来控制方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)
字符串常量池存储在代码中定义的常量字符串内容。比如“123”就会被放入字符串常量池
String.intern()可以手动将字符串放入字符串常量池中
String a="1";
String b="2";
String c="12";
//由于在底层中使用了StringBuilder,
//最后使用了new String,所以d的值在堆中
String d=a+b;
//是直接通过在字符串常量池中相加,
//所以e的值还是在字符串常量池中
String e="1"+"2";
-----------------------------------------------------------
String input1 =scanner.next().intern();
String input2 =scanner.next().intern();
//若input1和input2输入的值相同,则会相等
input1 == input2;// true
JDK6版本的intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代中字符串的引用。JVM启动时会把Java加入到常量池中
JDK6及之前的版本,静态变量是存放在方法区中,也就是永久代
JDK7及之后版本中由于字符串常量池在堆中,所以intern()方法会将第一次遇到的字符串的引用放入字符串常量池中
JDK7及之后的版本,静态变量是存放在堆中的Class对象,脱离了永久代
直接内存并不在《Java虚拟机规范》中,不属于Java运行时的内存区域
使用直接内存,是为了解决:
Java堆中的对象若不再使用要回收,回收时会影响对象的创建和使用
IO操作比如读文件,需要将文件读入直接内存(缓存区)再把数据复制到Java堆中
现在直接放入直接内存,同时Java堆上维护直接内存的引用,减少了数据复制的开销
JDK8之后,主要为了保护在方法区中的数据
若需要手动调整直接内存的大小,可以使用**-XX:MaxDirectMemorySize=大小**
单位k或k表示千字节,m或M表示兆字节,g或G表示千兆字节。
默认不设置该参数情况下,JVM自动选择最大分配的大小
为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection 简称GC)机制
通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行会后
自动根据对象是否使用由虚拟机来回收对象
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。
而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存
大厂的系统出现的许多系统僵死问题都与频繁的垃圾回收有关
对垃圾回收器进行合理的设置可以有效地提升程序的执行性能
常见的垃圾回收器、常见的垃圾回收算法、四种引用、项目中有用哪一种垃圾回收器
方法区中能回收的内容主要就是不再使用的类
判定一个类可以被卸载,需要同时满足下面三个条件:
此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
Class<?> clazz = loader.loadClass("com.a.b.C");
Object o = clazz.newInstance();
o = null;
加载该类的类加载器已经被回收
URLClassLoader loader = new URLClassLoader(
new URL[] {new URL("file:D:\\lib\\")}
);
loader=null;
例:每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
该类对应的java.lang.Class对象没有在任何地方被引用
Class<?> clazz = loader.loadClass("com.a.b.C");
clazz = null
若需要手动触发垃圾回收,可以调用System.gc()方法
语法:System.gc();
注意事项:
调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
优点:实现简单
缺点:
每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
A、B实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏
查看垃圾回收日志的信息:使用 -verbose:gc参数
可达性分析算法将对象分为两类,且对象与对象之间存在引用关系:
可达性分析算法指的是如果从某个普通对象到GC Root对象是可达的(也就是普通对象能通过引用链找到GC Root对象),对象就不可被回收;若引用链不存在就可以被回收。
Thread线程对象,引用线程栈帧中的方法参数、局部变量等
比如main主线程Thread对象
System Class系统类加载器加载的java.lang.Class对象,引用类中的静态变量
比如sun.misc.Launcher系统类加载器
Busy Monitor监视器对象,用来保存同步锁synchronized关键字持有的对象
JNI Global本地方法调用时使用的全局对象
通过arthas和eclipse Memory Analyzer(MAT)工具查看GC Root
**强引用:**即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收;通过强引用引用的对象就是不可被回收的,可以被保留
**软引用:**相对于强引用是一种比较弱的引用关系,当程序使用完一个对象后,就会解除强引用对象;若一个对象只有软引用关联到它,当程序内存不足时,就会将引用中的数据进行回收,释放一定的堆内存;
在JDK1.2版之后提供了SoftReference类来实现软引用,常用于缓存
弱引用:与软引用的整体机制基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收
虚引用:也叫幽灵/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现
终结器引用:指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。
byte[ ] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[ ]> softReference = new SoftReference<byte[ ]>(bytes);
System.out.println(softReference.get());
软引用也可以使用继承自SoftReference类的方式来实现,StudentRef类就是一个软引用对象
通过构造器传入软引用包含的对象,以及引用队列
byte[ ] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[ ]> weakReference = new WeakReference<byte[ ]>(bytes);
bytes=null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
引入依赖
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.23</version>
</dependency>
创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0);
调用visit方法,创建字节码数据
classWriter.visit(Opcodes.v1_7,Opcodes.ACC_PUBLIC,name,null,"java/lang/Object",null);
byte[] bytes=classWriter.toByteArray();
参数列表:https://arthas.aliyun.com/doc/commands.html
**dashboard:
**dashboard -i 2000 -n 1:每隔两秒对运行的程序进行监控并输出到屏幕,一共执行3次
**memory:
**展示当前运行的程序的内存
**dump:
**dump对已加载类的字节码文件到特定目录,dump -d 文件存放的路径 包名.文件名
**jad:
**jad反编译已加载类的源码,jad 包名.类名
**sc:
**搜索出所有已经加载到JVM中的Class信息,输出java.lang.String的类信息:sc -d java.lang.String
**heapdump:
**heapdump 目录路径 文件名.hprof 将堆内存快照保存到本地磁盘中
**classloader:
**classloader查看类加载器的继承树,urls,类加载信息,使用classloader区getResource
查看所有的类加载器的hash值:classloader -l,查看指定的classloader的jar包:classloader -c hash值,查看类加载器的父子关系:classloader -t