1. 内存简介
在程序执行过程中,需要不断的将逻辑地址和物理地址进行映射。作为操作系统架构,Java面临着于其他程序完全相同的内存限制,受限于操作系统架构提供的可寻址地址空间。操作系统架构提供的可寻址地址空间由处理器的位数决定。
32位处理器:2^32的可寻址范围、4GB
64位处理器:2^64的可寻址范围。
地址空间的划分:
1.内核空间:连接计算机硬件,调度程序,提供联网、虚拟内存等
2 用户空间:Java程序实际运行时使用的内存空间。
2 JVM内存模型--JDK8
程序计数器(Program Counter Register):是一块较小的内存空间
1. 当前线程所执行的字节码行号指示器,是一个逻辑计算器,
2.通过改变计数器的值来选取下一条需要执行的字节码指令。(分支、循环、跳转、异常处理、线程恢复等)
3. 和线程是一对一的关系,即“线程私有“,因为在确定时刻,一个处理器只会执行一个线程。
4.对Java方法计数(计算器记录的时正在执行的JVM字节码指令的地址),如果Native方法则计数器值为Undefined.
5 不会发生内存泄露
Java虚拟机栈(Stack)
1. java 方法执行的内存模型、
2. 每个Java方法被执行时,都会创建一个栈帧,即方法运行期间的基础数据结构。因此java虚拟机栈包含许多栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、返回地址等,Java方法调用结束后,栈帧才会被销毁。
局部变量表和操作数栈:
局部变量表:包含方法执行过程中的所有的变量。
操作数栈:入栈、出栈、复制、交换、产生消费变量(在执行字节码指令过程中被用到)
例:c为 局部变量
通过Javac指令进行编译,得到,class 文件
通过javap 指令对 ,class 文件进行反编译,得到jvm 机器码指令
执行add(1,2)如下图:
如图: 局部变量的大小(深度)为3,操作数栈的深度为2,一个长方形代表一个栈帧,总7个栈帧。JVM栈会按照程序计数器从大到小依次把栈帧压入,执行时,根据栈的性质,就会从小到大依次执行。执行时先执行iconst_0,就是将int 0压入操作数栈中,同时入参是1和2,因此这里的局部变量表就是1和2;下一步就执行iconst_2执行,就是讲操作数栈的0 pop出,存入到局部变量表的第二个变量中;下一步,iload_0, 就是将第0个局部变量的数1 ,压入栈中。iload_1, 就是将第1个局部变量的数2 ,压入栈中。iadd 就是将栈中的元素pop出,执行运算相加,在将得到的结果压入栈。istore_2 将栈顶的元素3 弹出,存入到局部变量中,替换掉0。iload2再次把第二个元素3 压入操作数栈中,最后调用ireture,将栈顶元素返回。所有的栈帧被销毁。
问题:递归为什么会引发Java.lang.StackOverflowErrow异常。
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压入虚拟机栈中,当方法执行完毕时,便会将栈帧出栈,因此可知线程当前执行的方法所对应的栈帧必定位于Java栈的顶部,而我们的递归函数不断的调用自身,每一次方法调用会涉及一下三点:第一,每次调用一个方法就会生成一个栈帧。第二,保存当前方法的栈帧状态,将它放在虚拟机栈中。第三,栈帧上下文切换时,会切换到最新的方法栈帧中。 但是我们每个线程对的虚拟机栈的深度是固定的,递归实现将导致栈深度的增加,每次递归都会往栈里压一个栈帧,如果超过了线程的虚拟机栈的最大深度,就会引发Java.lang.StackOverflowErrow异常。
总结: 递归过深,栈帧数超过虚拟栈深度。
虚拟机栈过多会引发Java.lang.OutOfMenoryError异常(当虚拟机栈可以动态扩展时,如果无法申请足够多的内存,就会异常)
本地方法栈
与虚拟机栈相似,主要作用于标注了native的方法。
元空间(MetaSpace)与永久代(PermGen)的区别:
(在JDK8之后,开始把类的源数据放在本地堆内存中的一块区域,这块区域就叫做MetaSpace,该区域在JDK7之前属于永久代,元空间和永久代都是用来存贮class的相关信息,包括class对象的方法和属性。元空间和永久代都是方法区的实现,在Java7之后,原来位于方法区中的字符串常量池,已经被移动到Java堆中,并且在JDK8以后,使用了元空间替代了永久代。)
1. 元空间使用本地内存,永久代使用的是JVM的内存。使用本地内存最直接的好处就是、
java,lang,OutOfMemoryError:PermGen Space 异常将不复存在。因为默认类的源数据的分配只受本地内存的限制。
MetaSpace相比PermGen的优势:
1 字符串常量池存在永久代中,容易出现性能问题的内存溢出。
2 类和方法的信息大小难以确定,给永久代的大小指定带来了困难。
3 永久代会为GC带来不必要的复杂性。
4 方便HotSpot与其他JVM 如Jrockit的集成。
Java 堆(Heap):被所有线程共享的内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
1. 对象实例的分配区域
(图 32位处理器的java进程的内存布局)
2 垃圾回收(GC)管理的主要区域
问题: JVM三大性能调优参数 -Xms -Xmx -Xss的含义?
1 . -Xss: 规定了每个线程虚拟机栈(堆栈)的大小,一般256K,此配置会影响此进程中并发线程数的大小。
2. -Xms : 堆的初始值,表示初始化的java堆的大小,即该进程刚创建出来时专属java堆的大小。一旦对象容量超过java堆的初始容量,java堆就会自动扩容,扩容至Xmx
3. -Xmx:堆能达到的最大值。
问题:java内存中堆和栈的区别--内存分配策略:
java程序运行时,有三种内存分配策略:静态的、栈式的、堆式的
静态存储:编译时确定每个数据目标在运行时的存储空间需求。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套、递归的出现。
栈式存储:数据区需求在编译时未知,运行时模块入口前确定。该分配可成为动态存储分配,是由一个类似有堆栈的运行栈来实现的,和静态存储的分配方式相反。
堆式存储:专门针对在编译时或者运行时模块入口都无法确定存储空间需求的,动态分配。例如:可变长度串,对象实例。堆有大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
问题: java内存中堆和栈的区别与联系?
1. 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址。(创建好的数组、对象实例都会保存在堆中,想要引用这些数组和对象实例,可以在栈中定义一个特殊的变量,让这个变量的取值等于数据、对象实例在堆内存中的首地址,栈中的变量就是引用变量。引用变量是普通的变量,定义时在栈中分配,在程序运行到其作用域之外就会被释放掉。而数组和对象本身在堆中分配,即使程序运行在其作用域之外,数组和对象本身占据的内存不会被释放,他们在没有引用变量指向的时候,才会变成垃圾。)
2 区别:
1. 管理方式:栈自动释放,堆需要GC (JVM本身可以对内存栈管理操作,该内存空间的释放,编译器就可以操作。而堆空间,JVM执行引擎不会对其操作,会让垃圾回收机制,对其进行管理)
2.空间大小:栈比堆小
3 碎片相关:栈产生的碎片远小于堆。
4 分配方式:栈支持静态和动态分配,堆仅支持动态分配。
5 效率:栈的效率比堆高。(因为内存块本身的排列就是一个堆栈结构,并且计算机底层内存空间本身就是使用的是最基础的堆栈结构,因此栈的效率会高很多)
问题:元空间、堆、线程独占部分空间的联系--内存角度
元空间: Class :HelloWorld --method :sayhello\setName\main --Field:name
Class: System
(class对象的信息,还有System类对象,和System类中的成员变量和方法)
java堆: Object:String(“test”) (String实例test)
Object:helloWorld (对象实例)
线程独享: Parameter reference:"test" to String Object (Sring类型的引用参数)
Variable reference:"hw" to Hello World object (地址引用Hw,保存是堆中helloworld的地址值)
Loacl Variables: a with 1, lineNo (局部变量a 值为1,系统自带的行号 ,用来记录代码的执行)
程序执行时,main线程会分配对应的虚拟机栈、本地栈、程序计数器
问题: 不同JDK版本之间intern()方法的区别-----JDK6 与JDK6+
JDK6:当调用intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用,否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中该字符串的引用。否则,如果该字符串对象已经存在于java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。