0


【JVM】JVM02(方法区-串池-直接内存)

⭐️写在前面


  • 这里是温文艾尔の学习之路
  • 👍如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 👋博客主页🎉 温文艾尔の学习小屋
  • ⭐️更多文章👨‍🎓请关注温文艾尔主页📝
  • 🍅文章发布日期:2021.12.29
  • 👋java学习之路!
  • 欢迎各位🔎点赞👍评论收藏⭐️
  • 🎄新年快乐朋友们🎄
  • 👋jvm学习之路!
  • 🔎更多文章(以下redis文章均上CSDN热榜):
  • ⭐️【Redis二三事】一套超详细的Redis学习教程(步骤图片+实操)—第一集
  • ⭐️【Redis二三事】一套超详细的Redis学习教程(步骤图片+实操)—第二集
  • ⭐️【源码那些事】超详细的ArrayList底层源码+经典面试题
  • ⭐️HashMap底层红黑树原理(超详细图解)+手写红黑树代码
  • ⭐️HashMap底层源码解析上(超详细图解+面试题)
  • ⭐️HashMap底层源码解析下(超详细图解)

在这里插入图片描述

文章目录

⭐️1.方法区

在这里插入图片描述

⭐️1.1 方法区概述

  • 方法区(Method Area)与java堆一样,是各个线程共享的内存区域
  • 多个线程同时加载一个类时,只有一个线程能加载该类,其他线程只能等待该线程加载完毕,然后直接使用该类,即类只能加载一次
  • 方法区在JVM启动的时候被创建并且它的实际物理内存空间和java堆区一样都可以是不连续的,方法区在JVM关闭时释放这个区域的内存
  • 方法区的大小和堆空间一样,可以选择固定大小或者可扩展
  • 方法区的大小决定了系统可以保存多少个,因为方法区主要就是用来存储类信息的。如果系统定义了太多的类,将导致方法区溢出,虚拟机同样会抛出内存溢出错误- 在JDK7及以前,抛出java.lang.OutOfMemoryError:PermGen space错误- 在JDK8及以后,抛出java.lang.OutOfMemoryError:Metaspace错误以下情况可能会导致方法区溢出- 动态生成反射类过多- 第三方jar包过多

⭐️1.2方法区内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

⭐️1.2.1类型信息

对每个加载的类型

(类class、接口、枚举、注解)

,jvm必须在方法区存储以下类型信息

  1. 类型的完整有效名称(全名=包名.类名
  2. 类型直接父类的完整有效名称(接口和java.lang.Object,没有父类)
  3. 类型的修饰符(public,abstract,final的某个子集)
  4. 类型直接接口的一个有序列表

⭐️1.2.2域(Field)信息

  1. 保存类型的所有域的相关信息以及域的声明顺序
  2. 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient)

⭐️1.3.3方法(Method)信息

