Java类加载和对象创建
引言
Java代码需要被使用,必须要经过类加载器加载到内存中,然后对应的类才能够被创建使用,这文对类加载和对象创建和过程进行分析。
类加载
Java类通过懒加载的方式,经过了Loading、Linking、Initializing后加载到内存中,才能被进行使用。
Loading
懒加载
Java类并不是JVM虚拟机启动的时候,就对所有用到的类进行全部加载,而是在第一次使用到的时候,进行加载
LazyLoading五种情况
- new getstatic putstatic invokestatic指令,访问final变量除外
- java.lang.reflect对类进行反射调用时
- 初始化子类的时候,父类首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
双亲委派
Java类加载的时候,默认情况下,遵循双亲委派的原则(自下向上的查找,自顶向下的加载),为了类安全,核心类不会遭到破坏
ClassLoader源码
ClassLoader三个步骤
findInCache -> parent.loadClass -> findClass()
在抽象类ClassLoader里面,通过模板方法的方式,给出了默认未重写情况下的LoadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//先在自己的缓存中查找是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果自己没有加载过,就交给父类去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//Bootstrap加载器的parent变量为空,当发现parent为空时,则使用bootstrap自己去加载
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
// to find the class.
long t1 = System.nanoTime();
//当交给父类加载器去加载后,如果没有加载到返回了NULL,则自己去进行加载
//这个地方会报ClassNotFound异常,但如果还有子加载器的话,则会在上面被捕捉到,如果已经是最子级加载器的话,则直接报异常。
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;
}
}
一般情况下,不需要打破双亲委派的,在自定义ClassLoader的时候,只需要重写findClass方法即可。
自定义ClassLoader
- 继承ClassLoader
- 重写findClass方法
将字节码文件读入到内存,将其转换成Byte数据,通过defineClass将其转换成对应的Class对象。
FileInputStream -- read进内存 -> 写进ByteArrayOutputStream,再转换成Byte数组
默认的情况下,自定义ClassLoader的parent字段是AppClassLoader,如果没有重写LoadClass方法,则依然遵循双亲委派的加载原则。
思考:如何打破双亲委派原则
通过重写loadClass方法即可打破双亲委派的原则
思考:什么情况下需要打破双亲委派
- JDK1.2之前,自定义ClassLoader都必须重写loadClass()
- ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定、
- 热启动,热部署,tomcat有自己的模块指定classloader(可以加载同一类库的不同版本),还可以实现不同Web Context进行类隔离
Linking
Linking的时候,实际是有三个步骤
Verification(校验)
验证文件是否符合JVM规定
Preparation(准备)
将类的静态变量赋默认值
Resolution(解析)
将类、方法、属性等符号引用解析为直接引用
常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
Initializing
对类进行初始化,调用类初始化代码 clinit,给静态成员变量赋初始值,按顺序执行
对象创建
-
申请对象内存
图中可以看到,优先在栈上分配->TLAB->老年代->Eden区
栈上分配:对象生命周期随着方法的调用开始而开始,结束而结束,不需要进行垃圾回收减轻了GC的负担
TLAB: 线程私有的堆内存,可以很好的避免了共享堆中多个线程操作堆内存分配对象空间时产生的同步问题(虽然有CAS+失败重试),从而提高堆上对象分配的效率。 -
成员变量赋默认值
-
调用构造方法init(成员变量赋初始值,调用类的相应的构造方法)
JVM执行模式
解释模式
- 通过解释器(Bytecode Interpreter)解释执行
- 特点:启动快(不需要编译),执行慢
- 可通过-Xint参数指定为纯解释模式
编译模式
- 由JIT(Just In-Time Compiler)编译为本地代码(C语言实现)执行
- 特点:启动慢(编译过程较慢),执行快
- 可通过-Xcomp参数指定为纯编译模式
混合模式
- 混合使用解释器 + 热点代码编译
- 起始阶段采用解释执行
- 热点代码检测(HotSpot),默认-XX:CompileThreshold=10000
- 多次被调用的方法(方法计数器:监测方法执行频率)
- 多次被调用的循环(循环计数器:监测循环执行频率)
- 对热点代码进行编译
- 默认采用这种模式,可通过-Xmixed指定