by shihang.mai
0. 生命周期概述
- 我们编写的源代码(.java文件),需要经过javac,将从java文件变为class文件。这个阶段叫编译期,属前段编译。
- class文件,经过类初始化过程,然后new 类被我们程序中使用。这个阶段叫运行期,涉及后端编译。
- 类卸载
1. 编译期
源代码.java会经过词法分析、语法分析、语义分析与中间代码生成,生成.class文件
1.1 编译期优化
- 如没定义构造方法,自动添加默认无参构造
- 自动拆装箱
- 泛型擦除
- 可变参数。参数:String...args->String [] args
- lombok,在设置了相关注解后lombok会在编译期生成源代码中没有的方法等
2. 运行期
- jvm一般都有解析器和编译器。
- 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
- 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地机器码之后,可以获得更高的执行效率。即使用JIT技术。将热点代码变为机器码,把将.class文件翻译成机器指令
哪些是热点代码?
- 被多次调用的方法(JIT)
- 被多次执行的循环体 OSR
如何找到热点方法?
-
基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是热点方法
-
基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。
HotSpot 虚拟机采用的是第二种:基于计数器的热点探测。因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter),这两个计数器都有一个确定的阈值,当计数器超过阈值就会触发 JIT 编译
方法调用计数器
方法调用计数器
方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器值就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰期
回边计数器
回边计数器
回边计数器没有计算热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程
2.1 运行期优化
- 公共子表达式消除
如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替 E 就好了 - 数组边界检查消除
在访问数组时,系统会自动进行上下界的范围检查。但是有N个数组,这样多个判断要消耗很多资源。在循环的时候访问数组,如果编译器只要通过数据流分析就知道循环变量是不是在区间 [0, array.length] 之内,那在整个循环中就可以把数组的上下界检查消除 - 方法内联
调用同样的方法,直接拆去方法的调用 - 逃逸分析
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
4.1 栈上分配:如果确定一个对象不会逃逸到方法之外,那么就可以在栈上分配内存.对象所占的内存空间就可以随栈帧出栈而销毁,这样会大大减轻 GC 的压力
4.2 同步消除:如果逃逸分析能确定一个变量不会逃逸出线程,无法被其它线程访问,那这个变量的读写就不会有多线程竞争的问题,因而变量的同步措施也就可以消除了
4.3 标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,那程序执行的时候就可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来替代 -
锁粗化
JVM会检测到一连串的操作都对同一个对象加锁,那么会将锁粗化到这一连串操作外 锁粗化.jpg
-
锁消除
Java虚拟机在JIT编译时,通过对运行上下文的扫描,经过逃逸分析(没方法逃逸、没线程逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗
锁消除.png
3. 类初始化过程
类初始化过程3.1 class文件结构
可用idea插件BinEd查看.class 16进制码
可用idea插件JclassLib查看.class 详细信息
class文件有以下几个属性
属性 | 备注 |
---|---|
magic | class文件标示 CAFE BABE |
minor version | 小版本(52.0的0) |
Major version | 大版本(52.0的52) |
Constant_pool_count: | 常量池个数。常量池编号从1开始 |
Constant | 常量。个数=Constant_pool_count-1 |
access_flags | 标识public final interface |
This class | 当前class在常量池的位置 |
Super class | 父class在常量池的位置 |
Interface_count | 接口数量 |
Interfaces | 接口 |
Fields_count | 属性数量 |
fields | 属性 |
Method_count | 方法数量 |
Methods | 方法 |
Attributes_count | 附加值数量 |
Attributes | 附加值 |
3.2 Loading
3.2.1 双亲委派模型
ClassLoader和其加载的类路径
类加载器 | 加载的class |
---|---|
BootStrap | lib/rt.jar charset.jar等核心类,C++实现的 |
Extension | jre/lib/ext/*.jar |
App | classpath下的class |
Custom | 自定义加载 |
Class都是被ClassLoader加载入内存的,ClassLoader间并没有extends关系,ClassLoader(基础的App Extension)都是BootStrap加载的。
自定义的ClassLoader实际就是自己写的一个类,那么肯定是App加载,即自定义的ClassLoader由App加载。
ClassLoader双亲委派模型双亲委派模型作用:1.主要为了安全 2.次要为了资源,加载过的不加载
例如:现在有一个叫java.lang.String(自己写的),直接用CustomClassLoader加载,打包成一个jar,交给客户,然后客户输入密码,存放在String,将密码偷偷向我邮箱发送。全世界用到我类库的,我都能获取到密码。
3.2.2 破坏双亲委派模型
首先双亲委派模型在ClassLoader.loadClass()中写的,重写loadClass即可破坏双亲委派
-
JNDI定义了一些标准的接口放在BootStrap加载,例如,jdbc是需要各厂商第三方实现的,必然不是BootStrap加载,但是工程启动时,却父能看见子,根据双亲委派模型可见性,显然破坏了
-
tomcat作为一个容器,它可以启动多个war,而war中同一个类可以是多个版本
3.2.3 自定义类加载器
查看ClassLoader类中的loadClass(),其中findClass()用到了模版方法设计模式
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 {
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();
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;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
extends ClassLoader,重写findClass方法即可,在findClass方法里面还需要用到defineClass方法。
public class T006_MSBClassLoader extends ClassLoader {
public T006_MSBClassLoader() {
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean var5 = false;
int b;
while((b = fis.read()) != 0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception var7) {
var7.printStackTrace();
return super.findClass(name);
}
}
public static void main(String[] args) throws Exception {
ClassLoader l = new T006_MSBClassLoader();
Class clazz = l.loadClass("com.maishihang.jvm.Hello");
Class clazz1 = l.loadClass("com.maishihang.jvm.Hello");
System.out.println(clazz == clazz1);
Hello h = (Hello)clazz.newInstance();
h.m();
//===>App
System.out.println(l.getClass().getClassLoader());
//===>App
System.out.println(l.getParent());
System.out.println(getSystemClassLoader());
}
}
- 先利用loadClass进行双亲委派模型
- 父加载不了,进入findClass,找class加载
- 使用defindClass去返回Class对象
3.3 Linking
-
verification
验证文件是否符合JVM规定
-
preparation
静态变量赋默认值
-
resolution
将类、方法、属性等符号引用解析为直接引用。
在16进制的文件里面,下面方法用到了#1->常量池1号的地方,这个resolution就是将这个#1符号引用解析为真正内存->Object直接引用
符号引用 常量池
3.4 Initializing
静态变量赋初始值
public class T001_ClassLoadingProcedure {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T {
public static T t = new T(); // null
public static int count = 2; //0
//private int m = 8;
private T() {
count ++;
//System.out.println("--" + count);
}
}
- 当调用T.count时,AppClassLoader把T加载到内存
- 然后执行verification->preparation,将t=null,count=0,->resolution
- Initializing,t=new T(),调用构造方法,count=1
- count赋初始值,count=2
public class T001_ClassLoadingProcedure {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T {
public static int count = 2; //0
public static T t = new T(); // null
//private int m = 8;
private T() {
count ++;
//System.out.println("--" + count);
}
}
- 当调用T.count时,AppClassLoader把T加载到内存
- 然后执行verification->preparation,将t=null,count=0,->resolution
- count赋初始值,count=2
- Initializing,t=new T(),调用构造方法,count++,count=3
4. new 类()
对象中的成员变量赋值也分为两部,new T(),向内存申请空间,m=0,再调用构造方法,m=8
5. 类卸载
类除了以上的初始化过程,当然还有类卸载构成整个类的生命周期。
而当代表Msh类的Class对象不再被引用,即不可即不可触及时,Class对象就会结束生命周期,Msh类在方法区内的数据也会被卸载,从而结束Msh类的生命周期,
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
5.1 Class对象关系
ClassLoader会用一个集合存放自己加载过的Class对象的引用,然后Class对象. getClassLoader()又能获取到加载自己的ClassLoader
ClassLoader与Class对象相向联系
每个类的实例.getClass()能获取到自己对象的Class对象
5.2 类卸载条件
- 只有自定义类加载器加载的类才可能被卸载
- 该类所有的实例已经被回收
- 加载该类的ClassLoder已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用
https://zhuanlan.zhihu.com/p/71566226
网友评论