在讲类加载前,需要先了解一下方法区、堆和直接内存三块内存区域的运行模式
1. 方法区
JVM中的方法去是所有线程中共享的一块区域
它存储了跟类相关的信息
方法区 会在虚拟机被启动时创建。它逻辑上是堆的组成部分
它在不同的jvm厂商中存在的位置可能会不同,有些会放在堆区中,有些会放在本地存储中
如果方法区在申请内存空间不足时,也会抛出:内存溢出问题
1.1 溢出场景:
- 常用:spring、mybiats
由于两者框架底层生产的类都用的时cglib(动态代理),cglib会创建多个类来实现,所以内存被就会频繁占用,在1.8以前溢出场景非常多(永久代空间),在1.8以后由于使用的时本地存储,类文件都存储在元空间里,所以不那么容易溢出了
1.2 运行时常量池
给指令集提供常量符号,通过常量符号进行查表,查到后就可以执行命令了
Classfile /E:/Java/学习案例/2024.5.8 总复习/第一天:JAVA基本操作/basic/target/test-classes/two/JVM/test.class
Last modified 2024年9月2日; size 531 bytes
MD5 checksum 2aeac6e727155f8d343cdb28e189275d
Compiled from "test.java"
public class two.JVM.test
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // two/JVM/test
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
常量池:------------------------------------------------------
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello,world
#14 = Utf8 Hello,world
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // two/JVM/test
#22 = Utf8 two/JVM/test
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Ltwo/JVM/test;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 test.java
常量池:------------------------------------------------------
{
public two.JVM.test();
descriptor: ()V
flags: (0x0001) 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 Ltwo/JVM/test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
主要看以下代码:#7 代表这一行代码需要去常量池中找到的地址
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello,world
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "test.java"
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池
,常量池实 *.class 文件中的,当该类被加载,它的常量池就会放入运行时常量池(内存),并把里面的符号地址(类似于 # 1、#2)变为真实地址
1.1 小题目
/** 反编译后的执行顺序
* 0: ldc #7 // String a
* 2: astore_1
* 3: ldc #9 // String b
* 5: astore_2
* 6: ldc #11 // String ab
*
* 8: astore_3
* 9: aload_1
* 10: aload_2
* 11: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
* 16: astore 4
* 18: return
*/
// stringTable[] 这是一个hashtable结构,不能扩容,当存在同一种数值就不允许重复了
public class Pool {
// 常量池中的信息,都会加载到运行时常量池中,不过都还是常量池中的符号,只有在使用时才会把符号变为对象
// 例如: ldc #2 这种就调用了这个符号,那么对应符号代表的信息就会被转为对象
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
// 如上直接声明的字符串对象是会被存入stringTable中的。
String s4 = s1+s2;// 会存储在堆中但不存在于stringTable里
// 这种有字符串变量拼接的对象会调用makeConcatWithConstants方法,在java8版本会使用stringBuilder()进行.append()拼接。
/** makeConcatWithConstants方法说明
* 代码生成器有三种不同的方式来处理字符串连接表达式中的常量字符串操作数S。
* 首先,S可以具体化为引用(使用ldc),并作为普通参数(recipe '\1')传递。
* 或者,S可以存储在常量池中并作为常量(配方'\2')传递。
* 最后,如果S不包含配方标签字符('\1','\2'),则可以将S插入到配方本身中,从而将其字符插入到结果中。
*/
String s5 = "a"+"b";// 底层指令会直接在常量池中寻找对应的符号,如果有相符的会直接使用对应地址创建一个新的对象
/**
* 6: ldc #11 // String ab
* 8: astore_3
* 9: aload_1
* 10: aload_2
* 11: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
* 16: astore 4
* 18: ldc #11 // String ab
*/
// System.out.println(s3==s4);// 两者存储地址不一样一个是内存,一个是存储中,所以为false
System.out.println(s3==s5);// 因为两个对象本质都存储使用的同一个stringTable里的符号地址,所以为true
}
}
1.3 StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才会转为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接原理在1.8是StringBuilder,后面改为了makeConcatWithConstants
- 字符串常量拼接的原理是编译器优化
- 空要使用intern方法,主动讲串池中还没有转为对象的字符串放入串池
- 1.8 及以后,将某个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.8 以前(例如1.6),将这个对象尝试放串池,如果存在就不会放入,如果没有就会把此对象复制一份,将复制出来的对象放入串池,然后将串池中的对象返回
public class Pool2 {
public static void main(String[] args) {
String x = "ab";
/**
* 1. new String 在stringTable中添加了 "a" "b"
* 2. 通过拼接后转为了 s String对象(堆中)
*/
String s = new String("a")+new String("b");// 经过了拼接后,s还处于堆中
String s2 = s.intern();// intern 方法,将在堆中的对象尝试放入串池,如果串池中有则把串池中的对象返回
// 经过转换 s2 = 若s存在于串池中,那么就等于串池中的字符串,如果没有则会将s放入串池中
/**
* intern():如果s字符串对象已经存在于串池中,那么会返回串池中的字符串,而s字符串对象不变动
* 如果s字符串对象不存在于串池,那么就会将s字符串对象放进串池中,并返回这个字符串
*/
System.out.println(s=="ab");
System.out.println(s2=="ab");
}
}
/**
面试题
*/
public class pool3 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1+s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3==s4);// false
System.out.println(s3==s5);// true
System.out.println(s3==s6);// true
String x = new String("c")+new String("d");
String x2 = "cd";
String x3 = x.intern();
System.out.println(x==x2);// false
System.out.println(x2==x3);// true
// 如果 17行 和 18行 调换一下位置会输出什么?输出true、true。
}
}
1.4 StringTable 位置
在1.8以前,StringTable的位置都放置在永久代中,而1.8大改后就放到了Heap堆中。
1.8 以前,使用 -XX MaxPermSize=10m 设置VM的参数
1.8 使用 -Xmx10m 设置VM参数
import java.util.ArrayList;
/***
* StringTable 位置
* 测试永久代的或堆的内存溢出
*/
public class StringTablePosition {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
int j = 0;
try
{
for (int i = 0; i < 260000; i++) {
list.add(String.valueOf(i).intern());
j++;
}
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(j);
}
}
}
报错结果:
java.lang.OutOfMemoryError: Java heap space at java.lang.Integer.toString(Integer.java:401) at java.lang.String.valueOf(String.java:3099) at StringTablePosition.main(StringTablePosition.java:13)
1.5 StringTable 垃圾回收
当串池中存在过多字符串,会触发一次或多次的GC来清除
- 代码:
int i = 0;
try {
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
可以看到,这里什么都没有做,Java已经创建了1854个对象。这是因为Java的运行需要创建这么多对象,而往里面循环创建字符串加入串池那这个数会变成多少呢?
- 代码:
int i = 0;
try {
for (int j=0;j<10000;j++){
String.valueOf(j).intern();
i++;
}
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
当讲10000个字符串放进串池中时,实际上存在的对象并没有这么多。
那是因为GC已经清除了一次对象
这里就说明了GC已经被调用了,它将一部分无用的对象进行了清理。
1.6 性能调优
1.6.1 调整StringTable的桶个数
根据 -XX:StringTableSize=桶个数 这个参数,就可以设置StringTable的容量,越大的话,迭代就越快,运行速度也就随之变快。当然,它的范围是在1009~2305843009213693951之间。
若是小于或超出这个数值会报错:非法数值
/**
* StringTable性能调优
* -XX:StringTableSize=2000 -XX:+PrintStringTableStatistics
*/
try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("../demo.txt"), "utf-8"))){
String line = null;
long start = System.nanoTime();
while (true){
line = reader.readLine();
if (line == null){
break;
}
line.intern();
}
System.out.println("cost:"+(System.nanoTime()-start)/1000000+"ms");
}
1.6.2 字符串对象是否入池
考虑下,字符串对象是否能够入池来节省运行速度?
1.6.2.1 不入池
public void AdjustOptimize2() throws Exception {
ArrayList<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("../demo.txt"), "utf-8"))){
String line = null;
long start = System.nanoTime();
while (true){
line = reader.readLine();
if (line == null){
break;
}
// 不放入StringTable放进直接堆中
address.add(line);
System.out.println("cost:"+(System.nanoTime()-start)/1000000+"ms");
}
}
System.in.read();
}
1.6.2.2 入池
public void AdjustOptimize2() throws Exception {
ArrayList<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("E:/Java/学习案例/JVM/JVM/src/main/resources/demo.txt"), "utf-8"))){
String line = null;
long start = System.nanoTime();
while (true){
line = reader.readLine();
if (line == null){
break;
}
// 加进StringTable池
address.add(line.intern());
}
System.out.println("cost:"+(System.nanoTime()-start)/1000000+"ms");
}
}
System.in.read();
}
2. 直接内存
直接内存并不是值 JVM的内存,而是系统内存。
2.2 定义
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成功较高,但读写性能高
- 不受JVM GC管理
这里分别测试 IO和使用直接内存的ByteBuffer 复制文件的用时时间
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 直接内存
* ByteBuffer
*/
public class DreictMemory {
static final String FROM = "E:\\Java\\学习案例\\JVM\\JVM\\src\\main\\resources\\b.mp4";
static final String TO = "E:\\Java\\学习案例\\JVM\\JVM\\src\\main\\resources\\out\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
其结果:
io 用时:37.349
directBuffer 用时:19.8012
使用直接内存进行读写,快了几乎两倍,至少读写效率是高了很多。
2.3 原理
这是读写的底层原理。
Java并不具备与底层直接交互,需要依赖C才能访问CPU中的内核态完成读写。
因而在系统内存之外还创建有一个Java堆内存。
数据读取首先要通过系统内存的系统缓存区,才能进入Java堆内存。
只有数据存在于Java堆内存的Java缓冲区时,java才可以读写
使用直接内存后,会调用ByteBuffer.allocateDirect(_1Mb);
方法,来在系统内存和Java堆内存之间创建一块指定内存大小的缓冲区, 这样子Java代码就可以直接访问,系统也可以直接使用。
2.4 内存溢出
static final int _100Mb = 1024 * 1024 * 100;
/**
* 内存溢出
*/
@Test
public void test1(){
ArrayList<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer allocate = ByteBuffer.allocateDirect(_100Mb);
list.add(allocate);
i++;
}
}finally {
System.out.println(i);
}
}
tips:不论是堆内存还是直接内存,其实都会有内存溢出的风险。
2.5 分配与释放
static final int _1Gb = 1024 * 1024 * 1000;
/**
* 分配与释放
*/
@Test
public void test2() throws IOException {
ByteBuffer allocate = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕..");
System.in.read();
System.out.println("开始释放");
allocate = null;
System.gc();
}
开始后,会创建一个系统内存
第一次回车进行后,会进行释放操作
第二次回车开始释放对象
为什么GC会将直接内存清除掉呢?不是不会清除的吗?
其实释放内存的并不是GC,而是JVM底层使用了unsafe手动将系统内存清除了
这就是直接内存分配的底层原理,都是使用的unsafe
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。
这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。
在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
该类还会在 JUC 多次出现。
2.5.1 小结
- 使用了Unsafe对象完成直接内存的分配调用,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
ByteBuffer allocate = ByteBuffer.allocateDirect(_2Gb);
↓↓↓↓
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
↓↓↓↓
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap, null);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 设置直接内存空间大小
base = UNSAFE.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
UNSAFE.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// Cleaner虚引用,
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
↓↓↓ 经过不同的方法,最后使用unsafe.freeMemory方法释放内存
public void run() {
if (address == 0) {
// Paranoia
return;
}
// freeMemory方法释放内存
UNSAFE.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
如果禁用显式回收(GC),那么其实对直接内存来说可能释放的不会那么的及时(可以使用unsafe手动回收,或者等到真正的GC来自动回收释放),但对于其他类不会有太大的影响
3. 🎉类文件结构
一个简单的demo1.java
package Class;
public class demo1 {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
执行 javac -parameters -d . demo1.java
编译后进制文件是这个样子的
根据 JVM 规范,类文件结构如下:
3.1 魔术
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0~3字节,表示它是否是【class】类型的文件
3.2 版本
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
4~7字节,表示类的版本 00 34(52)表示是java 8
3.3 常量池
ConstantType | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTENT_Methodref | 10 |
CONSTENT_InterfaceMethodref | 11 |
CONSTENT_String | 8 |
CONSTENT_Integer | 3 |
CONSTENT_Float | 4 |
CONSTENT_Long | 5 |
CONSTENT_Double | 6 |
CONSTENT_NameAndType | 12 |
CONSTENT_Utf8 | 1 |
CONSTENT_MethodHandle | 15 |
CONSTENT_MethodType | 16 |
CONSTENT_InvokeDynamic | 18 |
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
8~9字节,表示常量池长度,0023(35)表示#1 ~ #34项,注意#0项不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#1项 0a 表示一个Method信息,00 06 和 00 15(21)表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】
…
二进制文件中排列的数据是非常紧凑的
里面不仅仅包含了所属类和方法名,还有访问标识、继承信息、Field信息、附加属性和方法信息
3.字节码指令
两组字节码指令
- public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
- public static void main(java.lang.Styring[]); 主方法的字节码指令
public cn.itcast.jvm.t5.HelloWorld();
2a b7 00 01 b1
- 2a=> aload_0 加载 slot 0的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
- b7 => invokespecial 预备调用构造方法,哪个方法呢?
- 00 01 引用常量池中 #1 项,即【Method java/lang/Object.“”: ()v】
- b1 表示返回
public static void main(java.lang.Styring[]);
b2 00 02 12 03 b6 00 04 b1
- b2 => getstatic 用来加载静态变量,哪个静态变量呢?
- 00 02 引用常量池中 #2 项,即【Field java/lang/System.out;Ljava/io/PrintStream;】
- 12=>ldc加载参数,哪个参数呢?
- 03 引用常量池中#3项,即【String hello world】
- b6=>invokevirtual 预备调用成员方法,哪个方法呢?
- 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1 表示返回
3.1 javap 工具
自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译工具
Classfile /E:/Java/test.class
Last modified 2024年10月28日; size 411 bytes #// 最后更改日期
MD5 checksum 1f24a99621f4aba89f176c75a621d56e #// 加密哈希值
Compiled from "test.java" #// 编译来源
public class test #// 类名
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // test
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // 娴嬭瘯 》》 指的是“测试”字面量,字符码不是utf-8
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // test
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 娴嬭瘯
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 test
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
# // stack 栈的深度;locals 局部变量表的长度;args_size 参数的数量
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC # 作用区域
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 娴嬭瘯
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "test.java"
3.2 图解方法执行流程
3.2.1 原始java代码
package Class;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class demo2 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE+1;
int c = a+b;
System.out.println(c);
}
}
当一个变量被创建出来后,基本数据类型会被放入栈中,而当一个类型的最大值突破了,那么该变量会被放进运行时常量池中
3.2.2 编译后的字节码文件
Classfile /E:/Java/demo2.class
Last modified 2024年10月28日; size 434 bytes
MD5 checksum 949fd83375d8625cebadfbc1cf1e19b5
Compiled from "demo2.java"
public class Class.demo2
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // Class/demo2
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #7.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #20.#21 // java/io/PrintStream.println:(I)V
#6 = Class #22 // Class/demo2
#7 = Class #23 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 demo2.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = Utf8 java/lang/Short
#18 = Class #24 // java/lang/System
#19 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#20 = Class #27 // java/io/PrintStream
#21 = NameAndType #28:#29 // println:(I)V
#22 = Utf8 Class/demo2
#23 = Utf8 java/lang/Object
#24 = Utf8 java/lang/System
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (I)V
{
public Class.demo2();
descriptor: ()V
flags: (0x0001) 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 6: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 17
}
SourceFile: "demo2.java"
3.2.3 常量池载入运行时常量池
3.2.4 方法字节码载入方法区
3.2.5 main线程开始运行,分配栈帧内存
(stack=2,locals=4)
当栈的深度为2时,那么就会分配为一个深度为2的栈(蓝色)
当帧为4时,那么会分配为一个长度为4的阵(绿色)
3.2.6 执行引擎开始执行字节码
- bipush 10 将一个byte压入操作数栈(其长度会补齐4个字节),类似的指令还有
- sipush 将一个short压入操作数栈(其长度会补齐4个字节)
- ldc 将一个int压入操作数栈
- ldc2_w 将一个long 压入操作数栈(分两次压入,因为long是8个字节)
- 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入了常量池
- istore_1 将操作数栈顶数据弹出,存入局部变量表的 slot 1
ldc #3 从常量池加载 #3 数据到操作数栈
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE+1实际是在编译期间计算好的
- istore_2 从操作数栈栈顶弹出至局部变量
- iload_1 重新将1项压入操作数栈
- iload_2 重新将2项压入操作数栈
…
到目前为止,
istore:可以看作是把对象放入局部变量中
iload:可以看作是把局部变量放入操作数栈中
getstatic #4
在常量池中找到一个成员变量(该对象在堆中),获得到它的引用 。
然后会把它的引用地址放入操作数栈中
iload_3
将局部变量3项存放进操作数栈中
invokevirtual #5
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配locals、stack等)
- 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除main操作数栈内容
最后 return
- 完成main方法调用,弹出main栈帧
- 程序结束
3.3 分析 i++
/**
* 从字节码角度分析a++相关题目
*/
@Test
public void test1(){
int a = 10;
int b = a++ + ++ a + a--;
System.out.println(a);
System.out.println(b);
}
字节码:
Classfile /E:/Java/学习案例/JVM/JVM/src/test/java/Class/demo3.class
Last modified 2024年10月28日; size 423 bytes
MD5 checksum de9c94aef90c89bfe8a02cec9b7f6a9b
Compiled from "demo3.java"
public class Class.demo3
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // Class/demo3
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // Class/demo3
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 demo3.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 Class/demo3
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public Class.demo3();
descriptor: ()V
flags: (0x0001) 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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
}
SourceFile: "demo3.java"
分析:
- iinc 指令是直接在局部变量slot上进行运算
- a++和++a 的区别是先执行iload还是先iinc
- a++ 先 iload 再 iinc
- ++a 先iinc 再 iload
3.4 条件判断指令
指令 | 助记符 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否==0 |
0x9a | ifne | 判断是否!=0 |
0x9b | iflt | 判断是否<0 |
0x9c | ifge | 判断是否>=0 |
0x9d | ifgt | 判断是否>0 |
0x9e | ifle | 判断是否<=0 |
0x9f | if_icmpeq | 两罐int是否 == |
0xa0 | if_icmpne | 两个int是否!= |
0xa1 | if_icmplt | 两个int是否< |
0xa2 | if_icmpage | 两个int是否>= |
0xa3 | if_icmpgt | 两个int是否> |
0xa4 | if_icmple | 两个int是否<= |
0xa5 | if_acmpeq | 两个引用是否== |
0xa6 | if_acmpne | 两个引用是否!= |
0xc7 | ifnonnull | 判断是否!=null |
几点说明:
- byte、short、char都会按int比较,因为操作数栈都是4字节
- goto用来进行跳转到指定行号的字节码
3.5 小结:
当执行invokevirtual指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际class
- class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 差表得到方法的具体地址
- 执行方法的字节码
4. 类加载阶段
4.1 加载
将类的字节码载入方法区,内部采用C++的instanceKlass描述java类,它的重要field有:
列 | 含义 |
---|---|
_java_mirror | java的类镜像,例如对String来说就是String.class。作用:把klass暴露给java使用 |
_super | 父类 |
_fields | 成员变量 |
_methods | 方法 |
_constants | 常量池 |
_class_loader | 类加载器 |
_vtable | 虚方法表 |
_itable | 接口方法表 |
如果这个类还有父类没有加载,那么会优先加载父类
加载和链接可能时交替运行的
instanceKlass 这样的【元数据】是存储在方法区(1.8后的元空间内),但**_java_mirror是存储在堆中的**
_java_mirror地址中的会同步交换映射给在堆中的instanceKlass地址。而创建的类的地址也会存进Klass中,而Klass又会与_java_mirror同步映射。这样java就可以通过_java_mirror来操控对象了
4.2 链接
- 验证阶段:验证类是否符合JVM规范,安全性检查
- 准备:为staic变量分配空间,设置默认值
- static 变量在JDK7 之前存储于 instanceKlass末尾,从 JDK 7 开始,存储与_java_mirror末尾
- static分配空间喝赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量时final的,但属于引用类型,那么赋值也会在初始化阶段完成。
解析,将常量池中的符号引用解析为直接引用
public class demo1 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classLoader = demo1.class.getClassLoader();
Class<?> c = classLoader.loadClass("ClassLoad.C"); // LoadClass 它只是加载了C类,但是并没有触发解析C类中的方法,因此并不会加载D类
new C();// 而 new 会导致C的加载并且触发解析
System.in.read();
}
}
class C{
D d = new D();
}
class D{
}
4.3 初始化
4.3.1 <cinit>()V方法
初始化,即调用<cinit>()V,虚拟机会保证这个类的【构造方法】的线程安全
4.3.2 发生的时机
概括的说,类初始化是 【懒惰的】
- main方法所在的类,总是会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化会触发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
而不会导致类初始化的情况:
- 访问类的static final 静态常量(基本类型和字符串)不会触发初始化
- 类的对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
实验:
package ClassLoad;
public class demo2 {
static {
System.out.println("main init");// main方法所在的类,总是会被首先初始化
}
public static void main(String[] args) throws ClassNotFoundException {
// 访问类的static final 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 类的对象.class不会触发初始化
System.out.println(B.class);
// 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 不会初始化类B,但会加载 B、A
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("ClassLoad.B");
// 不会初始化类B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("ClassLoad.B",false,c2);
// 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 子类初始化,如果父类还没初始化会触发
System.out.println(B.c);
// 子类访问父类的静态变量,只会触发父类的初始化
Class.forName("ClassLoad.B");
}
}
class A{
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
4.3.3 练习1
从字节码分析,使用 a、b、c 这三个常量是否会导致E初始化
public class demo4{
public static void main(String[] args){
System.out.println(E.a);
System.out.println(E.b);
/**
E.a 和 E.b 都不会引起E类初始化,因为他们都是已经确定的值
而 E.c 使用的是Integer包装类,他在编译期中会有 Integer.valueOf(obj);这个操作
所以他会导致 E 类初始化
*/
System.out.println(E.c);
}
}
class E{
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}
4.3.4 练习2
public final class Singleton{
private Sinleton(){}
// 内部类中保留单例
private static class LazyHolder{
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance方法,才会导致内部类加载和初始化其静态成员
public static Sinleton getInstance(){
return LazyHolder.INSTANCE;
}
}
5. 类加载器
以JDK8为例:
名称 | 加载哪里的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载 | 自定义 | 上级为Application |
这几种类加载器都会管理不同包下的类
5.1 启动类加载器
public class demo3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("ClassLoad.F");
System.out.println(aClass.getClassLoader());
}
}
package ClassLoad;
public class F {
static {
System.out.println("bootstrap F init");
}
}
输出:
java -Xbootclasspath/a:. ClassLoad.demo3
bootstrap F init
null
- Xbootclasspth 表示设置 bootclasspath
- 其中
/a:.
表示将当前目录追加至 bootcalsspath 之后 - 可以用这个办法替换核心类
- java -Xbootclasspath:<new bootclasspath>
- java -Xbootclasspath/a: <追加路径>
- java -Xbootclasspath/p: <追加路径>
5.2 应用程序加载器和扩展器加载器
public class demo3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("ClassLoad.F");
System.out.println(aClass.getClassLoader());
}
}
package ClassLoad;
public class F {
static {
System.out.println("bootstrap F init");
}
}
输出:
bootstrap F init
jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
开始》结束顺序,加载器的运行
- 启动类加载器
- 如果启动类加载器中有这个类,那么使用应用程序加载器
- 如果应用程序加载器中存在这个类,那么使用扩展器加载器
5.3 双亲委派模式
指:调用类加载器的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) {
// 如果有上级,委派上级LoadClass (ExtClassLoader)
c = parent.loadClass(name, false);
} else {
// 没有上级(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 类没有找到,则从非空父类装入器
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍然没有找到,则反射findClass方法,找到这个类的加载器自己扩展
// 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
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
5.4 线程上下文类加载器
我们在使用JDBC时都需要加载Driver驱动,当我们不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver正确加载的
追踪一下源码看看:
public class DriverManager {
private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
static{
loadinitialDrivers();
}
}
Drivermanager的类加载器:
System.out.println(DriverManager.class.getClassLoader());
在jdk11中,它使用的加载器是:PlatformClassLoader 是一个平台类加载器
扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
整个JDK都基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,因为分成了更小颗粒,可以对 moudle 进行组合,而并非都是固定某个 jar,那自然无须再保留<JAVA_HOME>\lib\ext目录,此前使用这个目录或者java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了,用来加载这部分类库的扩展类加载器也完成了它的历史使命。
类似地,在新版的JDK中也取消了<JAVA_HOME>\jre目录,因为随时可以组合构建出程序运行所需的JRE来,譬如假设我们只使用java.base模块中的类型,那么随时可以通过以下命令打包出一个“JRE”:jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre
而在 jdk9以前,Drivermanager的类加载器还是 Bootstrap Classloader,会在JAVA_HOME/jre/lib 下搜索类
- jdk 8 时,拓展类加载器和用户类加载都是继承的 UrlClassLoader,jdk 11 之后,三个默认的类加载器都继承了 BuiltinClassLoader
- BuiltinClassLoader 和 UrlClassLoader 对比
- 原理差不多,都是基于 UrlClassPath 来实现查找的。
- 但 BuiltinClassLoader 支持从 moudle 加载 class。
- 还有和通常的双亲委派不同,如果一个 class 属于某个 moudle 那么会直接调用该 moudle 的类加载器去加载,而不是说直接用当前类加载器的双亲委派模型去加载。但是找到这个 class 对应的类加载器后,还是会按照双亲委派去加载。
5.5 自定义类加载器
什么时候需要自定义类加载器
- 加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写findClass方法
- 不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
package ClassLoad;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class LoadTest {
}
class MyClassLoader extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader cl = new MyClassLoader();
/**
* 在同一个类加载器中,加载的类并不会被重新加载(c2)
*/
Class<?> a1 = cl.loadClass("demo1");
Class<?> a2 = cl.loadClass("demo1");
System.out.println(a1 == a2);
/**
* 而在不同类加载器中,哪怕读取的是同一个类,他们由于类加载器的不同,内存的地址也是不同的。
*/
MyClassLoader cl2 = new MyClassLoader();
Class<?> a3 = cl2.loadClass("demo1");
System.out.println(a1==a3);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "E:\\Java\\学习案例\\JVM\\"+name+".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path),os);
byte[] bytes = os.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
// throw new RuntimeException(e);
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到:",e);
}
}
}