
概述
-
是什么
JVM构成的一部分, 负责将类从磁盘或网络读到 JVM 内存,然后交给执行引擎执行 -
用于做什么
加载类, 校验类, 初始化类, 构建类的字节码对象
类加载器
-
作用
类加载子系统中负责将类读取到内存的对象 -
有哪些类加载器
BootStrapClassLoader
ExtClassLoader
AppClassLoader
自定义的类加载器
类加载机制-双亲委派模型?
原理
-
向上询问,向下委派
特点
-
优点:可以保证一个类只能被同一个类加载器加载一次,同时保证类体系的健壮性(例如我们自己写的java.lang.Object类不能被加载)
-
缺点:1)可能对效率会多少有一些影响。 2)可能对不同项目中的包名、类名相同的类无法实现正确类加载。
类加载的方式
-
显示加载
直接调用类加载器的方法对类加载即可 -
隐式加载
1)访问类的成员(静态属性或方法) 2)构建类的对象(new)
class ClassA{
static int a=10;
static{
System.out.println("ClassA.static{}");
}
}
class ClassB{
static int b=10;
static{
System.out.println("ClassB.static{}");
}
}
class ClassC{
static int c=10;
static{
System.out.println("ClassC.static{}");
}
}
/**跟踪类的加载可以使用JVM参数:-XX:+TraceClassLoading*/
public class ClassLoaderTraceTests {
public static void main(String[] args) throws ClassNotFoundException {
//1.隐式加载
ClassA a1;//这种情况不会加载ClassA
int a2=ClassA.a;//此时会加载ClassA
new ClassA();//假如ClassA已经被加载,则此时会使用内存中ClassA,不再进行加载
//2.显示加载(直接使用指定类加载器去加载)
ClassLoader.getSystemClassLoader()//获取类加载器
.loadClass("htproject.jvm.loader.ClassB");//类会被加载但不会执行静态代码块
//Class.forName("htproject.jvm.loader.ClassC")
Class.forName("htproject.jvm.loader.ClassC",
false,//false表示只加载类,但是不会执行初始化
ClassLoader.getSystemClassLoader());
}
}
跟踪类的加载可以使用JVM参数:-XX:+TraceClassLoading 类加载时一定会执行静态代码块?不一定
类加载过程
加载:
1) 通过一个类的全限定名(类全名-包名.类名)来获取其定义的二进制字节流(byte[])。
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3) 在堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数 据的访问入口
链接:
验证、准备、解析
验证: 确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机的安全
准备:此阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配
1) 类变量(static)内存分配。
2) 按类型进行初始默认值分配(如 0、0L、null、false 等)
1) 类变量(static)内存分配。 2) 按类型进行初始默认值分配(如 0、0L、null、false 等)。
假设一个类变量的定义为:public static int value = 3;那么变量 value 在准备 阶段过后的初始值为 0,而不是 3,把 value 赋值为 3 的动作将在初始化阶段才会执行。 但是如果类字段的字段属性表中存在 ConstantValue 属性,即
同时被final和static修饰
,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。
初始化:
负责对类进行初始化,主要对
类变量进行初始化
1) 声明类变量时指定初始值。
2) 使用静态代码块为类变量指定初始值。
class ClassD{
private static int a=10;//这里的a=10是在类加载过程中的初始化阶段完成的。
private static final int b=20;//这里的a=20是在类加载过程中的准备阶段完成的。
private static int c;
private static final int d;//final修饰的变量要么定义时赋值,要么静态代码块赋值。
static{
c=30;
d=40;
}
private static ClassD instance=new ClassD();//准备阶段instance的值为null
public ClassD(){
System.out.println("ClassD().instance="+instance);//null
}
static{
System.out.println("static.instance="+instance);
}
}
public class ClassInitTests {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("htproject.jvm.loader.ClassD");
}
}
自定义类加载器
目的:
- 修改类的加载方式(打破类的双亲委派模型)
- 扩展加载源(例如从数据库中加载类)
- 防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密)
方式:
1.继承 URLClassLoader,此类可以直 接从指定目录、jar 包、网络中加载指定的类资源
2.继承 java.lang.ClassLoader 抽象类的方式,实现自己的类加载器
/**
* 自定义类加载器
*/
public class SimpleAppClassLoader extends ClassLoader{
private String baseDir;
public SimpleAppClassLoader(String baseDir){
this.baseDir=baseDir;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
String classPath=baseDir+className.replace(".", "\\")+".class";
File file=new File(classPath);
InputStream in=null;
try {
in=new FileInputStream(file);
byte[] array=new byte[in.available()];//in.available()获取流中有效字节数
int len=in.read(array);
return defineClass(className, array, 0, len);
}catch (IOException e){
throw new RuntimeException(e);
}finally{
if(in!=null)try{in.close();}catch (Exception e){}
}
}
public static void main(String[] args) throws ClassNotFoundException {
SimpleAppClassLoader loader=
new SimpleAppClassLoader("D:\\eclipse-workspace\\Unit01\\bin\\");
//loader.loadClass("pkg.Hello");
Class.forName("unit01.HelloWorld",true,loader);
}
}
/**
* 自定义类加载器 , 打破双亲委派机制?
* 1)为什么要打破?(一个tomcat中假如运行两个服务,这两个服务中有类全名相同的类如何加载?)
* 2)如何打破?(重写ClassLoader类中的loadClass方法)
*/
public class BreakDoubleParentAppClassLoader extends ClassLoader{
private String baseDir;
public BreakDoubleParentAppClassLoader(String baseDir){
this.baseDir=baseDir;
}
//一般要打破双亲委派机制时可以重写loadClass方法
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
Class<?> clazz = findClass(name);//自己直接从指定位置加载自己写的类
return clazz;
}catch (Exception e){
return super.loadClass(name, resolve);//使用双亲委派机制加载(推荐基础类库还要使用这种方式)
}
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
String classPath=baseDir+className.replace(".", "\\")+".class";
File file=new File(classPath);
InputStream in=null;
try {
in=new FileInputStream(file);
byte[] array=new byte[in.available()];//in.available()获取流中有效字节数
int len=in.read(array);
return defineClass(className, array, 0, len);
}catch (IOException e){
throw new RuntimeException(e);
}finally{
if(in!=null)try{in.close();}catch (Exception e){}
}
}
public static void main(String[] args) throws ClassNotFoundException {
BreakDoubleParentAppClassLoader loader1=
new BreakDoubleParentAppClassLoader("D:\\eclipse- workspace\\Unit01\\bin\\");
BreakDoubleParentAppClassLoader loader2=
new BreakDoubleParentAppClassLoader("D:\\eclipse-workspace\\Unit01\\bin\\");
//loader.loadClass("pkg.Hello");
Class<?> c1=Class.forName("unit01.HelloWorld",true,loader1);
Class<?> c2=Class.forName("unit01.HelloWorld",true,loader2);
System.out.println(c1==c2);//false
}
}
FAQ:
1.一个类的字节码对象在内存中只能有一份,正确吗?
不正确, 这句话是否正确要看具体的环境, 比方说你是否打破了双亲委派机制,你使用的是否是 同一个类加载器
2.判断两个类的字节码对象是否相同的标准是什么?
类相同,类加载器也要相同
3.每个类加载器都有自己的内存,正确吗?
正确