源文件
package com.mncode.jvm.bytecode;
public class Test1 {
private int a;
public int inc() {
return a + 1;
}
}
编译
基本格式:javac <options> <source files>
例如:javac -g Test1.java
分析字节码工具
WinHex
HEX Editor Neo
javap
javap -verbose
public class com.mncode.jvm.bytecode.Test1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/mncode/jvm/bytecode/Test1.a:I
#3 = Class #20 // com/mncode/jvm/bytecode/Test1
#4 = Class #21 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/mncode/jvm/bytecode/Test1;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Test1.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // a:I
#20 = Utf8 com/mncode/jvm/bytecode/Test1
#21 = Utf8 java/lang/Object
{
public com.mncode.jvm.bytecode.Test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mncode/jvm/bytecode/Test1;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/mncode/jvm/bytecode/Test1;
}
SourceFile: "Test1.java"
jclasslib
https://github.com/ingokegel/jclasslib
idea插件
jclasslib
分析字节码
整体结构
序号 | 类型 | 名称 | 数量 | 说明 |
---|---|---|---|---|
1 | u4 | magic | 1 | 魔数 |
2 | u2 | minjor_version | 1 | 次版本号 |
3 | u2 | major_version | 1 | 主版本号 |
4 | u2 | constant_pool_count | 1 | 常量池数量 |
5 | cp_info | constant_pool | constant_pool_count | 常量池 |
6 | u2 | access_flags | 1 | 类访问权限 |
7 | u2 | this_class | 1 | 类名 |
8 | u2 | super_class | 1 | 父类名 |
9 | u2 | interfaces_count | 1 | 接口数量 |
10 | u2 | interfaces | interfaces_count | 接口名 |
11 | u2 | fields_count | 1 | 字段数量 |
12 | field_info | fields | fields_count | 字段表 |
13 | u2 | methods_count | 1 | 方法数量 |
14 | method_info | methods | methods_count | 方法表 |
15 | u2 | attributes_count | 1 | 附加属性数量 |
16 | attribute_info | attributes | attributes_count | 附加属性表 |
1、魔数:cafebabe
2、次版本号:0
3、主版本号:对应十进制为52,对应 JDK为1.8
4、常量池数量:22,注意常量池计数是从1开始的(0有特殊意义),所以常量池中的数量应为21个;
5、常量池:以上红框圈中的即是常量池;
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/mncode/jvm/bytecode/Test1.a:I
#3 = Class #20 // com/mncode/jvm/bytecode/Test1
#4 = Class #21 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/mncode/jvm/bytecode/Test1;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Test1.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // a:I
#20 = Utf8 com/mncode/jvm/bytecode/Test1
#21 = Utf8 java/lang/Object
6、类访问权限:0021,对应:ACC_PUBLIC
和ACC_SUPER
7、类名:对应3号常量,3号又引用了20号,所以为:com/mncode/jvm/bytecode/Test1
8、父类名:java/lang/Object
9、接口数量:0
10、接口名:无
11、字段数量:1
12、字段表:
字段表结构
序号 | 类型 | 名称 | 数量 | 说明 |
---|---|---|---|---|
1 | u2 | access_flags | 1 | 访问权限 |
2 | u2 | name_index | 1 | 字段名 |
3 | u2 | descriptor_index | 1 | 字段描述符 |
4 | u2 | attributes_count | 1 | 属性数量 |
5 | attributes_info | attributes | attributes_count | 属性表 |
0002 0005 0006 0000
访问权限:private
字段名:a
字段描述符:I(即int类型)
属性数量:0
13、方法数量:2
14、方法表:
方法表结构
序号 | 类型 | 名称 | 数量 | 说明 |
---|---|---|---|---|
1 | u2 | access_flags | 1 | 访问权限 |
2 | u2 | name_index | 1 | 字段名 |
3 | u2 | descriptor_index | 1 | 字段描述符 |
4 | u2 | attributes_count | 1 | 属性数量 |
5 | attributes_info | attributes | attributes_count | 属性表 |
对应以下两个方法:
public com.mncode.jvm.bytecode.Test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mncode/jvm/bytecode/Test1;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/mncode/jvm/bytecode/Test1;
15、 附加属性数量:1
16、 附加属性表:
附加属性表结构
序号 | 类型 | 名称 | 数量 | 说明 |
---|---|---|---|---|
1 | u2 | attribute_name_index | 1 | 属性名 |
2 | u4 | attribute_length | 1 | 属性值长度 |
3 | u1 | info | attribute_length | 属性值 |
0010 00000002 0011
attribute_name_index:16号常量为SourceFile
attribute_length:2(即后面的两个字节存储属性值)
info:17号常量为Test1.java
表示这个class文件是从Test1.java编译来的。
类加载器
作用
加载class文件字节码,在运行时数据区中生成一个代表这个类的Class对象。
类的生命周期
种类
启动类加载器:负责加载<JAVA_HOME>\lib目录下或者参数-Xbootclasspath指定路径下的类库(如rt.jar);
扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录下或者系统变量java.ext.dirs指定路径下的类库;
应用类加载器:负责加载用户类路径(ClassPath)下的类库,默认的类加载器;
自定义类加载器:可以通过继承ClassLoader来实现;
双亲委派模型
引入时间:JDK1.2
说明:
当一个类加载器收到类加载请求时,它不会先自己去加载类,而是把请求委托给父类加载器,请求依次向上传递;当父类加载器无法加载时(在负责的范围内没有找到指定类),子加载器才会自顶向下依次尝试加载类。
好处:
- 避免同一个类被多次加载;
- 每个加载器只能加载自己范围内的类;
源码(ClassLoader类的loadClass方法):
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载过
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) {
long t1 = System.nanoTime();
// 若还没找到,则调用findClass方法查找
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
主要方法:loadClass
,findClass
、defineClass
loadClass
:通过类的全限定名完成类的加载,返回Class对象,内部实现了双亲委派的逻辑,若要破坏双亲委派机制,可以重写这个方法;
findClass
:自定义类加载器时,一般只重写这个方法即可;loadClass内部会调用这个方法;
源码:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass
:将class文件字节数组转换为Class
对象;
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
自定义类加载器
代码 1:
package com.mncode.jvm.classloader;
public class MyClassLoader extends ClassLoader {
private final String POSTFIX = ".class";
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] classBytes = getClassBytes(className);
if (classBytes == null) {
throw new ClassNotFoundException(className);
}
return defineClass(className, classBytes, 0, classBytes.length);
}
/**
* 获取class字节数组(可以通过文件、网络等方式获取)
*
* @param className 类全限定名
* @return class字节数组
*/
private byte[] getClassBytes(String className) {
Path path = Paths.get(classPath, className.replace('.', File.separatorChar) + POSTFIX);
try {
return Files.readAllBytes(path);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
代码 2:
public class Hello {
}
测试代码:
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
// 自定义类加载器
MyClassLoader classLoader = new MyClassLoader("F:\\classes");
// 根据全限定名加载指定类
Class c = classLoader.loadClass("com.mncode.jvm.classloader.Hello");
System.out.println(c.getClassLoader());
Object obj = c.newInstance();
System.out.println(obj);
}
}
测试结果:
备注:
如果开发工具自动做了编译,如下图所示:
则测试结果为:
因为根据双亲委派机制可知,类加载请求会传递给父加载器,而应用类加载器会在类路径下找到文件,所以最终由应用类加载器加载成功;把这个文件删掉,才会是自定义的类加载器去加载。
运行时数据区
基本结构
简要说明
程序计数器:当前线程所执行的字节码的行号指示器(线程私有);
虚拟机栈:java方法执行的内存模型(线程私有);
本地方法栈:类似虚拟机栈,不同的是为native方法服务(线程私有);
堆:存储对象实例的区域(线程共享);
方法区:存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(线程共享)。
垃圾回收
回收什么?
回收已经死掉的对象(没有任何引用指向的对象)。
怎么知道哪些对象已死?
方法有以下两种:
- 引用计数算法
简介 | 对象中添加一个引用计数器;对象每被引用一次,计数器加 1;引用每取消一次,计数器减 1 ;当计数器为 0 时,表示对象死掉 |
优点 | 实现简单,判定效率高 |
缺点 | 当对象之间互相引用时,即使这些对象已经没用了,计数器不为 0,所以不能被回收 |
- 可达性分析算法
简介 | 以一系列的 "GC Roots" 对象为起点,开始向下搜索,节点走过的路径称为引用链,当一个对象与 "GC Roots" 之间没有任何引用链相连时,表示对象死掉 |
优点 | 可以解决对象互相引用的问题 |
可作为GC Roots
的对象包括下面几种:
- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中
JNI
(Native
方法)引用的对象。
回收方法区
主要回收两方面的内容:
一. 废弃常量
判定条件:没有地方引用这个常量。
二. 无用的类
判定条件:
1.该类的所有实例都被回收;
2.加载该类的ClassLoader已被回收;
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
- 标记 - 清除算法(
Mark-Sweep
)
最基础的收集算法,分为标记和清除两个阶段。
流程:
1.先标记出所有需要回收的对象;
2.清除被标记的对象。
缺点:
1.效率问题,标记和清除的效率都不高;
2.空间问题,会产生大量不连续的内存碎片。
- 复制算法(
Copying
)
流程:
1.将可用内存划分为大小相等的两块,每次只使用其中的一块;
2.当其中一块内存用完后,就将还活着的对象复制到另外一块上;
3.把刚才的那块内存清空。
优点:
1.不会产生内存碎片;
2.对象存活率较低时需要的复制操作少,效率高。
缺点:
1.可用内存只有一半;
2.对象存活率较高时需要的复制操作多,效率低。
说明:
1.新生代回收一般用这种算法。
2.因为新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的
比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块
较小的Survivor空间,每次使用Eden和其中一块Survivor。
3.当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外
一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
4.HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1
- 标记 - 整理算法(
Mark-Compact
)
流程:
1.先标记出所有需要回收的对象;
2.所有存活的对象都向一端移动;
3.清掉剩下的内存。
优点:
1.不会产生内存碎片;
2.不需要像复制算法那样浪费一半的空间。
缺点:
1.整理阶段的效率比较低。
说明:
1.老年代回收一般用这种算法。
- 分代收集算法
1.当前商业虚拟机的垃圾收集都采用分代收集算法。
2.这种算法没什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
3.一般把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的算法。
4.新生代中,每次回收只有少量对象存活,采用复制算法,效率高。
5.老年代中,对象存活率高、没有额外空间对它进行分配担保,
就要用标记—清除或者标记—整理算法来进行回收。
垃圾收集器
执行引擎
简介
物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统上的,而虚拟机的执行引擎则是由自己实现的,所以可以自行制定指令集与执行引擎的结构体系。
栈帧
栈帧存储了方法的局部变量、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用到执行完成的过程,都对应者一个栈帧在虚拟机栈中从入栈到出栈的过程。每个栈帧需要分配多少内存,代码编译后就已经确定,不受运行期的影响。
结构如下:
说明:
- 局部变量表:存储方法参数和内部定义的局部变量,单位为:Slot,最大容量由Code属性中的
locals
可知; - 操作数栈:在方法执行的过程中,会有各种字节码指令往栈中写入和读取数据,栈最大深度由Code属性中的
stack
可知;
执行过程
开头处的源码 inc:
public int inc() {
return a + 1;
}
编译后对应内容:
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/mncode/jvm/bytecode/Test1;
}
说明:
- 操作数栈最大深度
2
,局部变量表中变量数量为1
; aload_0
:将局部变量表第0个slot中的引用类型变量复制到栈顶;getfield
:获取指定类的实例域,并将其值压入栈顶;iconst_1
: 将int型1推送至栈顶;iadd
: 将栈顶两个数相加,并将结果推送至栈顶;ireturn
:方法执行结束,并将栈顶结果返回;
测试代码:
Test1 obj = new Test1();
obj.inc();
inc方法执行流程如下(内部各个组件的变化情况):