jvm运行时数据区java虚拟机在运行时将自己管理的内存划分为若干个区域。这些区域有自己的用途、创建和销毁时间,其中一些区域遵循虚拟机进程。它是在启动时创建的,有些是随着用户线程的启动和终止而创建和销毁的。根据虚拟机规格,虚拟机内存分为以下几个区域。在执行过程中,分支、循环、跳转、线程恢复都需要通过计数器获取下一条指令的地址才能执行。Threadprivate每个线程都有自己独立的程序计数器,因为多线程是通过线程轮流切换来实现的。同时cpu或core上总是只有一个线程在执行。一个线程执行到一半,就轮到下一个线程执行了。需要通过计数器保存指令执行的地址。恢复后,会从计数器保存的地址继续执行。内存占用很小,不存在。内存溢出风险案例让我们用一个例子来看看程序计数器是如何工作的publicclassPCTest{publicstaticvoidmain(String[]args){for(inti=0;i<2;i++){if(i==0){System.out.println("你好");}else{System.out.println("世界");}}}}使用javap-vPCTest反编译得到如下片段,左边红框内是字节码指令地址,篮子表示这条字节码指令会跳转到指定地址执行。PC的作用就是记录这些指令的地址。虚拟机栈定义了每个线程执行所需的内存空间。它被称为堆栈。调用方法时,会创建堆栈帧并将其压入堆栈。调用该方法后,将其堆栈帧弹出堆栈。堆栈帧包含局部变量。表、操作数栈、动态链接、返回地址等信息。每个栈只有一个活动栈帧,即栈顶的栈帧,对应一组变量值的存储空间正在执行的方法的栈帧局部变量表中。理解为一个数组,数组中的每个位置用来存储一个局部变量,或者说方法参数,这个变量(只针对实例方法)。在编译成class文件的时候,局部变量表的最大长度已经确定,保存在code属性的max_locals附加属性中。变量槽每个变量槽可以存储一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。如果数据是double或long类型,则应使用两个连续的变量槽。有两种引用类型。该函数可以直接或间接地找到对象在堆中的地址。它可以直接或间接地在方法区中找到对象所属数据类型的类型信息。如果变量槽的分配是实例方法,则第一个变量槽先分配this参数分配给变量槽,然后变量槽分配给局部变量。变量槽重用方法体中定义的变量。变量的范围不一定是整个方法。在变量范围之外定义的变量可以重用其变量槽操作数。栈是先进后出的栈。编译完成后堆栈的最大深度已经确定。code属性的max_stacks数据项中存放的double和long占2个栈容量,其他数据类型占1个栈容量,可以用来给调用传递参数。该方法可用于算术运算。动态链接每个栈帧都包含一个引用,指向当前方法所属类型在运行时常量池中的地址,用于支持方法代码的动态链接。在类文件中,一个方法通过符号引用调用其他方法或访问字段。动态链接将对这些方法的符号引用转换为对方法的直接引用,必要时加载类信息以解析未解析的符号引用,并将变量访问转换为存储结构中相对于这些变量的运行时位置的适当偏移量。移位。返回地址方法退出有两种,正常退出和异常退出。正常退出可以有返回值,但异常退出一定不能有返回值,但无论正常退出还是异常退出,都必须回到原来调用方法的位置。正常退出就是将调用方法保存在栈帧中的PC计数器的值作为返回地址。当返回地址异常退出时,通过异常处理表确定返回地址。方法退出的过程可能如下。局部变量表,将当前栈帧的返回值压入调用者栈帧的操作数栈,调整PC计数器的值指向方法调用后指令所在的位置。线程、栈和栈帧之间的关系。堆栈框架演示。用idea写测试。类,并在methodC()方法上设置断点,然后以调试模式启动程序publicclassStackFrameTest{publicstaticvoidmain(String[]args){methodA();}privatestaticvoidmethodA(){System.out.println("A");方法B();}privatestaticvoidmethodB(){System.out.println("B");方法C();}privatestaticvoidmethodC(){System.out.println("C");}}当程序运行到methodC()时,查看控制台,发现有4个栈帧。本机方法堆栈与虚拟机堆栈非常相似。不同的是,虚拟机栈是为调用java方法服务的,而本地方法栈是为虚拟机调用本地方法服务的。当栈深度溢出或者栈扩展失败时,本地方法栈会分别抛出StackOverflowError和OutOfMemoryError异常。堆定义了用于存储对象实例的内存区域。特点由线程共享,堆中的对象需要考虑线程安全问题。通过自动内存回收机制管理堆内存。堆的最小和最大内存分别由-Xms和-Xmx设置。例如,-Xms10m将最大堆内存设置为10m。代码可以测试堆内存的溢出。启动程序前,需要设置虚拟机参数-Xmx10m,设置堆内存最大值为10mpackagedataarea;importjava.util.ArrayList;importjava.util.List;/***VMArgs-Xmx10m*堆内存溢出问题*@authorct*@date2021/10/21*/publicclassHeapOOMTest{staticclassOOMObject{byte[]bytes=newbyte[1024];}publicstaticvoidmain(String[]args){Listlist=newArrayList<>();尝试{while(true){list.add(newOOMObject());}}catch(Throwablee){e.printStackTrace();}最后{System.out.println(list.size());从运行结果可以看出堆内存已经溢出,OOM异常8641Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspaceatjava.lang.Throwable.printStackTrace(Throwable.java:649)atjava.lang.Throwable.printStackTrace(Throwable.java:643)atjava.lang.Throwable.printStackTrace(Throwable.java:634)atdataarea.HeapOOMTest.main(HeapOOMTest.java:24)方法区定义方法区是所有线程共享的区域,主要用来存放类相关的信息,比如运行时虚拟机启动时创建的常量池、字段、方法数据、方法或构造函数代码方法区,虽然方法区是逻辑上是堆的一部分,虚拟机的实现可以选择不进行垃圾收集或组织方法区是规范,永久代或元空间是它的实现。hotspot1.8之前使用的permanentgeneration,1.8之后使用的metaspacepermanentgeneration在堆内存中,metaspace在localmemoryhotspot8和hotspot8之前的methodarea比较内存溢出jdk1的内存溢出。8测试元空间。通过动态代理技术创建大量类,并设置元空间大小为10m:-XX:MaxMetaspaceSize=10mpackagecom.company;importjdk.internal.org.objectweb.asm.ClassWriter;importjdk.internal.org.objectweb.asm.Opcodes;/***创建元空间内存溢出,通过*VMargs-XX:MaxMetaspaceSize=10m**@authorct*@date2021/10/22*/publicclassMethodAreaOOMTest1extendsClassLoader{publicstaticvoidmain(String[]args){MethodAreaOOMTest1测试=newMethodAreaOOMTest1();ClassWritercw=newClassWriter(0);for(inti=0;i<10000;i++){//jdk版本、修饰符、类名、包名、父类cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);byte[]code=cw.toByteArray();test.defineClass("类"+i,code,0,code.length);}}}执行结果如下{:height107,:width554}运行时常量池常量池是编译后的字节码文件中的字面量和符号引用信息,当一个类被jvm加载时,常量中的数据pool会被加载到方法区的内存中,在运行时常量池中由符号引用翻译成直接引用,同时也会存放在运行时常量池中的字符串池试题中(如果知道输出下面的程序,你可以跳过这个讲座)packagestringtable;/***stringtable.StringTableTest**@authorct*@date2021/10/23*/public类StringTableTest{publicstaticvoidmain(String[]args){Strings1="a";字符串s2="b";字符串s3="a"+"b";字符串s4=s1+s2;字符串s5="ab";字符串s6=s4.intern();System.out.println(s3==s4);//System.out.println(s3==s5);//System.out.println(s3==s6);//Stringx1=newString("c")+新字符串("d");字符串x3=x1.intern();字符串x2="cd";//如果第22行和23行的位置调换了,还是在jdk1.6中运行System.out.println(x1==x3);//System.out.println(x2==x3);//}}运行常量池和字符串池关系常量池信息加载在运行时常量池中,字符串池是运行时常量池的一部分。字符串池功能。常量池中的字符串只是一个符号。它在第一次使用时成为一个对象。利用字符串池的机制避免重复创建。字符串对象和字符串变量的拼接是通过StringBuilder来完成的。字符串常量拼接的原理是可以通过编译器优化使用intern()方法,主动将不在字符串池中的字符串对象放入字符串池中。stringpool是底层使用hashTable,可以使用变量拼接如下,s1+s2会转换成newStringBuilder().append("a").append("b").toString()Strings1="a";字符串s2="b";字符串s3=s1+s2;可以通过查看字节码来验证常量拼接对于两个字符串常量的直接相加,可以在编译时直接通过javac优化成一个字符串常量Strings1="a";Strings2="b";Strings3="a"+"b";通过分析字节码指令可以看出,s3直接编译成ab字符串常量懒加载的验证通过下面的代码和idea的debug函数,我们来验证字符串常量懒加载包stringtable的过程;/***stringtable.StringTableTest**@authorct*@date2021/10/23*/publicclassStringTableTest2{publicstaticvoidmain(String[]args){System.out.println();System.out.println("0");System.out.println("1");System.out.println("2");System.out.println("3");System.out.println("4");System.out.println("5");System.out.println("6");System.out.println("7");System.out.println("8");System.out.println("9");System.out.println();System.out.println("0");System.out.println("1");System.out.println("2");System.out.println("3");System.out.println("4");System.out.println("5");System.out.println("6");System.out.println("7");系统输出。println("8");System.out.println("9");系统输出。打印();}}断点打在第12、22、32行,当代码执行到第11行时,我们可以看到断点执行时在调试工具界面右侧string类的实例数为949.在第22行的末尾可以看到实例数变成了959,但是在第32行的末尾看实例数还是959,从上面的执行结果可以看出看到字符串“0”、“1”、“2”.....“9”原来在字符串池中是不存在的。执行时会创建一个字符串对象,添加到字符串池中并返回其引用,然后执行,因为它已经存在于字符串池中,所以不再创建新对象。intern()方法intern的字面意思是拘留和软禁。听起来很符合这个方法的功能。将一个字符串常量放入字符串池中。在jdk1.8和jdk1.6中,略有不同:1.8尝试将字符串放入字符串池,如果没有则放入字符串池,并返回一个对象引用1.6尝试将字符串放入字符串池,如果没有就放入,如果没有就复制一份放入字符串池,返回一个指向字符串池所在位置的对象引用。不高,但字符串是经常使用的对象。1.8虚拟机中,字符串池被转移到堆中,验证如下代码一直向字符串常量池写入数据,导致内存溢出packagestringtable;importjava.util.ArrayList;importjava.util.List;/***测试jdk1.8下字符串池过大导致的堆内存溢出。运行前需要修改heapsize为-Xmx=10m*VMargs:-Xmx10m*@authorct*@date2021/10/27*/publicclassStringTableOOMTest{publicstaticvoidmain(String[]args){Listlist=newArrayList<>();对于(inti=0;i<1000000;i++){list.add(String.valueOf(i).intern());}}}执行后发现抛出异常,但是异常提示是GCoverheadlimitexceeded,根据Oracle官方文档文件,意思是:默认情况下,如果Java进程98%以上的时间都在执行GC,而每次只有不到2%的heap被恢复,JVM就会抛出这个错误Exceptioninthread"main"java.lang.OutOfMemoryError:在java.lang.Integer.toString(Integer.java:401)在java.lang.String.valueOf(String.java:3099)在stringtable.StringTableOOMTest.main(StringTableOOMTest.java:16)超出了GC开销限制可以使用-XX:-UseGCOverheadLimit参数让虚拟机在内存溢出时直接抛出异常。从打印信息可以看出,抛出了堆内存溢出异常。字符串池垃圾回收字符串常量池也会被垃圾回收。通过-XX:+PrintStringTableStatistics参数,我们可以打印出jvm退出时字符串池的详细信息。公共类StringTableGCTest{publicstaticvoidmain(String[]args){for(inti=0;i<100;i++){String.valueOf(i).intern();}}}从打印信息可以看出,字符串池中共有846个字符串。我们把100-199这100个数字的字符串也加入到字符串池中,然后运行:publicclassStringTableGCTest{publicstaticvoidmain(String[]args){for(inti=0;i<200;i++){String.valueOf(i).intern();}}}可以看到字符串数量增加到946个,此时没有内存溢出。然后我们增加字符串个数到100000个,最大堆内存设置为-Xmx10m,同时加上-XX:+PrintGCDetails参数看gc日志publicclassStringTableGCTest{publicstaticvoidmain(String[]args){for(inti=0;i<100000;i++){String.valueOf(i).intern();}}}可以看到结果字符串池中的字符串数量只有3万多条。查看日志,可以看到发生了新生代的垃圾回收,从侧面说明字符串池存在于堆中。性能调优设置bucketsize由于StringTable是一个hashtable,bucket的数量越多,hash冲突的可能性越小,效率也越高,我们可以通过jvm参数-XX:StringTableSize来增加bucket的数量,看一个例子,首先我们不修改StringTableSize(默认为60013),执行如下代码,将字符串写入字符串池:packagestringtable;importjava.util.Random;/***通过增加StringTable中Bucket的数量,intern()efficiency*VMArgs:-XX:StringTableSize=500000-XX:+PrintStringTableStatistics*@authorct*@date2021/10/23*/publicclassStringTableSizeTest{publicstaticvoidmain(String[]args){随机random=newRandom(1);长启动=System.currentTimeMillis();对于(inti=0;i<1000000;i++){String.valueOf(random.nextInt()).intern();}System.out.println("成本"+(System.currentTimeMillis()-开始));}}可以看到执行时间是800ms,那么把StringTableSize改成500000,可以看到效率提高了将近一倍。将重复的字符串放入字符串池中,可以有效减少内存占用。假设有大量的字符串数据需要存放在内存中,并且数据中有重复的部分,可以考虑将其驻留到字符串池中,以减少内存占用。