文章目录
概念
在java代码中,类型的加载、连接、初始化都是在程序运行期间完成
提供了更大的灵活性,增加了更多的可能性
java虚拟机的生命周期
java虚拟机的生命周期
几种结束虚拟机生命周期的情况
- 调用System.exit(),命令结束生命周期
- 程序执行完成
- 程序抛出异常到最上层给予虚拟机一直未处理
- 操作系统发生错误导致jvm虚拟机进程关闭
System.exit()执行 finally就不会执行,因为程序已经退出
在连接的准备阶段,为静态变量设置默认值。
在初始化阶段为静态变量赋值为正确的值,所以在初始化中静态变量实际上值从默认值到设置的值
jvm参数
参数格式:-XX固定格式
- -XX:+ 表示开启参数
- -XX:- 表示关闭参数
- -XX:= 将option选项值设置为value
有趣的final
User.OUT有final和没有final输出不一样,查看类加载也可以看到jvm.User from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/] 被加载
package jvm;
/**
* Created by LiRui on 2019-07-25.
*
* @author LiRui
* @version 1.0
*/
public class Main {
public static void main(String[] args) {
System.out.println(User.OUT);
}
}
class User {
public static String OUT = "Hello User!";
static {
System.out.println("I'm User static");
}
public User() {
System.out.println("I'm User");
}
}
输出:
I'm User static
Hello User!
package jvm;
/**
* Created by LiRui on 2019-07-25.
*
* @author LiRui
* @version 1.0
*/
public class Main {
public static void main(String[] args) {
System.out.println(User.OUT);
}
}
class User {
public static final String OUT = "Hello User!";
static {
System.out.println("I'm User static");
}
public User() {
System.out.println("I'm User");
}
}
输出:
Hello User!
问题答案
答案就在编译后class
package jvm;
public class Main {
//没带final关键字编译结果
public Main() {
}
public static void main(String[] args) {
System.out.println(User.OUT);
}
}
package jvm;
public class Main {
//带final关键字编译结果
public Main() {
}
public static void main(String[] args) {
System.out.println("Hello User!");
}
}
原因:
因为声明字符串为final说明不可修改,不可变。在编译阶段编译器直接将字符串移动到调用位置成为普通常量(放到当前调用处的常量池中),所以就不会调用User类,User类也就不会初始化
从这里看出来常量类全部定义成为final也可以少加载一个类,可以节约资源。在书中也有记载在类、方法明确定义为final时就不会去寻找加载父类,也算是节约资源。但是也缺失了延展性
final 使用影响类加载另一种情况
package jvm;
import java.util.UUID;
/**
* Created by LiRui on 2019-07-29.
*
* @author LiRui
* @version 1.0
*/
public class OutUUID {
public static void main(String[] args) {
System.out.println(UUIDOut.UUID_OUT);
}
}
class UUIDOut{
public static final String UUID_OUT = UUID.randomUUID().toString();
static {
System.out.println("I'm UUIDOut");
}
}
输出结果:
I'm UUIDOut
ad790d0d-8c47-4f37-9079-03d7ef7ace17
在打印加载类时发现确实加载了UUIDOut类
查看编译class
package jvm;
public class OutUUID {
public OutUUID() {
}
public static void main(String[] args) {
System.out.println(UUIDOut.UUID_OUT);
}
}
原因:在编译期不能确定常量值时该变量就不会放到调用处的常量池中,所以在调用时也会加载初始化UUIDOut类
结论
当一个常量的值并非编译期可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然导致这个类被初始化
反编译工具
命令:java -c xxxx地址
code: 前面是助记符
Java助记符
localhost:jvm LiRui$ javap -c out/production/jvm/jvm/Main.class
Compiled from "Main.java"
public class jvm.Main {
public jvm.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello User!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
助记符也是通过jdk的类进行操作的查看:com.sun.org.apache.bcel.internal.generic包下
创建数组
- 数组类型是在运行期动态生成的[Ljvm.xxx类型 类。动态生成的类型,其父类是Object
对于数组来说,javaDoc经常构成数组元素为Component,实际上就是将数组降低一个维度后的类型
package jvm;
import java.util.UUID;
/**
* Created by LiRui on 2019-07-29.
*
* @author LiRui
* @version 1.0
*/
public class OutUUID {
public static void main(String[] args) {
UUIDOut[][] outs = new UUIDOut[20][];
System.out.println(UUIDOut.outs.getClass().toString());
}
}
class UUIDOut {
public static final String UUID_OUT = UUID.randomUUID().toString();
public static final UUIDOut[][] outs = new UUIDOut[20][];
static {
System.out.println("I'm UUIDOut");
}
}
输出结果:
I'm UUIDOut
class [[Ljvm.UUIDOut;
- 如输出结果可得,因为数组在编译器不可得,所以在调用数组时,也初始化UUIDOut类。即数组在UUIDOut的常量池中
数组加载器
数组类的加载器由数组元素的类加载器加载
注意:原生类型数组没有加载器,所以以下程序的两个null不同
package jvm;
/**
* Created by LiRui on 2019-08-01.
*
* @author LiRui
* @version 1.0
*/
public class TestMain0801 {
/**
* 输出结果:
* sun.misc.Launcher$AppClassLoader@18b4aac2
* sun.misc.Launcher$ExtClassLoader@610455d6
* null
* 输出null说明是启动类(根类)加载器
*
* @param args
*/
public static void main(String[] args) {
String[] strings = new String[10];
System.out.println(strings.getClass().getClassLoader());
TestMain0801[] main0801s = new TestMain0801[20];
System.out.println(main0801s.getClass().getClassLoader());
int[] ints = new int[10];
System.out.println(ints.getClass().getClassLoader());
}
}
输出:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
String类为根加载器加载所以输出为null
TestMain0801为系统加载器加载
接口
-
在接口中定义的变量默认是public static final
-
接口运行期能确定值
package jvm;
/**
* Created by LiRui on 2019-07-30.
*
* @author LiRui
* @version 1.0
*/
public class MainTest {
public static void main(String[] args) {
System.out.println(People.name);
}
}
interface People extends Obj {
String name = "name";
int age = 20;
}
interface Obj {
String ha = "ha";
}
编译文件:
package jvm;
public class MainTest {
public MainTest() {
}
public static void main(String[] args) {
System.out.println("name");
}
}
- 运行期不能确定值
package jvm;
import java.util.UUID;
/**
* Created by LiRui on 2019-07-30.
*
* @author LiRui
* @version 1.0
*/
public class MainTest {
public static void main(String[] args) {
System.out.println(People.name);
}
}
interface People extends Obj {
String name = UUID.randomUUID().toString();
int age = 20;
}
interface Obj {
String ha = "ha";
}
编译文件:
package jvm;
public class MainTest {
public MainTest() {
}
public static void main(String[] args) {
System.out.println(People.name);
}
}
跟踪类加载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
总结
直接调用接口中的运行期可确定变量(public static final),该变量在编译期直接放到调用处的常量池中。调用运行期不可确定变量(在运行期确定),则会初始化接口。并且会初始化当前接口的父类接口。
在连接的准备阶段静态变量会被赋上初始化值,在初始化时再给赋程序值,所以static的顺序可能影响变量的值。
加载器
static
静态块只有在初始化时第一次运行
{}语法,在每次初始化都会执行
class Child implements People{
{
System.out.println("每次实例化都会执行");
}
public Child() {
System.out.println("实例化");
}
}
每次实例化输出:
每次实例化都会执行
实例化
类初始化时机
- 当虚拟机初始化一个类时,要求它的所有类都已经初始化,但是这条规则不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
- 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定的静态变量时,才会导致该接口初始化
package jvm;
/**
* Created by LiRui on 2019-07-30.
*
* @author LiRui
* @version 1.0
*/
public class MainTest {
public static void main(String[] args) {
System.out.println(Child.child_name);
}
}
interface People extends Obj {
Thread thread = new Thread() {
{
System.out.println("初始化people接口");
}
};
}
interface Obj {
String ha = "ha";
}
class Child implements People{
public static String child_name = "name";
}
输出结果:
name
类装载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.Child from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
当Prople换成class
package jvm;
/**
* Created by LiRui on 2019-07-30.
*
* @author LiRui
* @version 1.0
*/
public class MainTest {
public static void main(String[] args) {
System.out.println(Child.child_name);
}
}
class People implements Obj {
public static Thread thread = new Thread() {
{
System.out.println("初始化people接口");
}
};
}
interface Obj {
String ha = "ha";
}
class Child extends People{
public static String child_name = "name";
}
输出:
初始化people接口
name
类装载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.Child from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
结果
如上两段代码,People不管为接口还是类时都装载了,但是接口时并没有输出“初始化people接口”,说明接口时并没有初始化。
- 验证上面说法
- 说明装载不一定被初始化,这是两个过程一定要注意
自定义类加载器
Java自定义类加载器
在自定义加载器时,没有置空父加载器时。默认父加载器为系统加载器,由于加载器的双气委派机制,所以自定加载器默认情况下一般是不会调用到自己写的方法,直接由父加载器直接加载。
类加载器双亲委托模型作用
-
确保Java核心库类型安全
所有的Java应用都会引用java.long.Object类,也就是在运行期 java.long.Object类会被加载到虚拟机中。如果加载由自定义加载器完成,很可能在jvm中存在多个版本的java.long.Object类,而且这些类相互不兼容相互不可见(命名空间不同)。
借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间相互兼容
-
确保Java核心库的类不会被自定义类库替代
-
不同的类加载器可以为相同名称的类创建额外的命名空间,相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可(命名空间不同)。不同类加载器加载的类之间是不兼容的,相当于在Java虚拟机内部创建了相互隔离的java类空间,这类技术在很多框架都得到了应用
摘抄牛客网回答:加载,JVM第一次使用到这个类时需要对,这个类的信息进行加载。一个类只会加载一次,之后这个类的信息放在堆空间,静态属性放在方法区。JVM类加载器从上到下一共分为三类 1.启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
原始类加载器
内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块人特殊的机器码会运行,他会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(bootstrap)
启动类加载器并不是Java类,而其他的加载类是Java类,启动类加载器是特定于平台的机器指令,他负责开启整个加载过程。
所有类加载器(除了启动类加载器)都被实现为Java类。不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,家在第一个纯Java类加载器就是启动类加载器的职责。
启动类加载器还会负责加载提供JRE正常运行所需的基本组件,这包括java.util与java.lang包中的类等等。
线程上下文类
/**
* 输出
* sun.misc.Launcher$AppClassLoader@18b4aac2
* null
*
* @param args
*/
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Thread.class.getClassLoader());
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
解释:Thread为com.long包中,由根加载器加载。
当前类加载器
Thread.currentThread().getContextClassLoader()即获取当前类加载器
每个类都会使用自己的类加载器(即加载自身的类加器),来去加载其他类(指的是所依赖的类),如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提的ClassY尚未被加载)
线程上下文加载器(Context ClassLoader)
线程上下文类加载器是从JDK1.2开始引入。类Thread中的getContextClassLoader()与setContextClassLoader(ClasssLoader loader)分别用来获取和设置上下文类加载器
如果没有通过setContextClassLoader(ClasssLoader loader)进行设置的话,线程默认将继承其父线程的上下文的类加载器。Java应用的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过类加载器来加载资源。
上下文类加载器重要性
SPI(Sservice Provider Interface)
SPI机制详解
父ClassLoader可以使用当前线程Thread.currentThread.getContextLoader()所指定的classLoader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载类的情况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的Current ClassLoader
在双亲委托模型下,类加载器是由上至下,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不用的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文加载器,就可以由设置的上下文类加载器来实现对于接口类的加载
上下文类加载器的使用模板
上下文类加载器默认为系统类加载器
/**
* 线程上下文类加载器一般是使用模式(获取-使用-还原)
*/
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(设置类加载器);
执行逻辑,这里就可以通过当前线程获取上下文类加载器执行自己想加载的类
}finally {
//还原上下文类加载器
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
上下文累类加载器主要作用是为了“破坏双亲委托机制”能够在SPI情况下,高层类加载器也能加载低层实现类
ServiceLoader
在ServiceLoader描述中:通过在资源目录 META-INF/services 中放置提供者配置文件 来标识服务提供者。文件名称是服务类型的完全限定二进制名称。
ServiceLoader在JDK1.6中加入,就是为了处理SPI情况下的类加载问题。
本身ServiceLoader在java.util中,由根加载器加载。由于SPI情况,根加载器不能加载服务厂商实现类,可以由系统类加载器进行加载,所以使用上下文类加载器来处理。
主要代码:
//默认使用上下文类加载器为系统类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
文件加载主要实现在LazyIterator中:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
/**
* 判断是否存在下一个服务实现
*/
private boolean hasNextService() {
//nextName为null说明调用next或者第一次调用
if (nextName != null) {
return true;
}
//判断是否存在配置资源
if (configs == null) {
try {
//配置位置"META-INF/services/" + "java.sql.Driver"
String fullName = PREFIX + service.getName();
//没有配置加载器,默认系统类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
//使用配置加载器加载
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
//没有更多的元素
if (!configs.hasMoreElements()) {
return false;
}
//解析文件里名称
pending = parse(service, configs.nextElement());
}
//写入下一个实现类名称
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
//文件中需要加载的类名如:com.mysql.jdbc.Driver
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//加载指定类,不用初始化,指定类加载器(这里为是上下文加载器即:系统类加载器)
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口(做校验)
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//将一个对象强制转换成此 Class 对象所表示的类或接口
S p = service.cast(c.newInstance());
//kv形式存储对象
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
修改上下文加载器
import java.sql.Driver;
import java.util.ServiceLoader;
/**
* 线程上下文类加载器一般是使用模式(获取-使用-还原)
*/
public class Test20190809 {
public static void main(String[] args) {
//修改上下文加载器为扩展类加载器
Thread.currentThread().setContextClassLoader(Test20190809.class.getClassLoader().getParent());
//1.6版本引入
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
for (Driver driver : serviceLoader) {
System.out.println("driver: " + driver.getClass() + ", loader: " + driver.getClass().getClassLoader());
}
System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader());
//输出null 由启动类负责加载
System.out.println("serviceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
}
}
输出:
当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@6a38e57f
serviceLoader的类加载器:null
由程序输出可以看到上下文类加载器为扩展类加载器,但是没有输出mysql jdbc的加载类,说明没有识别到文件
问题: 问题就在于这个hasNextService方法中configs = loader.getResources(fullName);,由于loader为扩展类加载器所以加载的根目录就变成了JAVA_HOME/jre/lib/ext/ 所有找不到对应的配置值JAVA_HOME/jre/lib/ext/META-INF/services/ java.sql.Driver 所以并没有输出