这里是引用jvm保存所有方法的以下信息,同域信息一样的声明顺序

  1. 方法名称
  2. 方法返回参数
  3. 方法参数的数量和类型
  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)
  5. 方法的字节码,操作数栈,局部变量表及大小(abstract和native除外)
  6. 异常表(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

在这里插入图片描述

/*演示元空间内存溢出
*
*/publicclassDemo02extendsClassLoader{//ClassLoader能动态加载类的二进制字节码publicstaticvoidmain(String[] args){int j =0;try{Demo02 test =newDemo02();for(int i =0; i <10000; i++){//ClassWriter:生成类的二进制字节码ClassWriter cw =newClassWriter(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();}}}
由于类加载过多,导致了方法区内存溢出
3002Exception in thread "main"java.lang.OutOfMemoryError:Compressedclass space // 元空间内存溢出
    at java.lang.ClassLoader.defineClass1(NativeMethod)
    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

⭐️2.StringTable串池

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象

  • 利用串池的机制,来避免重复创建字符串对象

  • 字符串变量拼接的原理是StringBuilder(1.8)

  • 字符串常量拼接的原理是编译器优化

  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池 - 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池,会把串池中的对象返回

注意:无论是串池还是堆里面的字符串,都是

对象

串池作用:

用来放字符串对象且里面的元素不重复

⭐️2.1 常量池与串池的关系

publicclassDemo03{publicstaticvoidmain(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"的时候的时候,该字符串对象才会被真正的创建并添加进串池

⭐️2.2串池中字符串变量的拼接

案例1:使用字符串变量创建字符串

publicclassDemo04{publicstaticvoidmain(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:使用字符串常量创建字符串

publicclassDemo05{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()方法又产生了新的字符串对象

⭐️2.3 intern()方法【jdk1.8】

将字符串对象尝试放入串池(StringTable)中,如果有则不放入
publicclassDemo06{publicstaticvoidmain(String[] args){String s1 =newString("a")+newString("b");String s2 = s1.intern();}}

以上面代码为例,new String(“a”)创建字符串"a"对象,并在串池中添加"a",new String(“b”)创建字符串"b"对象,并在串池中添加"b",s1为"ab"字符串对象,但是不存在于串池中,调用intern()方法之后将s1对象放入串池并将值"ab"返回

那么如果串池中提前存在了"ab"呢
publicstaticvoidmain(String[] args){String x ="ab";String s1 =newString("a")+newString("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中,所以存放的是对象本身和字面量

⭐️2.4 intern()方法【jdk1.6】

jdk1.6和1.8不同的是,如果调用intern()方法且串池中没有此对象,则会把此对象复制一份,放入串池,将串池中的对象返回

⭐️2.5面试题

publicclassDemo02{publicstaticvoidmain(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 =newString("c")+newString("d");String x1 ="cd";
        x2.intern();//问,如果调换了最后两行代码的位置呢,如果是jdk1.6呢System.out.println(x1==x2);}}
falsetruetruefalse//如果调换后两行代码的位置true//如果是jdk1.6false

⭐️2.6 StringTable位置

在这里插入图片描述

从图片中我们可以看出

-1.6及以前,串池(StringTable)存在于方法区
-1.8及以后,串池(StringTable)存在于堆中

⭐️2.7 StringTable垃圾回收

我们通过一个案例来观测StringTable的垃圾回收现象

先设置虚拟机参数

-Xmx10m-XX:+PrintStringTableStatistics-XX:+PrintGCDetails-verbose:gc
publicclassDemo07{publicstaticvoidmain(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);}}}

在这里插入图片描述

⭐️3.直接内存

直接内存不属于java虚拟机的内存管理,而是属于

系统内存

Direct Memory

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
文件的读写过程图解

在这里插入图片描述

使用DirectBuffer之后获得的效果

在操作系统方面划出缓冲区,这块区域操作系统可以直接访问,系统也可以直接访问,相比于之前,晒了一次缓冲区的复制操作,速度得到成倍提升

在这里插入图片描述

⭐️3.1直接内存的释放原理

publicclassDemo01{staticint _1mb =1024*1024;publicstaticvoidmain(String[] args)throwsIOException{ByteBuffer byteBuffer =ByteBuffer.allocateDirect(_1mb);System.out.println("分配完毕...");System.in.read();System.out.println("开始释放...");
        byteBuffer =null;**加粗样式**System.gc();}}

直接内存的回收是通过

unsafe.freeMemory

手动释放的

allocateDirect实现

publicstaticByteBufferallocateDirect(int capacity){returnnewDirectByteBuffer(capacity);}

DirectBuffer实现

DirectByteBuffer(int cap){// package-privatesuper(-1,0, cap, cap);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.create(this,newDeallocator(base, size, cap));
        att =null;}

Cleaner在java类库里是一种虚引用类型,当他所关联的对象(DirectBuffer)被回收时,就会触发虚引用对象中的clean方法来清除直接内存中占用的内存

cleaner =Cleaner.create(this,newDeallocator(base, size, cap));
        att =null;
publicvoidrun(){if(address ==0){// Paranoiareturn;}
            unsafe.freeMemory(address);
            address =0;Bits.unreserveMemory(size, capacity);}

⭐️3.2直接内存的分配和回收原理总结

- 使用了unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
-ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收
- 那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存

注意:

-XX:+DisableExplicitGC

配置虚拟机参数可以使System.gc()显式垃圾回收(Full GC)无效

在这里插入图片描述

标签: java 面试 jvm

本文转载自: https://blog.csdn.net/wenwenaier/article/details/122627570
版权归原作者 温文艾尔 所有, 如有侵权,请联系我们删除。

“【JVM】JVM02(方法区-串池-直接内存)”的评论:

还没有评论