diff --git a/README.md b/README.md index c4b1630..cb388d8 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,5 @@ - [记循环依赖](/notes/记循环依赖.md) - [记System.identityHashCode(obj)与obj.hashCode()的关系](/notes/记System.identityHashCode(obj)与obj.hashCode()的关系.md) - [从字节码角度分析try、catch、finally运行细节](/notes/从字节码角度分析try、catch、finally运行细节.md) +- [Java常量池理解及总结](/notes/Java常量池理解及总结.md) +- [浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) diff --git "a/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" "b/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" new file mode 100644 index 0000000..454b0b0 Binary files /dev/null and "b/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" differ diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" index 02cbe11..986cd58 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" @@ -40,7 +40,7 @@ String变量创建后是放入方法区的常量池(或者常量池)中, - 方式二:String str2=new String("123"); -首先"123"是一个常量字符串,因此会先在常量池创建"123"字符串对象,然后在堆中再创建一个字符串对象,将"123"的字符数组复制到堆中新创建的对象字符数组中,因此该方式不仅会在堆中,还会在常量池中创建"123"字符串对象。 +首先"123"是一个常量字符串,因此会先在常量池创建"123"字符串对象,然后在堆中再创建一个字符串对象,将常量池中的"123"字符串复制到堆中新创建的对象字符数组中,因此该方式不仅会在堆中,还会在常量池中创建"123"字符串对象。 - 方式三:String str3="123".intern(); diff --git "a/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" "b/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" new file mode 100644 index 0000000..20d0402 --- /dev/null +++ "b/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" @@ -0,0 +1,292 @@ +# Java常量池理解及总结 + +## 1、相关概念 + +Java中的常量池,实际上分为两种形态:**静态常量池** 和 **运行时常量池** 。 + +### 1.1 静态常量池 + +在Class文件结构中,最头的4个字节用于存储魔数Magic Number(OxCAFEBABE),用于确定一个文件是否能被JVM接受;再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号;再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。这部分常量池称为静态常量池。与Java中语言习惯不同,常量池容量计数是从1而不是0开始的。 + +Class常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等,而符号引用则属于编译原理方面的概念。Class常量将在类加载后进入方法区的运行时常量池中存放(稍后讲解)。其中,符号引用主要包括下面几类常量: + +- 被模块导出或者开放的包(Package) +- 类和接口的全限定名(Fully Qualified Name) +- 字段的名称和描述符(Descriptor) +- 方法的名称和描述符 +- 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) +- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant) + +常量池中每一项常量都是一个表,共有17种不同类型的表。它们共有一个特定,表结构起始的第一位是个u1类型的标志位(tag,用于区分常量类型),代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如下表所示。 +| 类 型 | 标 志 | 描 述 | +| :-----| :----: | :----: | +| CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 表示方法类型 | +| CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +| CONSTANT_Module_info | 19 | 表示一个模块 | +| CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 | + +以简单的Java代码为例: + +```java +public static void main(String[] args) { + String str="zingbug";// +} +``` + +利用javap命令,生成字节码后,静态常量池如下,具体含义见上表。 + +```java +Constant pool: + #1 = Methodref #2.#3 // java/lang/Object."":()V + #2 = Class #4 // java/lang/Object + #3 = NameAndType #5:#6 // "":()V + #4 = Utf8 java/lang/Object + #5 = Utf8 + #6 = Utf8 ()V + #7 = String #8 // zingbug + #8 = Utf8 zingbug + #9 = Class #10 // Main + #10 = Utf8 Main + ... +``` + +顺便提一下,最常见的CONSTANT_Utf8_info型常量结构体如下表所示。 +| 类 型 | 名 称 | 数 量 | +| :----: :----: | :----- | +| u1 | tag | 1 | +| u2 | length | 1 | +| u1 | bytes | length | + +其中,length值说明了这个UTF-8编码的字符串长度是多少字节,它后面就紧跟着长度为length字节的连续数据,用于表示UTF-8编码的字符串。 +由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。 + +### 1.2 运行时常量池 + +一个类加载到JVM中后对应一个运行时常量池。运行时常量池,是指jvm虚拟机在类加载的解析阶段,将class常量池内的符号引用替换为直接引用(具体请参考另一篇笔记),,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。这其中涉及到类加载的解析阶段。 + +运行时常量池相对于CLass文件常量池的另外一个重要特征是**具备动态性**,Class文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。 + +Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 + +> String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。 + +我自己的理解是,**运行时常量池是Class文件常量池在运行时的表示。** + +我们都知道运行时常量池是放在方法区的,但需要注意的是,方法区本身就是一个逻辑性的区域。在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;但在JDK8及以后,取消了永久代,新增了元空间(Metaspace),方法区就"四分五裂了",不再是在单一的一个去区域内进行存储,其中,元空间只存储类和类加载器的元数据信息,符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这时候方法区只是对逻辑概念的表述罢了。 + +### 1.3 常量池的优势 + +常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 + +- **节省内存空间**:常量池中所有相同的字符串常量被合并,只占用一个空间。 +- **节省运行时间**:比较字符串时,==比equals()快。对于两个引用变量,只用==判断地址引用是否相等,也就可以判断实际值是否相等。 + +> 双等号==的含义: +> 基本数据类型之间应用双等号,比较的是他们的数值。 +> 复合数据类型(字符串、类)之间应用双等号,比较的是他们在内存中的存放地址。当然,内存地址相同,值也必定相同。 + +## 2、Java基本数据类型的包装类和缓存池 + +Java基本数量类型有8种,分别是byte、short、int、long、float、double、char和boolean。 + +### 2.1 6种基本数据类型的包装类实现了缓存池技术,即Byte、Short、Integer、Long、Character和Boolean + +这五种基本数据类型默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。以Integer为例: + +```java +Integer i1=100; +Integer i2=100; +System.out.println(i1==i2);//输出true +Integer i3=300; +Integer i4=300; +System.out.println(i3==i4);//输出false +``` + +对于"Integer i1=100;",Java在编译的时候会直接将代码封装成"Integer i1=Integer.valueOf(100);",从而使用常量池的缓存。进一步去看Integer.valueOf方法的实现源码。 + +```java +public static Integer valueOf(int i) { + if (i>= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +其中,IntegerCache.low为-128,IntegerCache.high默认为127,但也可以通过JVM的启动参数 -XX:AutoBoxCacheMax=size 修改。 + +更多比较场景: + +```java +Integer i1 = 100; +Integer i2 = 0; +Integer i3 = new Integer(100); +Integer i4 = new Integer(100); +Integer i5 = new Integer(0); + +System.out.println("i1=i3 " + (i1 == i3)); +System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); +System.out.println("i3=i4 " + (i3 == i4)); +System.out.println("i3=i4+i5 " + (i3 == i4 + i5)); +System.out.println("100=i4+i5 " + (100 == i4 + i5)); + +//输出 +i1=i3 false +i1=i2+i3 true +i3=i4 false +i3=i4+i5 true +100=i4+i5 true +``` + +对此,有如下解释: + +- Integer i3 = new Integer(100);这种情况下会创建新的对象,所以i1不等于i3; + +- 语句i1 == i2 + i3,因为+这个操作符不适用于Integer对象,首先i2和i3会自动拆箱操作,进行数值相加,即i1 == 100,然后Integer对象无法与数值进行直接比较,所以i1自动拆箱转为int值100,最终这条语句转为100 == 100进行数值比较,故输出true; + +- i3和i4分别是不同的新对象,地址不同,故而不相等。 + +- 语句i3 == i4 + i5,和100 == i4 + i5,同样是自动拆箱操作,最后为数值比较而已。 + +### 2.2 2种浮点数类型的包装类Float,Double并没有实现缓存池技术 + +```java +Double d1=1.8; +Double d2=1.8; +System.out.println(d1==d2);//输出false +``` + +## 3、String类和常量池 + +### 3.1 不同场景下的String比较 + +String与常量池的关系比较复杂多样,我们直接以实际代码为例。 + +```java +String s1 = "Hello"; +String s2 = "Hello"; +String s3 = "Hel" + "lo"; +String s4 = "Hel" + new String("lo"); +String s5 = new String("Hello"); +String s6 = s5.intern(); +String s7 = "H"; +String s8 = "ello"; +String s9 = s7 + s8; + +System.out.println(s1 == s2); // true +System.out.println(s1 == s3); // true +System.out.println(s1 == s4); // false +System.out.println(s1 == s9); // false +System.out.println(s4 == s5); // false +System.out.println(s1 == s6); // true +``` + +首先说明一点,在String中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。对上述场景一一分析: + +- s1 == s2 这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。 +- s1 == s3 这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello",所以s1 == s3成立。**只有使用引号包含文本的方式创建的String对象之间使用"+"连接产生的新对象才会被加入字符串池中。** +- s1 == s4 当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,不清楚s4被分配到哪去了,所以地址肯定不同。**对于所有包含new方式新建对象(包括null)的"+"连接表达式,它所产生的新对象都不会被加入字符串池中。** +- s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。jvm常量池,堆,栈内存分布如下: +![N2xxLd.jpg](https://s1.ax1x.com/2020/06/28/N2xxLd.jpg) +- s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。 +- s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。 + +### 3.2 静态变量赋值 + +```java +public class Main { + public static final String A = "ab"; // 常量A + public static final String B = "cd"; // 常量B + + public static void main(String[] args) { + String s = A + B; // 将两个常量用+连接对s进行初始化 + String t = "abcd"; + System.out.println(s == t);//true + } +} +``` + +此时A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd"; + +### 3.3 静态方法赋值 + +```java +public class Main { + public static final String A; // 常量A + public static final String B; // 常量B + + static { + A = "ab"; + B = "cd"; + } + + public static void main(String[] args) { + // 将两个常量用+连接对s进行初始化 + String s = A + B; + String t = "abcd"; + System.out.println(s == t);//false + } +} +``` + +此时A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 + +### 3.4 intern方法 + +运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 + +```java +public class Main { + public static void main(String[] args) { + String s1 = new String("Hello"); + String s2 = s1.intern(); + String s3 = "Hello"; + System.out.println(s1 == s2);//false + System.out.println(s3 == s2);//true + } +} +``` + +String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回指向该字符串的引用(CONSTAT_String_info),如果没有则添加自己的字符串进入常量池,也同时返回引用。 + +### 3.5 延伸 + +```java +String s1 = new String("Hello"); +``` + +上面这行代码中,一共创建了几个对象? + +"Hello"会首先在常量池中里创建一个字符常量,然后在new创建对象时,会将常量池中"Hello"的字符串复制到堆中新创建的对象字符数组中,并建立引用s1指向它。所以,这条语句共创建了2个对象,分别位于常量池和堆。 + +## 4、总结 + +- 运行时常量池中的常量,基本来源于各个Class文件中的常量池。也就是说,运行时常量池是Class文件常量池在运行时的表示。 +- 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则JVM不会自动添加常量到常量池。 + +## 5、参考资料 + +[《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) + +[浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) + +[深入浅出java常量池](https://www.cnblogs.com/syp172654682/p/8082625.html) + +[Java常量池理解与总结](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485452&idx=1&sn=64178cb2b4e2768b2feedbe0d0971ee1&chksm=fd2d7e4eca5af7587be00fccbec403117cdb7eb681c0542ff542335b1239049bd0216fc1bb1d&mpshare=1&scene=1&srcid=0623b0MIhBaU2sZEhvxEjoME&sharer_sharetime=1592875293438&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=242af3e89b1070825a226d604188d71bc5c856baa07c40c96c01389ddc232167ec2a8196658f02f540cce13a26664b46549909f0156708f4d7d26fc7c470faeb92a6c0d4f3ad7328a7cbe59a5c3a0cd6&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=62090523&lang=zh_CN&exportkey=AVRh0T9RxdKUe0T%2BS0Vu7Jk%3D&pass_ticket=mrrMmVKWvl4QF8i0mBVDO7Xre7kYXlm7qLoXUV%2FJeUsPvRILMjcMWMW1A%2BzBALMH) + +ZingBug,2020/6/28 +[EOF] diff --git "a/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" "b/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" index 3dfed29..ce946bb 100644 --- "a/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" +++ "b/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" @@ -209,12 +209,16 @@ Exception table: - finally块中避免再次抛出异常,否则整个包含try语句块的方法回抛出异常,并且会消化掉try、catch块中的异常。 -参考资料: +## 5、参考资料 + +[《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) -《Java核心技术系列:Java虚拟机规范(Java SE 8版)》 -《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 [Java字节码指令收集大全](https://www.cnblogs.com/longjee/p/8675771.html) + [try catch finally异常机制Java源码+JVM字节码(内含小彩蛋)](https://blog.csdn.net/whiteBearClimb/article/details/104131005) + [Java中关于try、catch、finally中的细节分析](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485213&idx=1&sn=982c5e92d2c15a48b59194258a40c2d2&chksm=fd2d715fca5af84976b8cd034fc52aa94e78c2750ba6bcb586f979438bf38baee283fee7de9b&mpshare=1&scene=1&srcid=0623mdjtJSGrvIRPkaj6HhNx&sharer_sharetime=1592875254832&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=c32b5c198f0599707981fa0619311426b3104054b7dd310f3346ed0d2159420a6119aaa288eb9df1d369808227d2bb1eec8f9cbe2510d831061991b4215d7bd786a293f6f9c1033009510d6df3fc2f7f&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=6209007b&lang=zh_CN&exportkey=AYaxCNjrIahDUSf8A0fYYT4%3D&pass_ticket=4yLw2AbquUCok67oAc4LWcLTUZY6fTDbcUcWHB4Mj69rD%2BHCBbKgabd4I%2BGIDkfk) ZingBug,2020/6/24 diff --git "a/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" "b/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" new file mode 100644 index 0000000..11c30ee --- /dev/null +++ "b/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" @@ -0,0 +1,194 @@ +# 浅谈JVM中的符号引用和直接引用 + +## 1、前言 + +在 JVM 类加载过程的解析阶段,Java虚拟机将常量池中的符号引用替换为直接引用。符号引用存在于 Class 常量池,在Class文件格式中多以 CONSTANT_Class_info 、 CONSTANT_Fieldref_info 、 CONSTANT_Methodref_info 等类型的常量出现,符号引用和直接引用的区别在于: + +- 符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只用使用时能无歧义地定位到目标即可,与当前虚拟机实现的内存布局无关。 +- 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在虚拟机的内存中存在。 + +此外,在JVM 中方法调用(分派、执行)过程中,有五条相关指令用于方法调用: + +- invokevirtual 指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这 也是 Java 语言中最常见的方法分配方式。 +- invokeinterface 指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。 +- invokespecial 指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。 +- invokestatic 指令:用于调用类静态方法( static 方法)。 +- invokedynamic 指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面四条调用指令的分派逻辑都固化在 Java 虚拟机内部,用户无法改变,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。 + +方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn (当返回值是 boolean、 byte、 char、 short和 int类型时使用)、 ireturn、 ireturn、ireturn 和 ireturn,另外还有一个 return 指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。 + +以上的概念介绍,结合一个实例来分析举证。看下面的Java代码: + +```java +public class Main { + public static void main(String[] args) { + Sub sub = new Sub(); + sub.a(); + } +} + +class Sub { + public void a() { + } +} +``` + +编译后使用javap工具,得到Class字节码,如下: + +```java +Constant pool: + #1 = Methodref #2.#3 // java/lang/Object."":()V + #2 = Class #4 // java/lang/Object + #3 = NameAndType #5:#6 // "":()V + #4 = Utf8 java/lang/Object + #5 = Utf8 + #6 = Utf8 ()V + #7 = Class #8 // Sub + #8 = Utf8 Sub + #9 = Methodref #7.#3 // Sub."":()V + #10 = Methodref #7.#11 // Sub.a:()V + #11 = NameAndType #12:#6 // a:()V + #12 = Utf8 a + #13 = Class #14 // Main + #14 = Utf8 Main + #15 = Utf8 Code + #16 = Utf8 LineNumberTable + #17 = Utf8 LocalVariableTable + #18 = Utf8 this + #19 = Utf8 LMain; + #20 = Utf8 main + #21 = Utf8 ([Ljava/lang/String;)V + #22 = Utf8 args + #23 = Utf8 [Ljava/lang/String; + #24 = Utf8 sub + #25 = Utf8 LSub; + #26 = Utf8 SourceFile + #27 = Utf8 Main.java +{ + public Main(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + LineNumberTable: + line 8: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this LMain; + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=2, args_size=1 + 0: new #7 // class Sub + 3: dup + 4: invokespecial #9 // Method Sub."":()V + 7: astore_1 + 8: aload_1 + 9: invokevirtual #10 // Method Sub.a:()V + 12: return + LineNumberTable: + line 10: 0 + line 11: 8 + line 12: 12 + LocalVariableTable: + Start Length Slot Name Signature + 0 13 0 args [Ljava/lang/String; + 8 5 1 sub LSub; +} +``` + +因为篇幅有限,上面的内容只保留了静态常量池和 Code 部分。 + +- Sub 实例初始化的指令如下,印证invokespecial指令。 + +```java +4: invokespecial #9 // Method Sub."":()V +``` + +- void 方法的返回指令如下,印证return指令。 + +```java +12: return +``` + +下面我们主要对 Sub.a() 方法的调用来进行说明。 + +## 2、符号引用 + +在 main 方法的字节码中,调用 Sub.a() 方法的指令如下: + +```java +9: invokevirtual #10 // Method Sub.a:()V +``` + +invokevirtual 指令就是调用实例方法的指令,后面的操作数 10 是 Class 文件中常量池的下标,表示用来指定要调用的目标方法。我们再来看常量池在这个位置上的内容: + +```java +#10 = Methodref #7.#11 // Sub.a:()V +``` + +这是一个 Methodref 类型的数据,我们再来看看虚拟机规范中对该类型的说明: + +```java +CONSTANT_Methodref_info { + u1 tag; + u2 class_index; + u2 name_and_type_index; +} +``` + +这实际上就是一种引用类型,tag 表示了常量池数据类型,这里固定是 10 (参照 Class 常量池项目类型表)。class_index 表示了类的索引,name_and_type_index 表示了名称与类型的索引,这两个也都是常量池的下标。在 javap 的输出中,已经将对应的关系打印了出来,我们可以直接的观察到它都引用了哪些类型: + +```java +#10 = Methodref #7.#11 // Sub.a:()V 类中方法的符号引用 +|--#7 = Class #8 // Sub 类或接口的符号引用 +| |--#8 = Utf8 Sub +|--#11 = NameAndType #12:#6 // a:()V 字段或者方法的部分符号引用 +| |--#12 = Utf8 a +| |--#6 = Utf8 ()V +``` + +这里我们将其表现为树的形式。可以看到,我们可以得到该方法所在的类,以及方法的名称和描述符。于是我们根据 invokevirtual 的操作数,找到了常量池中方法对应的 Methodref,进而找到了方法所在的类以及方法的名称和描述符,当然这些内容最终都是 UTF8 字符串形式。 + +实际上这就是一个符号引用的例子,符号引用可以理解为利用文字形式来描述引用关系,简单来说是一个包含足够信息的字符串,以供实际使用时可以找到相应的位置。比如说"java/io/PrintStream.println:(Ljava/lang/String;)V",这里面有类、方法名和方法参数等信息。 + +## 3、直接引用 + +在第一次运行时,发现指令还没有被解析,根据指令去把常量池中有关系的所有项找出来,得到以"UTF-8"编码描述的此方法所属的"类,方法名,描述符"的常量池项,这就是上文中提到的**符号引用**的字符串信息,类的加载解析过程会根据字符串的内容,到该类的方法表中搜索这个方法,得到放发的偏移量(指针),这个偏移量(指针)就是**直接引用** 。再将偏移量赋给常量池的#10 (根据指令,在常量池中找到的第一个项),最后再将 Code 中方法调用的 invokevirtual 指令修改为invokevirtual_quick,并把操作数修改成指向方法表的偏移量(指针), 并加上参数个数,类似于以下格式: + +```java +#10 = Methodref 偏移量(指针) // Sub.a:()V +9: invokevirtual_quick 偏移量(指针) // Method Sub.a:()V +``` + +运行一次之后,符号引用会被替换为直接引用,下次就不用重新搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。 + +上面提到的"invokevirtual_quick"是R大借用Sun JDK 1.0.2 虚拟机为例,提出解释直接引用过程的,详细说明请去看 RednaxelaFX 的回答,参考链接在后文。 + +## 4、总结 + +谈一下我自己的理解,Java代码中所有方法调用的目标方法在 Class 文件里面都是一个常量池的符号引用,在类加载的解析阶段,会将其中符号引用转换为直接引用,这个根据字符串搜素具体方法的过程有些类似于类加载器的运行机制。 + +此外,解析能够顺利进行的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译的那一刻就已经确定下来了,这类方法的调用就叫解析。 + +解析调用一定是一个静态的过程,在编译期间就已经完成,将涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。而另一种主要的方法调用形式,分派调用则复杂很多,它可能是静态的也可能是动态的,与继承、多态相关,有机会去学习理解一波,这里就先挖个坑,不再做多介绍了。 + +## 5、参考资料 + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) + +[《自己动手写Java虚拟机》](https://book.douban.com/subject/26802084/) + +[浅析 JVM 中的符号引用与直接引用](https://www.dazhuanlan.com/2019/10/18/5da94ef24e667/) + +[JVM里的符号引用如何存储? - RednaxelaFX的回答 - 知乎](https://www.zhihu.com/question/30300585/answer/51335493) + +[JVM方法调用(invokevirtual)](https://www.cnblogs.com/Jc-zhu/p/4482294.html) + +ZingBug,2020年6月28日 +[EOF] diff --git "a/notes/350円256円260円BitMap-BitSet.md" "b/notes/350円256円260円BitMap-BitSet.md" index 858eae7..5205b9c 100644 --- "a/notes/350円256円260円BitMap-BitSet.md" +++ "b/notes/350円256円260円BitMap-BitSet.md" @@ -1,5 +1,7 @@ # 2020届秋招面试题总结——网络篇 +## 前言 + 提到一个bitmap,肯定想到它是非常经典的海量数据处理工具,其本质是用bit数组的某一位来表示某一数据,从而一个bit数组可以表示海量数据。 在Java中,BitSet类实现了bitmap算法。BitSet是位操作的对象,值只有0或1即false和true,**内部维护了一个long数组,初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充,最终内部是由N个long来存储**,这些针对操作都是透明的。 @@ -8,6 +10,8 @@ 一个1G的空间,有 `8*1024*1024*1024=8.58*10^9bit`,也就是可以表示85亿个不同的数。 +## 源码实现 + 我们来看一下其内部实现,首先看一下**构造函数**。 ```java @@ -82,6 +86,8 @@ public boolean get(int bitIndex) { 整体意思就是判断bitIndex所对应的位数是否为1。 +## 应用场景 + 怎么去**运用**这个bitMap思想呢。 首先,在原理上(1L << bitIndex),这个联想到LeetCode上一个题,第268题,**Missing Number**,从0到n之间取出n个不同的数,找出漏掉的那个。 @@ -182,9 +188,10 @@ public class HugeBitset { ); ``` -参考文章: +## 参考文章 [Java的BitSet原理及应用](https://www.jianshu.com/p/4fbad3a6d253) + [无限下标超大型bitset的java实现,超越原生int 20亿下标的限制](https://blog.csdn.net/flyflyflyflyflyfly/article/details/82952529) 弥有,2019年8月5日 diff --git "a/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" "b/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" index 02e5c42..2cc754e 100644 --- "a/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" +++ "b/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" @@ -109,13 +109,13 @@ b hashcode: 1794106630 identityHashCode()方法保证了返回的值在对象的生命周期内不会改变,注意的是,identityHashCode不能严格保证唯一!这个标识符在理论上可以用于哈希和哈希表以外的其他用途。 -## 简单的结论 +## 总结 hashCode方法可以被重写,并返回重写后的值。 identityHashCode方法是返回对象的hash值,而不管对象是否重写了hashCode方法。 -参考资料: +## 参考资料 [System.identityHashCode(obj) 与 obj.hashcode()](https://www.jianshu.com/p/24fa4bdb9b9d) diff --git "a/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" "b/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" index 478a736..582386d 100644 --- "a/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" +++ "b/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" @@ -1,5 +1,7 @@ # 动态代理 +## 前言 + 先说普遍的知识点,动态代理是实现AOP的方法,分为两种,JDK原生和CGLIB方法,两者**主要区别**在于: - JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象,对于Cglib来说,目标对象有没有实现接口都可以代理,但不能代理final方法,因为不能继承。JDK和Cglib都是在运行期生成字节码, @@ -13,7 +15,7 @@ - JDK动态代理,类中内部方法间的调用,被调用的方法不会被代理。 - CGLIB动态代理,类中内部方法间的调用,被调用的方法会被代理。 -**下面进行具体细节讲解。** +**我们结合具体实例,来进行具体细节讲解。** 首先新建一个用于代理的接口(后续委托类和代理类的公共接口)。 @@ -40,7 +42,7 @@ public class UserServiceImpl implements UserService { } ``` -**先介绍动态代理。** +## 源码分析 创建代理类,需要实现InvocationHandler接口,并重写invoke方法。 @@ -469,7 +471,7 @@ private void init() { 至此,CGLIB动态代理原理就差不多搞清楚了,还是深究源码才能弄懂。 -参考链接: +## 参考链接 [jdk和cglib原理区别](https://blog.csdn.net/weixin_39158271/article/details/78074442) diff --git "a/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" "b/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" index c99ebd7..27e0a07 100644 --- "a/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" +++ "b/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" @@ -2,7 +2,7 @@ 本文将一步步介绍单例模式。 -## 一、懒汉模式 +## 1、懒汉模式 ```java public class Singleton { @@ -19,9 +19,9 @@ public class Singleton { 这是最简单的,缺点很明显,第一次加载类的时候会连带着创建Singleton实例,这样的结果与我们所期望的不同,因为创建实例的时候可能并不是我们需要这个实例的时候。同时如果这个Singleton实例的创建非常消耗系统资源,而应用始终都没有使用Singleton实例,那么创建Singleton消耗的系统资源就被白白浪费了。但是也有优点,这个是线程安全的。 -为了避免这种情况,我们通常使用**惰性加载**的机制,也就是在使用的时候才去创建。但简单的惰性加载会有线程安全问题,一般会加入锁来保证每次只有唯一线程获取实例。 +为了避免这种情况,我们通常使用**惰性加载** 的机制,也就是在使用的时候才去创建。但简单的惰性加载会有线程安全问题,一般会加入锁来保证每次只有唯一线程获取实例。 -## 二、饿汉模式 +## 2、饿汉模式 ```java public class Singleton { @@ -41,7 +41,7 @@ public class Singleton { 这种方法解决了多线程并发安全问题,但是它却很影响性能,每次调用getInstance()方法的时候都必须获得Singleton类的锁(静态方法,synchronized锁针对的是类)。而实际上,当单例实例被创建后,其后的请求没有必要再使用互斥机制了。为了解决这个问题,提出了double-checked locking的解决方案。 -## 三、双重检查锁 +## 3、双重检查锁 ```java public static Singleton getInstance() { @@ -66,7 +66,7 @@ public static Singleton getInstance() { 4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。 5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。 -## 四、内部类 +## 4、内部类 ```java public class Singleton { diff --git "a/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" "b/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" index 40c85cf..af6305c 100644 --- "a/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" +++ "b/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" @@ -1,6 +1,8 @@ -## 循环依赖 +# 循环依赖 -**什么是循环依赖。** +## 问题概述 + +### 什么是循环依赖 Bean A 依赖 B,Bean B 依赖 A这种情况下出现循环依赖。 @@ -10,7 +12,7 @@ Bean A → Bean B → Bean A Bean A → Bean B → Bean C → Bean D → Bean E → Bean A -**循环依赖会产生什么结果?** +### 循环依赖会产生什么结果 当Spring正在加载所有Bean时,Spring尝试以能正常创建Bean的顺序去创建Bean。 @@ -81,17 +83,15 @@ The dependencies of some of the beans in the application context form a cycle: └────┘ ``` - - -**解决方法。** +## 解决方法 处理这种问题有几种常见的方式。 -**1、重新设计** +### 1、重新设计 重新设计结构,消除循环依赖。 -**2、使用懒注解@Lazy** +### 2、使用懒注解@Lazy 最简单的消除循环依赖的方式是通过延迟加载。具体代码如下。 @@ -110,7 +110,7 @@ CircularDependencyB是被延迟加载了,它在注入CircularDependencyA中时 总结为:对于单实例bean,默认是在容器启动的时候创建对象。但当使用懒加载时,将对象的创建推迟到第一次获取的时候。 -**3、直接使用Autowired单独注入** +### 3、直接使用Autowired单独注入 直接使用@Autowired注入依赖,不要使用构造器的方式注入,如下。 @@ -130,7 +130,7 @@ public class CircularDependencyB { } ``` -**4、 使用Setter注入** +### 4、使用Setter注入 如下: @@ -158,10 +158,13 @@ public class CircularDependencyB { 另外,还有一些其他方法,比如使用@PostConstruct等等。 -**总结** +## 总结 最后,当遇到循环依赖时。首先考虑是否能够通过重新设计依赖来避免循环依赖。如果确实需要循环依赖,那么可以通过前文提到的方式来处理。优先建议使用setter注入来解决。 -参考链接:[Circular Dependencies in Spring](https://www.baeldung.com/circular-dependencies-in-spring) +## 参考链接 + +[Circular Dependencies in Spring](https://www.baeldung.com/circular-dependencies-in-spring) -ZingBug,2019/7/26. \ No newline at end of file +弥有,2019/7/26 +[EOF]

AltStyle によって変換されたページ (->オリジナル) /