目录
JVM
类加载器
提前加载.class
类型文件到内存中,加载类时,使用双亲委派模型,初始化继承树上还没有初始化过的所有父类,并且会执行这个链路上的所有未执行过的静态代码块、静态变量赋值语句等。
全小写的class
是关键字,用来定义类,首字母大写的Class
,是所有class
的类。
以下是一些示例,主要是反射相关的知识:
private static Class<One> one = One.class;
One oneObject = one.newInstance();
Field field = one.getDeclaredField("inner");
field.setAccessible(true);
field.set(oneObject, "world change");
newInstance()
已过时,可以使用getDeclaredConstructor().newInstance()
。
new
与newInstance()
的区别:
new
是强类型校验,可以调用任何构造方法,在使用new
的时候,这个类可以没有被加载过。newInstance()
只能调用无参数构造方法。
层次结构
- 最高层的加载器是
Bootstrap
,负责最核心的Java类 - 其次是
Platform ClassLoader
(JDK9
之前是Extension ClassLoader
),记载一些扩展的系统类 - 最底层是
Application ClassLoader
,加载用户定义的CLASSPATH
路径下的类
双亲委派模式:从下至上依次询问是否加载过,如果未加载过,才逐层向下询问是否可加载。
自定义类加载器作用:1.隔离加载类(主要是因为中间件可能会自定义自己的jar包,从而导致包路径、类名相同的情况发生。通过类加载器实现类隔离,避免类冲突) 2.修改类加载方式 3.扩展加载源 4.防止源码泄漏
自定义类加载器:
public class CustomClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 从自定义的路径中加载指定类
}
public static void main(String[] args) {
ClassLoader classLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, classLoader);
final Object object = clazz.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
内存布局
Heap(堆区)
堆区是OOM
最主要产生的地方,存储几乎所有的实例对象,由垃圾收集器自动回收,由各子线程共享使用。
-Xms256M -Xmx1024M
:-X
表示是JVM
的运行参数,ms
表示最小堆容量,mx
表示最大堆容量。通常情况下,这两个数值设为一样,避免GC后动态调整堆大小的压力。
堆分成新生代和老年代,对象一开始都在新生代,存活时间过长则进入老年代,老年代也容纳新生代无法容纳的超大对象。
新生代=1个Eden
区+2个Survivor
区。Eden
填满的时候会触发YGC
,直接清除没有引用的对象,随后将依然存活的对象送至Survivor
区,Survivor
区分为S1
和S2
两块内存空间,将所有存活的对象(包括Survivor
区)复制到未使用的那块空间,然后清除掉当前正在使用的空间,交换两块区的使用状态。
每次YGC
后对象计数器+1,超过一定的阈值后晋升至老年代。
JVM设置运行参数-XX:+HeapDumpOutOfMemoryError
,遇到OOM
异常时输出堆内信息。
Metaspace(元空间)
JDK8
用元空间替换永久代。
字符串常量移动至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等移动至元空间内。
核心:元空间包括常量池、方法元信息和类元信息
在常量池中的String,其实际对象是被保存在堆内存中,所以为什么说相同的字符串是在常量池中先查找,但是这个String实际的对象并不是在常量池中。
JVM Stack(虚拟机栈)
java方法执行的内存区域,线程私有。
每个方法的调用到结束就是入栈到出栈的过程,在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧,所有指令都只对当前栈帧进行操作。
每个方法对应的栈帧包括:
局部变量表
存放方法参数和局部变量的区域,局部变量没有准备阶段,必须显式初始化。
非静态方法在index[0]的位置存储调用该方法的对象的实例引用,随后是参数和局部变量。
操作栈
方法执行过程中,会有各种指令往栈中写入和读取信息,如下例子就是需要先把常量入栈,然后把常量赋值给局部变量表中对应的下标元素,随后将局部变量表中的元素压入操作栈。执行加法的时候,从操作栈弹出两个操作数,随后计算然后压入栈。
public int simpleMathod() {
int x = 13;
int y = 14;
int z = x + y;
return z;
}
动态连接
每个栈帧中包含一个在常量池中对于当前方法的引用,支持调用过程的动态连接。
方法返回地址
方法退出的过程相当于弹出当前栈帧,有三种方式退出:
- 返回值压入上层调用栈帧
- 异常信息抛给能够处理的栈帧
- PC计数器指向方法调用后的下一条指令
Native Method Stacks(本地方法栈)
本地方法栈为Native方法服务。
所谓Native方法,例如System.currentTimeMills()
,就是需要依赖本身的java程序以外的系统本地的方法类获取结果,这种方法一般是非Java的,与操作系统有关的。本地方法可以通过JNI
获得和JVM
相同的能力和权限。
Program Counter Register(程序计数寄存器)
每个线程产生后,都会有自己的程序计数器和栈帧,程序计数器用于存放执行指令的偏移量和行号指示器等,线程执行和恢复都需要依赖这个。
堆和元空间是所有线程共享的,虚拟机栈、本地方法栈、程序计数器都是线程内部私有的。
垃圾回收
垃圾回收的主要目的是清除不再使用的对象,自动释放内存。
如何判断对象是否存活:JVM引入了GC Roots,如果一个对象与GC Roots没有直接或间接的引用关系,比如某个失去任何引用的对象,或者两个互相环岛循环引用的对象等。
回收算法:
- 标记-清除算法:会带来大量的空间碎片,导致需要分配一个较大连续空间
- 标记-整理算法:标记存活的对象,将存活对象整理到内存空间的一端,不会产生空间碎片的问题,是主流算法
垃圾回收器:
Serial
:单线程,垃圾回收的某个阶段会暂停整个应用程序的执行,严重影响应用程序的性能,标记-整理算法CMS
:部分步骤依旧会暂停程序运行,但部分步骤可以并行,标记-清除算法G1
:JDK11中默认回收器,具备压缩功能,避免了碎片问题,预测停顿时间