类加载器
类加载
类加载器
(这里参考ZYG同学的博客——类加载)
①类加载器之间的关系如上图示
[BootstrapClassLoader]-启动类加载器:(Cpp)加载JAVA核心类库,位于java.*
在Java_Home/lib
目录下
[ExtClassLoader] -扩展类加载器:(java)加载扩展库,如classpath
中的jre
,javax.*
或者java.ext.dir
指定位置中的类,开发者可以直接使用标准扩展类加载器。
[AppClassLoader]-系统类加载器(应用程序类加载器):(java)加载程序所在的目录,如user.dir
所在的位置的.class
。也就是说我们写的代码的类是被这个加载器加载的。(系统类加载器和启动类加载器名称区分清)
[CustomClassLoader]-自定义类加载器:java
编写,用户自定义的类加载器,可加载指定路径的class
文件
自定义类加载器的使用情景:热插拔。比如同名jsp文件要替换掉,那么重新生成一个累加载器,再加载一下这个类,把原来的关了,重新生成页面,不用停机
一些实现和API:ClassLoader
是类加载器类,通过ClassLoader.getSystemClassLoader()
方法返回应用程序的系统类加载器。然后其提供一系列方法getParent()
获取类加载器的父类加载器、loadClass(String name)
、findClass(String name)
、findLoadedClass(String name)
、defineClass(String name,byte[] b,int off,int len)
(可以将.class文件转化为Class对象)、resolveClass(Class<?> c)
(链接指定的java类)
见解析ClassLoader、getSystemClassLoader与Launcher
双亲委派机制
当某个类加载器需要加载某个
.class
文件时,它首先把这个任务[从当前类加载器]委托[给他的上级类加载器],[递归]这个操作,如果上级的类加载器没有加载,自己才会去加载这个类,加载了就直接返回。这是一种代理模式。意义
①保证一个类只被加载一次,省内存
②安全(保证不被篡改)
为了系统类的安全,类似“
java.lang.Object
”这种核心类,JVM需要保证他们生成的对象都会被认定为同一种类型。即“通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的”。具体来说:比如我们想写一个
java.lang.Object
类,我们写的类属于程序的类,当前由系统类(应用程序类)加载器加载,它是启动类加载器和扩展类加载器的儿子,那么根据双亲委托机制,就会先由父类的类加载器来加载java.lang.Object
,那么这样我们自己写的类就无法被加载(比如我们自己定义包名和类名叫java.lang.Object)
,那么我们的系统类加载器加载这个类之前先会找更上层的类加载器,更上层的类加载器在java核心类库找到了java.lang.Object
,那么久不会再调用系统类加载器加载我们自己写的了);不过我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。注意
注意要明白是从当前类加载器开始往上递归(这也导致实现SPI机制的类库无法通过原始方式加载,原始方式的当前类加载器是启动类加载器,找不到SPI类库)
上下文类加载器
可以破坏双亲委派机制,上级可指派下级类加载器来加载类
上面的SPI机制中就用到了
类加载阶段包括加载、连接(验证、准备、解析)、初始化、使用和卸载,它们开始的顺序一定,但是完成的顺序并不一定,因为在一个阶段内会调用其他阶段。
整个初始化流程见JAVA-Ch.14类型信息-初始化流程
JVM如何认定两个对象同属于一个类型,必须同时满足下面两个条件:
基于双亲委派机制,可以看出
都是用同名的类完成实例化的。
两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被JVM认定为不同类型的对象。
TiPS
常量池
在Java方法区会维护一个常量池,对于使用简单赋值直接创建的一些可以共享的字符串、基本类型,会先在常量池中查找是否存在相同的内容,如果有就直接返回,不创建新对象。(JAVA字符串不可修改,修改其实就是建立一个新的String对象)
而如果是显式创建对象就不会是这样的,new就是new个新的了//显式创建(非字符串和基本类型,只能显式创建) System.out.println(new test() == new test()); //输出false,不是同一个对象 //显式创建 String a = new String("abc"); String b = new String("abc"); System.out.println(a==b); //输出false,不是同一个对象,未共享对象,其它基本类型的包装类也是同理 //非显式创建 String a = "abc"; String b = "abc"; System.out.println(a==b); //输出true,是同一个对象,共享对象,其它基本类型同理
static
变量也是常量,储存于常量池类加载器的加载路径和
import
引入声明是两个东西,import是一个使用声明,是给编译器用的,静态;类加载器的加载路径是运行时,类加载器只认识加载路径下的类,动态。类加载器认识,但不一定会import:比如lang是启动类加载器认识,且默认import(只有lang默认import),util是扩展类加载器认识,但不默认import。JAVA的很多东西都是告诉建议不这么做,但你可以这么做,比如final修饰符,意思是告诉别人你不要改我,但是可以用反射破坏;双亲委派机制就是告诉你别替代我,但我可以用上下文类加载器破坏;异常机制提醒你要处理异常,但你可以直接抛出不处理
基本数据类型存放在哪里?
①在方法/循环中申明的基本类型放在栈中
②在对象中申明的基本类型放在堆中
③被final修饰的基本数据类型和String类型变量在编译时会被确定下来,存放在常量池中。
java哪些情况会内存泄漏
程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被强引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中的内存泄露
①长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。
比如有一个循环,循环内部不断生成对象,但这个对象是在循环外面定义的,外部持有内部的对象的引用。在一般的循环中建立的变量都是局部变量(局部方法栈中),然而由于外部对象的引用,即使循环结束后,局部对象迟迟无法被gc回收,导致不被使用的对象不断增加导致内存泄漏。
可以在循环内部用完将对象设置为null即可。
②集合中的内存泄漏,比如 HashMap、ArrayList 等(加入集合等于一个强引用)
我们将引用add入集合,然后令该引用=null,但实际上由于集合对象还对该对象有一个强引用,那么该对象实际上并没有被释放
见:纳尼,Java 存在内存泄泄泄泄泄泄漏吗? - 纯洁的微笑的文章 - 知乎 https://zhuanlan.zhihu.com/p/66689341
可以使用JVisualVM等工具来监视jvm运行信息来判断内存泄漏