=========================================================================
对每个加载的类型(类class、接口、枚举、注解)
,jvm必须在方法区存储以下类型信息
-
类型的完整
有效
名称(全名=包名.类名
) -
类型直接父类的完整有效名称(接口和java.lang.Object,没有父类)
-
类型的修饰符(public,abstract,final的某个子集)
-
类型直接接口的一个
有序列表
===============================================================================
-
保存类型的所有域的
相关信息
以及域的声明顺序
-
域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient)
=================================================================================
这里是引用jvm保存所有方法的以下信息,同域信息一样的声明顺序
- 方法名称
- 方法返回参数
- 方法参数的数量和类型
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)
- 方法的字节码,操作数栈,局部变量表及大小(abstract和native除外)
- 异常表(abstract和native除外),每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类常量池索引
这里是引用 上:jdk1.6结构 下:jdk1.8结构
在1.6中方法区的实现是永久代(PermGen),永久代中包括Class,ClassLoader以及StringTable(运行时常量池)
在1.8之后永久代的实现被废弃,方法区脱离了JVM交给本地内存管理(操作系统内存),实现变成了元空间,元空间
包括Class,ClassLoader,StringTable(运行时常量池)被移动到了堆空间中
方法区的内存溢出问题
-
1.8以前会导致
永久代内存溢出
-
1.8之后会导致
元空间内存溢出
我们通过一个实例来演示方法区内存溢出的情况
为了更好的演示,我们需要调整虚拟机参数
-XX:MaxMetaspaceSize=8m
/*演示元空间内存溢出
*/
public class Demo02 extends ClassLoader {//ClassLoader能动态加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo02 test = new Demo02();
for (int i = 0; i < 10000; i++) {
//ClassWriter:生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,public,类名,包名,父类,实现的接口
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,“Class”+i,null,“java.lang/Object”,null);
//生成一个类,并返回该类的字节码数组
byte[] code = cw.toByteArray();
//执行了类的加载
test.defineClass(“Class”+i,code,0,code.length);//Class对象
}
}finally {
System.out.println();
}
}
}
由于类加载过多,导致了方法区内存溢出
3002
Exception in thread “main” java.lang.OutOfMemoryError: Compressed class space // 元空间内存溢出
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.wql.jvm.MethodArea.Demo02.main(Demo02.java:23)
运行时常量池
-
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的
类名,方法名,参数类型,字面量等信息
-
运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
我们通过反编译来查看类的信息
首先找到类的.class文件
.class文件在out目录下
输入指令
javap -v Demo01.class
类的信息:
常量池信息:
类的方法定义,第一个是构造方法,第二个是main方法
我们以getstatic为例说明虚拟机指令的执行流程
执行过程中需要对#2,#3,#4进行查常量池表翻译,比如#2,在常量池表中对应#21,#22
所以继续找表中的#21,#22
#21,#22在表中又对应#28,#29,#30
#28代表静态变量所在的类为java/lang/System
#29代表要找到System类中名叫out的变量
#30代表它的类型是java/io/PrintStream
所以getstatic指令代表找到在java/lang/System类下的名叫out的成员变量,变量类型为java/io/PrintStream
===============================================================================
StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是StringBuilder(1.8)
-
字符串常量拼接的原理是编译器优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
-
1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
-
1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池,会把串池中的对象返回
注意:无论是串池还是堆里面的字符串,都是对象
串池作用:用来放字符串对象且里面的元素不重复
=============================================================================
public class Demo03 {
public static void main(String[] args) {
String s1 = “a”;
String s2 = “b”;
String s3 = “ab”;
}
}
编译之后我们查看以上代码常量池的结果
我们可以看到常量值中最中存放的是[“a”,“b”,“ab”]
常量池最初存在于字节码文件中,运行的时候被加载到运行时常量池中,也就是Constant pool,此时池中的信息还没有成为字符串对象,还仅仅是符号,直到执行到ldc指令,比如ldc #2,通过#2在池中找到a符号,才会将a符号变为字符串对象,"a"作为key去StringTable串池(长度固定,不能扩容)中进行寻找,如果没有取值相同的key,则"a"加入串池
我们需要注意,字符串对象的创建是懒惰的,只有运行到String s1 = "a"并且串池中不存在"a"的时候的时候,该字符串对象才会被真正的创建并添加进串池
==============================================================================
案例1:使用字符串变量创建字符串
public class Demo04 {
public static void main(String[] args) {
String s1 = “a”;
String s2 = “b”;
String s3 = “ab”;
String s4 = s1 + s2;//new StringBuilder().append(s1).append(s2).toString()
}
}
我们编译后查看常量池,观察新生成的s4是如何工作的
我们可以看到StringBuilder对象被创建,说明s4是通过StringBuilder对象拼接,然后调用toString()方法创建了一个新的字符串对象,该对象在堆中,所以
System.out.println(s3==s4);false
因为这已经是两个不同的对象了
案例2:使用字符串常量创建字符串
public class Demo05 {
String s1 = “a”;
String s2 = “b”;
String s3 = “ab”;
String s4 = “a”+“b”;
}
我们编译之后查看常量池,观察s4是如何生成的
我们可以看到,ldc #6,到常量池中寻找“ab”,而"ab"在常量池中已经存在,所以直接返回
但是为什么"a"+"b"变成了"ab"呢?这是因为
在拼接字符串常量时,javac在编译期间的优化,他认为"a"和"b"都是常量,拼接之后的值是固定的,所以直接确定编译结果,如果串池中已经有拼接好的值,则不用创建直接取
在拼接字符串变量时,因为内容不确定,所以编译期间会创建StringBuilder对象进行内容拼接,调用toString()方法又产生了新的字符串对象
======================================================================================
将字符串对象尝试放入串池(StringTable)中,如果有则不放入
public class Demo06 {
public static void main(String[] args) {
String s1 = new String(“a”)+new String(“b”);
String s2 = s1.intern();
}
}
以上面代码为例,new String(“a”)创建字符串"a"对象,并在串池中添加"a",new String(“b”)创建字符串"b"对象,并在串池中添加"b",s1为"ab"字符串对象,但是不存在于串池中,调用intern()方法之后将s1对象放入串池并将值"ab"返回
那么如果串池中提前存在了"ab"呢
public static void main(String[] args) {
String x = “ab”;
String s1 = new String(“a”)+new String(“b”);
String s2 = s1.intern();
System.out.println(s2==x);
System.out.println(s1==x);
}
因为串池中已经有"ab",所以s1.intern();并不会把s1再放入串池,但是返回的s2是串池中的对象,
所以答案是true,false
注意:
串池在1.6之前存放的是字符串对象的引用,在1.8之后,因为位置移动到了Heap中,所以存放的是对象本身和字面量
======================================================================================
jdk1.6和1.8不同的是,如果调用intern()方法且串池中没有此对象,则会把此对象复制一份,放入串池,将串池中的对象返回
======================================================================
public class Demo02 {
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);
System.out.println(s3==s5);
System.out.println(s3==s6);
String x2 = new String(“c”)+new String(“d”);
String x1 = “cd”;
x2.intern();
//问,如果调换了最后两行代码的位置呢,如果是jdk1.6呢
System.out.println(x1==x2);
}
}
false
true
true
false
//如果调换后两行代码的位置
true
//如果是jdk1.6
false
=================================================================================
从图片中我们可以看出
-
1.6及以前,串池(StringTable)存在于方法区
-
1.8及以后,串池(StringTable)存在于堆中
===================================================================================
我们通过一个案例来观测StringTable的垃圾回收现象
先设置虚拟机参数
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public class Demo07 {
public static void main(String[] args) {
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);
}
}
}
======================================================================
直接内存不属于java虚拟机的内存管理,而是属于系统内存
Direct Memory
-
常见于NIO操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受JVM内存回收管理
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/b12c620fa119df589a4f9da4f27fa56d.jpeg)
知其然不知其所以然,大厂常问面试技术如何复习?
1、热门面试题及答案大全
面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer
2、多线程、高并发、缓存入门到实战项目pdf书籍
3、文中提到面试题答案整理
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
常问面试技术如何复习?
1、热门面试题及答案大全
面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer
[外链图片转存中…(img-v9gPM1hN-1713529024198)]
2、多线程、高并发、缓存入门到实战项目pdf书籍
[外链图片转存中…(img-qRGwo9yG-1713529024200)]
[外链图片转存中…(img-6ezhBgS2-1713529024202)]
[外链图片转存中…(img-lIR4yYBl-1713529024204)]
3、文中提到面试题答案整理
[外链图片转存中…(img-2Ve63msu-1713529024205)]
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
[外链图片转存中…(img-ZUhA7Tsc-1713529024207)]
[外链图片转存中…(img-XugO4pSe-1713529024208)]
[外链图片转存中…(img-x21y9txI-1713529024210)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!