机票旅游网站建设,wordpress页头插件,公司简介万能模板,青岛企业建设网站企业Java虚拟机在运行Java程序过程中管理的内存区域#xff0c;称之为运行时数据区。《Java虚拟机规范》中规定了每一部分的作用。 3.1 程序计数器 程序计数器#xff08;Program Counter Register#xff09;也叫PC寄存器#xff0c;每个线程会通过程序计数器记录当前要执行的…Java虚拟机在运行Java程序过程中管理的内存区域称之为运行时数据区。《Java虚拟机规范》中规定了每一部分的作用。 3.1 程序计数器 程序计数器Program Counter Register也叫PC寄存器每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。 一个程序计数器的具体案例 在加载阶段虚拟机将字节码文件中的指令读取到内存之后会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。 在代码执行过程中程序计数器会记录下一行字节码指令的地址。执行完当前指令之后虚拟机的执行引擎根据程序计数器执行下一行指令。这里为了简单起见使用偏移量代替真实内存中执行时保存的应该是地址。 比如当前执行的是偏移量为0的指令那么程序计数器中保存的就是下一条的地址偏移量1。 一路向下执行 一直执行到方法的最后一行指令此时方法执行return语句当前方法执行结束程序计数器中会放入方法出口的地址栈中讲解简单来说就是这个B方法结束了A调用了B那么要回到A方法 所以程序计数器可以控制程序指令的进行实现分支、跳转、异常等逻辑。不管是分支、跳转、异常只需要在程序计数器中放入下一行要执行的指令地址即可。
在多线程执行情况下Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行。 程序计数器会出现内存溢出吗 内存溢出指的是程序在使用某一块内存区域时存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。由于每个线程只存储一个固定长度的内存地址程序计数器是不会发生内存溢出的。程序员无需对程序计数器做任何处理。
3.2 Java虚拟机栈 Java虚拟机栈Java Virtual Machine Stack采用栈的数据结构来管理方法调用中的基本数据先进后出First In Last Out,每一个方法的调用使用一个栈帧Stack Frame来保存。
public class MethodDemo { public static void main(String[] args) { study(); }public static void study(){eat();sleep();} public static void eat(){ System.out.println(吃饭); } public static void sleep(){ System.out.println(睡觉); }}main方法执行时会创建main方法的栈帧 接下来执行study方法会创建study方法的栈帧 进入eat方法创建eat方法的栈帧 eat方法执行完之后会弹出它的栈帧 然后调用sleep方法创建sleep方法栈帧 最后study方法结束之后弹出栈帧main方法结束之后弹出main的栈帧。
在IDEA中也可以看到对应的栈帧
package chapter03.frame;/*** 栈帧测试1*/
public class FrameDemo {public static void main(String[] args) {A();}public static void A() {System.out.println(A执行了...);B();}public static void B() {System.out.println(B执行了...);C();}public static void C() {System.out.println(C执行了...);throw new RuntimeException(测试);}
}打上断点debug之后会出现栈帧内容 Java虚拟机栈随着线程的创建而创建而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行每个线程都会包含一个自己的虚拟机栈。如下就有两个线程的虚拟机栈main线程和线程A。 Java虚拟机栈的栈帧中主要包含三方面的内容
局部变量表局部变量表的作用是在运行过程中存放所有的局部变量操作数栈操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域帧数据帧数据主要包含动态链接、方法出口、异常表的引用
局部变量表 局部变量表的作用是在方法执行过程中存放所有的局部变量。局部变量表分为两种一种是字节码文件中的另外一种是栈帧中的也就是保存在内存中。栈帧中的局部变量表是根据字节码文件中的内容生成的。
我们先来看下字节码文件中的局部变量表编译成字节码文件时就可以确定局部变量表的内容。
public static void test1(){int i 0;long j 1;
}test1方法的局部变量表如下
局部变量表中保存了字节码指令生效的偏移量
比如i这个变量它的起始PC是2代表从lconst_1这句指令开始才能使用i长度为3也就是2-4这三句指令都可以使用i。为什么从2才能使用因为0和1这两句字节码指令还在处理int i 0这句赋值语句。j这个变量只有等3指令执行完之后也就是long j 1代码执行完之后才能使用所以起始PC为4只能在4这行字节码指令中使用。
接下来看下栈帧中的局部变量表栈帧中的局部变量表是一个数组数组中每一个位置称之为槽(slot) long和double类型占用两个槽其他类型占用一个槽。
i占用数组下标为0的位置j占用数组下标1-2的位置。
刚才看到的是静态方法实例方法中的序号为0的位置存放的是this指的是当前调用方法的对象运行时会在内存中存放实例对象的地址。
方法参数也会保存在局部变量表中其顺序与方法中参数定义的顺序一致。局部变量表保存的内容有实例方法的this对象方法的参数方法体中声明的局部变量。
test3方法中包含两个参数k,m这两个参数也会被加入到局部变量表中。
以下代码的局部变量表中会占用几个槽
public void test4(int k,int m){{int a 1;int b 2;}{int c 1;}int i 0;long j 1;
}为了节省空间局部变量表中的槽是可以复用的一旦某个局部变量不再生效当前槽就可以再次被使用。 1、方法执行时实例对象this、k、m 会被放入局部变量表中占用3个槽 2、将1的值放入局部变量表下标为3的位置上相当于给a进行赋值。 3、将2放入局部变量表下标为4的位置给b赋值为2。 4、ab已经脱离了生效范围所以下标为3和4的这两个位置可以复用。此时c的值1就可以放入下标为3的位置。
4、脱离c的生效范围之后给i赋值就可以复用c的位置。 5、最后放入jj是一个long类型占用两个槽。但是可以复用b所在的位置所以占用4和5这两个位置 所以局部变量表数值的长度为6。这一点在编译期间就可以确定了运行过程中只需要在栈帧中创建长度为6的数组即可。 操作数栈 操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构如果一条指令将一个值压入操作数栈则后面的指令可以弹出并使用该值。 在编译期就可以确定操作数栈的最大深度从而在执行时正确的分配内存大小。 比如之前的相加案例中操作数栈最大的深入会出现在这个时刻 所以操作数栈的深度会定义为2。
帧数据 帧数据主要包含动态链接、方法出口、异常表的引用。
动态链接 当前类的字节码指令引用了其他类的属性或者方法时需要将符号引用编号转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。 方法出口 方法出口指的是方法在正确或者异常结束时当前栈帧会被弹出同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中需要存储此方法出口的地址。 异常表 异常表存放的是代码中异常的处理信息包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。 如下案例i1这行源代码编译成字节码指令之后会包含偏移量2-4这三行指令。其中2-3是对i进行赋值1的操作4的没有异常就跳转到10方法结束。如果出现异常的情况下继续执行到7这行指令7会将异常对象放入操作数栈中这样在catch代码块中就可以使用异常对象了。接下来执行8-9对i进行赋值为2的操作。 所以异常表中异常捕获的起始偏移量就是2结束偏移量是4在2-4执行过程中抛出了java.lang.Exception对象或者子类对象就会将其捕获然后跳转到偏移量为7的指令。
栈内存溢出 Java虚拟机栈如果栈帧过多占用内存超过栈内存可以分配的最大大小就会出现内存溢出。Java虚拟机栈内存溢出时会出现StackOverflowError的错误。 如果我们不指定栈的大小JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。 要修改Java虚拟机栈的大小可以使用虚拟机参数 -Xss 。
语法-Xss栈大小单位字节默认必须是 1024 的倍数、k或者K(KB)、m或者M(MB)、g或者G(GB) 例如
-Xss1048576
-Xss1024K
-Xss1m
-Xss1g操作步骤如下不同IDEA版本的设置方式会略有不同 1、点击修改配置Modify options 2、点击Add VM options 3、添加参数 注意事项 1、与-Xss类似也可以使用 -XX:ThreadStackSize 调整标志来配置堆栈大小。 格式为 -XX:ThreadStackSize1024
2、HotSpot JVM对栈大小的最大值和最小值有要求 比如测试如下两个参数会直接报错: -Xss1k -Xss1025m Windows64位下的JDK8测试最小值为180k最大值为1024m。
一般情况下工作中即便使用了递归进行操作栈的深度最多也只能到几百,不会出现栈的溢出。所以此参数可以手动指定为-Xss256k节省内存。
3.3 本地方法栈 Java虚拟机栈存储了Java方法调用时的栈帧而本地方法栈存储的是native本地方法的栈帧。 在Hotspot虚拟机中Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。 比如测试下这段代码
/*** 本地方法栈*/
public class NativeDemo1 {public static void main(String[] args) {try {FileOutputStream fileOutputStream new FileOutputStream(E:\\123.txt);fileOutputStream.write(1);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}
}执行之后发生异常会打印出所有栈帧的名字 open0是一个本地方法所以创建了本地方法的栈帧。本地方法和Java虚拟机方法的栈帧在一个栈上。
3.4 堆内存 一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。栈上的局部变量表中可以存放堆上对象的引用。静态变量也可以存放堆对象的引用通过静态变量就可以实现对象在线程之间共享。
public class Test { public static void main(String[] args) { Student s1 new Student(); s1.name 张三; s1.age 18; s1.id 1;s1.printTotalScore(); s1.printAverageScore(); Student s2 new Student(); s2.name 李四; s2.age 19; s2.id 2; s2.printTotalScore(); s2.printAverageScore(); }
}
这段代码中通过new关键字创建了两个Student类的对象这两个对象会被存放在堆上。在栈上通过s1和s2两个局部变量保存堆上两个对象的地址从而实现了引用关系的建立。 堆内存的溢出 通过new关键字不停创建对象放入集合中模拟堆内存的溢出观察堆溢出之后的异常信息。
package chapter03.heap;import java.io.IOException;
import java.util.ArrayList;/*** 堆内存的使用和回收*/
public class Demo1 {public static void main(String[] args) throws InterruptedException, IOException {ArrayListObject objects new ArrayListObject();System.in.read();while (true){objects.add(new byte[1024 * 1024 * 100]);Thread.sleep(1000);}}
}堆内存大小是有上限的当对象一直向堆中放入对象达到上限之后就会抛出OutOfMemory错误。在这段代码中不停创建100M大小的字节数组并放入ArrayList集合中最终超过了堆内存的上限。抛出如下错误 三个重要的值 堆空间有三个需要关注的值used、total、max。used指的是当前已使用的堆内存total是java虚拟机已经分配的可用堆内存max是java虚拟机可以分配的最大堆内存。 堆内存used total max三个值可以通过dashboard命令看到。 手动指定刷新频率不指定默认5秒一次dashboard –i 刷新频率(毫秒) 随着堆中的对象增多当total可以使用的内存即将不足时java虚拟机会继续分配内存给堆。 此时used达到了total的大小Java虚拟机会向操作系统申请更大的内存。 但是这个申请过程不是无限的total最多只能与max相等。 那么是不是当used max total的时候堆内存就溢出了呢 不是堆内存溢出的判断条件比较复杂在下一章《垃圾回收器》中会详细介绍。
如果不设置任何的虚拟机参数max默认是系统内存的1/4total默认是系统内存的1/64。在实际应用中一般都需要设置total和max的值。 Oracle官方文档https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
设置堆的大小 要修改堆的大小可以使用虚拟机参数 –Xmxmax最大值和-Xms (初始的total)。 语法-Xmx值 -Xms值 单位字节默认必须是 1024 的倍数、k或者K(KB)、m或者M(MB)、g或者G(GB) 限制Xmx必须大于 2 MBXms必须大于1MB
-Xms6291456
-Xms6144k
-Xms6m
-Xmx83886080
-Xmx81920k
-Xmx80m这样可以将max和初始的total都设置为4g在启动后就已经获得了最大的堆内存大小。运行过程中不需要向操作系统申请。 使用arthas的memory命令同样可以看到 为什么arthas中显示的heap堆大小与设置的值不一样呢 arthas中的heap堆内存使用了JMX技术中内存获取方式这种方式与垃圾回收器有关计算的是可以分配对象的内存而不是整个内存。
建议 Java服务端程序开发时建议将-Xmx和-Xms设置为相同的值这样在程序启动之后可使用的总内存就是最大内存而无需向java虚拟机再次申请减少了申请并分配内存时间上的开销同时也不会出现内存过剩之后堆收缩的情况。-Xmx具体设置的值与实际的应用程序运行环境有关在《实战篇》中会给出设置方案。
3.5 方法区 方法区是存放基础信息的位置线程共享主要包含三部分内容
类的元信息保存了所有类的基本信息运行时常量池保存了字节码文件中的常量池内容字符串常量池保存了字符串常量
类的元信息 方法区是用来存储每个类的基本信息元信息一般称之为InstanceKlass对象。在类的加载阶段完成。其中就包含了类的字段、方法等字节码文件中的内容同时还保存了运行过程中需要使用的虚方法表实现多态的基础等信息。 运行时常量池 方法区除了存储类的元信息之外还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。 字节码文件中通过编号查表的方式找到常量这种常量池称为静态常量池。当常量池加载到内存中之后可以通过内存地址快速的定位到常量池中的内容这种常量池称为运行时常量池。 方法区的实现 方法区是《Java虚拟机规范》中设计的虚拟概念每款Java虚拟机在实现上都各不相同。Hotspot设计如下 JDK7及之前的版本将方法区存放在堆区域中的永久代空间堆的大小由虚拟机参数来控制。 JDK8及之后的版本将方法区存放在元空间中元空间位于操作系统维护的直接内存中默认情况下只要不超过操作系统承受的上限可以一直分配。 可以通过arthas的memory命令看到方法区的名称以及大小
JDK7及之前的版本查看ps_perm_gen属性。JDK8及之后的版本查看metaspace属性。 方法区的溢出 通过ByteBuddy框架动态创建类并将字节码数据加载到内存中。通过死循环不停地加载到方法区观察方法区是否会出现内存溢出的情况。分别在JDK7和JDK8上运行上述代码。 ByteBuddy是一个基于Java的开源库用于生成和操作Java字节码。 1.引入依赖
dependencygroupIdnet.bytebuddy/groupIdartifactIdbyte-buddy/artifactIdversion1.12.23/version/dependency2.创建ClassWriter对象 ClassWriter classWriter new ClassWriter(0);3.调用visit方法创建字节码数据。
classWriter.visit(Opcodes.V1_7,Opcodes.ACC_PUBLIC,name,null ,java/lang/Object,null);
byte[] bytes classWriter.toByteArray();package chapter03.methodarea;import net.bytebuddy.jar.asm.ClassWriter;
import net.bytebuddy.jar.asm.Opcodes;import java.io.IOException;/*** 方法区的溢出测试*/
public class Demo1 extends ClassLoader {public static void main(String[] args) throws IOException {System.in.read();Demo1 demo1 new Demo1();int count 0;while (true) {String name Class count;ClassWriter classWriter new ClassWriter(0);classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, java/lang/Object, null);byte[] bytes classWriter.toByteArray();demo1.defineClass(name, bytes, 0, bytes.length);System.out.println(count);}}
}实验发现JDK7上运行大概十几万次就出现了错误。 在JDK8上运行百万次程序都没有出现任何错误但是内存会直线升高。这说明JDK7和JDK8在方法区的存放上采用了不同的设计。
JDK7将方法区存放在堆区域中的永久代空间堆的大小由虚拟机参数-XX:MaxPermSize值来控制。JDK8将方法区存放在元空间中元空间位于操作系统维护的直接内存中默认情况下只要不超过操作系统承受的上限可以一直分配。可以使用-XX:MaxMetaspaceSize值将元空间最大大小进行限制。
在JDK8中将最大元空间内存设置为256m再次测试 这次就出现了MetaSpace溢出的错误 字符串常量池 方法区中除了类的元信息、运行时常量池之外还有一块区域叫字符串常量池(StringTable)。 字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123就会被放入字符串常量池。
如下代码执行时代码中包含abc字符串就会被直接放入字符串常量池。在堆上创建String对象并通过局部变量s1引用堆上的对象。 接下来通过s2局部变量引用字符串常量池的abc。 所以s1和s2指向的不是同一个对象打印出false。
字符串常量池和运行时常量池有什么关系 早期设计时字符串常量池是属于运行时常量池的一部分他们存储的位置也是一致的。后续做出了调整将字符串常量池和运行时常量池做了拆分。 StringTable的练习题1
/*** 字符串常量池案例*/
public class Demo2 {public static void main(String[] args) {String a 1;String b 2;String c 12;String d a b;System.out.println(c d);}
}1、首先将1放入字符串常量池通过局部变量a引用字符串常量池中的1字符串。 2、同理处理b和c 3、将a和b指向的字符串进行连接本质上就是使用StringBuilder进行连接最后创建了一个新的字符串放入堆中。然后将局部变量d指向堆上的对象。 4、所以c和d指向的不是同一个对象打印出的结果就是false。
StringTable的练习题2
package chapter03.stringtable;/*** 字符串常量池案例*/
public class Demo3 {public static void main(String[] args) {String a 1;String b 2;String c 12;String d 1 2;System.out.println(c d);}
}编译之后的字节码指令如下 说明在编译阶段已经将1和2进行连接最终生成12的字符串常量池中的结果。所以返回结果就是truec和d都指向字符串常量池中的对象。
总结一下 神奇的intern String.intern()方法是可以手动将字符串放入字符串常量池中分别在JDK6 JDK8下执行代码JDK6 中结果是false false JDK8中是true false
package chapter03.stringtable;/*** intern案例*/
public class Demo4 {public static void main(String[] args) {String s1 new StringBuilder().append(think).append(123).toString();System.out.println(s1.intern() s1);
// System.out.println(s1.intern() s1.intern());String s2 new StringBuilder().append(ja).append(va).toString();System.out.println(s2.intern() s2);}
}先来分析JDK6中代码执行步骤如下 1、使用StringBuilder的将think和123拼接成think123转换成字符串在堆上创建一个字符串对象。局部变量s1指向堆上的对象。 2、调用s1.intern方法会在字符串常量池中创建think123的对象最后将对象引用返回。所以s1.intern和s1指向的不是同一个对象。打印出false。 3、同理通过StringBuilder在堆上创建java字符串对象。这里注意字符串常量池中本来就有一个java字符串对象这是java虚拟机自身使用的所以启动时就会创建出来。 4、调用s2.intern发现字符串常量池中已经有java字符串对象了就将引用返回。所以s2.intern指向的是字符串常量池中的对象而s2指向的是堆中的对象。打印结果为false。 接下来分析JDK7中JDK7及之后版本中由于字符串常量池在堆上所以intern () 方法会把第一次遇到的字符串的引用放入字符串常量池。 代码执行步骤如下 1、执行第二句代码时由于字符串常量池中没有think123的字符串所以直接创建一个引用指向堆中的think123对象。所以s1.intern和s1指向的都是堆上的对象打印结果为true。 2、s2.intern方法调用时字符串常量池中已经有java字符串了所以将引用返回。这样打印出来的结果就是false。 后续JDK版本中如果Java虚拟机不需要使用java字符串那么字符串常量池中就不会存放java。打印结果有可能会出现两个true。
面试题静态变量存储在哪里呢
JDK6及之前的版本中静态变量是存放在方法区中的也就是永久代。JDK7及之后的版本中静态变量是存放在堆中的Class对象中脱离了永久代。具体源码可参考虚拟机源码BytecodeInterpreter针对putstatic指令的处理。
3.6 直接内存 直接内存Direct Memory并不在《Java虚拟机规范》中存在所以并不属于Java运行时的内存区域。 在 JDK 1.4 中引入了 NIO 机制使用了直接内存主要为了解决以下两个问题: 1、Java堆中的对象如果不再使用要回收回收时会影响对象的创建和使用。 2、IO操作比如读文件需要先把文件读入直接内存缓冲区再把数据复制到Java堆中。 现在直接放入直接内存即可同时Java堆上维护直接内存的引用减少了数据复制的开销。写文件也是类似的思路。
使用堆创建对象的过程 使用直接内存创建对象的过程不需要进行复制对象数据直接存放在直接内存中 使用方法 要创建直接内存上的数据可以使用ByteBuffer。 语法 ByteBuffer directBuffer ByteBuffer.allocateDirect(size); 注意事项 arthas的memory命令可以查看直接内存大小属性名direct。
package chapter03.direct;import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;/*** 直接内存的使用和回收*/
public class Demo1 {public static int size 1024 * 1024 * 100; //100mbpublic static ListByteBuffer list new ArrayListByteBuffer();public static int count 0;public static void main(String[] args) throws IOException, InterruptedException {System.in.read();while (true) {//1.创建DirectByteBuffer对象并返回//2.在DirectByteBuffer构造方法中向操作系统申请直接内存空间ByteBuffer directBuffer ByteBuffer.allocateDirect(size);//directBuffer null;list.add(directBuffer);System.out.println(count);Thread.sleep(5000);}}
}
运行之后用arthas监控发现每隔5秒直接内存会增长100M 如果将Thread.sleep(5000);注释掉让直接内存快速大量分配。操作系统内存不足时就会报错 但是工作中服务器上有可能部署了其他应用为了避免将内存耗尽需要设置直接内存的最大值。如果需要手动调整直接内存的大小可以使用XX:MaxDirectMemorySize大小 单位k或K表示千字节m或M表示兆字节g或G表示千兆字节。默认不设置该参数情况下JVM 自动选择 最大分配的大小。 以下示例以不同的单位说明如何将 直接内存大小设置为 1024 KB
-XX:MaxDirectMemorySize1m
-XX:MaxDirectMemorySize1024k
-XX:MaxDirectMemorySize1048576在Idea中设置直接内存最大值为1g 直接循环11次之后超过最大值就会报错