在讲类加载前,需要先了解一下方法区、堆和直接内存三块内存区域的运行模式
JVM中的方法去是所有线程中共享的一块区域
它存储了跟类相关的信息
方法区 会在虚拟机被启动时创建。它逻辑上是堆的组成部分
它在不同的jvm厂商中存在的位置可能会不同,有些会放在堆区中,有些会放在本地存储中
如果方法区在申请内存空间不足时,也会抛出:内存溢出问题
由于两者框架底层生产的类都用的时cglib(动态代理),cglib会创建多个类来实现,所以内存被就会频繁占用,在1.8以前溢出场景非常多(永久代空间),在1.8以后由于使用的时本地存储,类文件都存储在元空间里,所以不那么容易溢出了
给指令集提供常量符号,通过常量符号进行查表,查到后就可以执行命令了
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)变为真实地址
/** 反编译后的执行顺序
* 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
}
}
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.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)
当串池中存在过多字符串,会触发一次或多次的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已经被调用了,它将一部分无用的对象进行了清理。
根据 -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");
}
考虑下,字符串对象是否能够入池来节省运行速度?
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();
}
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();
}
直接内存并不是值 JVM的内存,而是系统内存。
Direct Memory
这里分别测试 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
使用直接内存进行读写,快了几乎两倍,至少读写效率是高了很多。
这是读写的底层原理。
使用直接内存后,会调用ByteBuffer.allocateDirect(_1Mb);
方法,来在系统内存和Java堆内存之间创建一块指定内存大小的缓冲区, 这样子Java代码就可以直接访问,系统也可以直接使用。
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:不论是堆内存还是直接内存,其实都会有内存溢出的风险。
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 多次出现。
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来自动回收释放),但对于其他类不会有太大的影响
一个简单的demo1.java
package Class;
public class demo1 {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
执行 javac -parameters -d . demo1.java
编译后进制文件是这个样子的
根据 JVM 规范,类文件结构如下:
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0~3字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
4~7字节,表示类的版本 00 34(52)表示是java 8
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信息、附加属性和方法信息
两组字节码指令
public cn.itcast.jvm.t5.HelloWorld();
2a b7 00 01 b1
public static void main(java.lang.Styring[]);
b2 00 02 12 03 b6 00 04 b1
自己分析类文件结构太麻烦了,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"
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);
}
}
当一个变量被创建出来后,基本数据类型会被放入栈中,而当一个类型的最大值突破了,那么该变量会被放进运行时常量池中
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"
(stack=2,locals=4)
当栈的深度为2时,那么就会分配为一个深度为2的栈(蓝色) 当帧为4时,那么会分配为一个长度为4的阵(绿色)
…
到目前为止,
istore:可以看作是把对象放入局部变量中 iload:可以看作是把局部变量放入操作数栈中
invokevirtual #5
最后 return
/**
* 从字节码角度分析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"
分析:
指令 | 助记符 | 含义 |
---|---|---|
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 |
几点说明:
当执行invokevirtual指令时
将类的字节码载入方法区,内部采用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来操控对象了
解析,将常量池中的符号引用解析为直接引用
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{
}
初始化,即调用<cinit>()V,虚拟机会保证这个类的【构造方法】的线程安全
概括的说,类初始化是 【懒惰的】
而不会导致类初始化的情况:
实验:
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");
}
}
从字节码分析,使用 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;
}
public final class Singleton{
private Sinleton(){}
// 内部类中保留单例
private static class LazyHolder{
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance方法,才会导致内部类加载和初始化其静态成员
public static Sinleton getInstance(){
return LazyHolder.INSTANCE;
}
}
以JDK8为例:
名称 | 加载哪里的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载 | 自定义 | 上级为Application |
这几种类加载器都会管理不同包下的类
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
/a:.
表示将当前目录追加至 bootcalsspath 之后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
开始》结束顺序,加载器的运行
指:调用类加载器的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;
}
}
我们在使用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 下搜索类
什么时候需要自定义类加载器
步骤:
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);
}
}
}