本来打算两篇结束,晚上实在是学不下去。G
1. 类加载
1.1 加载
将类的字节码载入到方法区中,内存采用C++的
instanceKlass
描述Java类。
与instanceKlass
对用关系,class对象和Klass对象之间互相持有对方的内存地址。实例对象如果想要调用部分方法时,需要使用对象头地址,然后去元空间中调用。
在加载过程主要完成:
- 通过类的全限定类名来获取定义此类的二进制字节流;
- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表整个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
特点:
- 如果当前类的父类还没有加载,优先加载父类;
- 加载和连接过程可能交替运行。
1.2 连接
1.2.1 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符号当前虚拟机的要求,并且不会威胁虚拟机自身安全。
主要分为四个阶段的校验动作:
- 文件格式验证:验证字节流是否符合Class文件规范。
- 元数据验证:对字节码描述的信息进行语义分析,确保其描述的信息符合Java语言规范;
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,是对类自身以外的信息进行匹配性校验。
1.2.2 准备
为类变量分配内存并设置类变量初始值的阶段,这写变量所使用的内存都将在方法区中进行分配。
static
变量分配空间在准备阶段完成,赋值在初始化阶段完成;final static
的基本变量类型,以及字符串常量,编译阶段值就确定了,赋值在准备阶段;final static
的引用类型,赋值会在初始化阶段完成。
1.2.3 解析
解析是将常量池内符号引用替换为直接引用的过程。
/**
* @Description 解析过程
* @date 2022/3/29 9:24
*/
public class ShowAnalysis {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
ClassLoader classloader = ShowAnalysis.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
- 如果只是调用
loadClass
方法加载类C
,并没有解析和初始化的发生,因此,通过HSDB工具是无法找到类D
的,并且在类C
中的D
对象也是一个未加载的类的信息。
- 如果实例化类
C
,就会对类C
连接和初始化,这样类D
也会被加载,在类C
中就可以找到类D
的地址。
1.3 初始化
类的初始化阶段是类加载过程的最后一步,只有到了初始化阶段,才开始执行类中定义的Java程序代码。实际上就是执行
<clinit>()
方法的过程。
初始化时机:
- 首先初始化
main
方法所在的类; - 首次访问这个类的静态变量或静态方法时;
- 子类初始化时,会引发没有进行初始化的父类进行初始化;
- 子类访问父类的静态变量,只会触发父类初始化;
Class.forName
new
对象。
不会初始化:
- 访问类的
static final
字符串和基本类型; - 类对象
.class
; - 创建该类的数组;
- 类加载器的
laodClass()
; Class.forName()
第二个参数为false
1.3.1 懒惰实现单例模式
/**
* @Description 懒惰初始化单例模式实现
* @date 2022/3/29 9:45
*/
public class SingleObject {
public static void main(String[] args) {
Singleton.getInstance();
}
}
class Singleton{
private Singleton() {}
private static class LazyHolder{
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("初始化 LazyHolder");
}
}
public static Singleton getInstance(){
return LazyHolder.SINGLETON;
}
}
2. 类加载器
通过一个类的全限定类名来获取此类的二进制字节流的方法放到虚拟机外部去实现,以便让应用程序自己去获取所需要的类。
JDK8中类加载器的层级关系:
2.1 双亲委派模式
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器,但类加载器之间的父子关系是通过组合关系来复用父加载器的代码。
双亲委派模型:
工作过程:如果一个类收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个类加载器都是如此,所有加载请求最终都会传送到最顶层的启动类加载器;只有父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
好处:
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
- 避免程序混乱。
源码实现:先检查是否已经被加载过,若没有则调用父加载器的loadClass()
方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException
异常,在调用自己的findClass()
方法进行加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1- t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.2 自定义类加载器
应用场景:
- 加载随意路径下的类文件;
- 通过接口来使用实现时,想要解耦;
- 希望类予以隔离,不同应用的同名类都可以加载,不发生冲突。
代码实现:如果打印出为true
就表示运行成功,因为如果类加载成功就会在类加载器中有缓存不会重复的加载类。
/**
* @Description 自定义类加载器
* @date 2022/3/29 16:59
*/
public class LoadClassTest {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> mapImp1 = myClassLoader.loadClass("jm.java.bytecode.AgaCom");
Class<?> mapImp11 = myClassLoader.loadClass("jm.java.bytecode.AgaCom");
System.out.println(mapImp11 == mapImp1);
}
}
class MyClassLoader extends ClassLoader{
/**
*
* @param name 类名称
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 获取本类的字节码文件
String path = "D:\\NewJava\\JavaFoundation\\java-jvm\\" + name +".class";
// 文件输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Files.copy(Paths.get(path),bos);
// 得到字节数组
byte[] bytes = bos.toByteArray();
return defineClass(name,bytes,0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到:" + e);
}
}
}
3. 运行期优化
3.1 即使编译
3.1.1 逃逸分析
/**
* @Description 内存优化
* @date 2022/3/29 20:55
*/
public class MemOptimize {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
运行上方代码,能够明显的看出创建最后一个对象比第一个对象快很多。
原因是JVM将执行状态分成5个层次:
- 0层:解释执行(Interpreter);
- 1层:使用 C1 即时编译器编译执行(不带 profiling);
- 2 层,使用 C1 即时编译器编译执行(带基本的 profiling);
- 3 层,使用 C1 即时编译器编译执行(带完全的 profiling);
- 4 层,使用 C2 即时编译器编译执行
profiling:在运行过程中收集一些程序执行状态的数据。
即使编译器和解释器的区别:
- 解释器将字节码解释为机器码,下次遇到相同的字节码也会重复的执行;
- 即时编译器将一些字节码编译为机器码,会将机器码存入
Code Cache
,下次遇到相同代码就直接执行; - 解释器将字节码解释针对所有平台的都通用的机器码;
- 即使编译器会根据平台生成特定的机器码。
3.1.2 方法内联
private static int square(final int i){
return i * i;
}
System.out.println(square(9));
如果square()
是热点方法且长度不太长,会进行内联。内联:将方法内代码进行复制、黏贴到调用者位置:
System.out.println(square(9 * 9));