JVM的内存区域划分
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:
程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
3.1 内存分配
3.1.1 程序计数器
想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址)。
当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,并执行相关指令。
在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。
虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示执行哪条指令的。
在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
简单的说就是用来获取需要执行的指令地址,并指示下一个需要执行的指令的地址位置。
3.1.2 Java栈
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,
Java栈是Java方法执行的内存模型
。
为什么这么说呢?下面就来解释一下其中的原因。
Java栈中存放的是一个个的栈帧,
每个栈帧对应一个被调用的方法
,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。
简单地说:当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。故java栈是更方法有关的。栈的结构如下
线程栈里能分配的栈帧越少,对JVM整体来说能开启的线程数会更多。
局部变量表
:
顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。
对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用,其引用的具体地址存储在堆中。
局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈:
想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。
想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
指向运行时常量池的引用
:
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址
:
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。故Java栈是线程私有的
注意:
顶部的栈帧表示的是当前的方法;注意的是如果栈的深度过大。如递归过深虚拟机会抛出StackOverError。
如果虚拟机允许栈动态扩展内存,则内存不足时抛出OutOfMemoryError.
3.1.3 本地方法栈
本地方法栈与Java栈的作用和原理非常相似,区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
3.1.4 堆(heap)
用来存储类的实例,如通过new关键字和构造器创建的对象放在堆空间;
在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。
那么在Java中是怎么样的呢?
Java中的堆是用来存储对象本身。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。
因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。
注意:堆中是不会包含类的静态变量的。
3.1.5 方法区
方法区又叫静态存储区,在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。
在方法区中,存储了类信息、类的静态变量(普通成员变量是跟随实例放在堆中)、常量以及编译器编译后的代码等。即方法区=
静态变量
+
常量
+
类信息(构造方法/接口定义)
+
运行时常量池
。
总结
线程共享的区域是堆(heap)和方法区,其他都是线程私有的。除了程序技术器不会发生内存溢出,其它都会发生内存溢出。
思考
1.什么是PermSpace?
PermSpace主要是存放静态的类信息和方法信息,静态的方法和变量,final标注的常量信息等。
@see JVM调优:PermSpace溢出 https://blog.csdn.net/blueheart20/article/details/39859733/
2. OOM和SOF
栈内存溢出:栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。
发生sof的可能原因:
1.方法递归调用
2.线程启动过多。如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过, 但是无法申请到足够的内存去完成扩展, 或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈, 那么 Java 虚拟机将抛出一个 OutOfMemory 异常。
3.当线上发生oom时,你该怎么办?
1.首先重启服务器对应进程,因为已经发生了oom,进程已无法为新分配的对象进行存储了,此时服务已不可用。
2.查看监控平台,观察服务器的GC情况,找出full GC的位置和原因
4.堆和方法区的区别?
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,方法区一对多个堆。
简单的理解就是:方法区主要存储类,而堆主要存储类实例化后的对象。
3.3 运行时常量池
运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。
运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。
这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)
3.4 堆内存模型
3.4.1 JDK1.7以前堆内存模型
3.4.2 JDK1.8堆内存模型
JDK 1.8之后将最初的永久代内存空间取消了,为了将HotSpot与JRockit两个虚拟机标准联合为一个.
新生代:新的对象和没有达到一定“年龄”的对象,所存放的空间(活跃对象)。
老年代:被长时间使用的对象,老年代空间要相对较大。
元空间:一些操作的临时对象,如方法中的创建的临时对象,直接使用物理内存。
注意:永久代和元空间都是存放临时对象的,但是永久代使用的是JVM直接分配的内存,而元空间使用的是物理内存
伸缩区
:
伸缩区的考虑在某个内存空间不足的时候,会自动打开伸缩区扩大内存,当发现当前的区域内存可以,满足要求的时候,就可以进行收缩了。(其实质就是jvm所配置的最大值减去所配置的初始值)
伸缩区的优缺点:
如果不进行收缩的话优点是:可以提升堆内存的结构优化。
如果不进行收缩的缺点:空间太大了。那么没有选择合适的GC算法,就会造成堆内存的性能下降。
伊甸园区:
所有使用关键字new新实例化的对象一定会在伊甸园区进行保存(如果Eden中内存空间充足)
存活区:
存活区保存的一定是已经在伊甸园区中存在好久,并且经过了好几次的Minor GC还保存下来的活跃对象。
老年代:
老年代主要是接收由年轻代发送来的对象,以下几种会进入到老年代。
1.如果你要保存的对象超过了伊甸园区的大小,那么此对象也将直接保存到老年代之中。
2.对象的存活年龄到了一定的阀值。
3.对象在新生代中经历固定次数minor GC,会进入老年代可通过-XX:MaxTenuringThreshold设置,默认15
思考
1.内存泄漏和内存溢出的区别和联系?
1)内存泄漏memory leak :
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete)即无法被回收,结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
自己理解:主要就是两种问题1.丢失了这块地址的引用。2.已分配的内存无法被垃圾回收。
2)内存溢出 out of memory :
就是内存不够了,当程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
关系:
1.一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
@see JAVA内存泄漏和内存溢出的区别和联系https://blog.csdn.net/mashuai720/article/details/79557670
2.栈内存分配
我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。
为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。
如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的 内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析
:就是分析对象动态作用域,当一个对象在方法中被定义后,判断其是否被外部方法所引用,例如作为调用参数传递到其他地方中。
public User test1() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
return user;
}
public void test2() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
}
很显然test1方法中的user对象被返回了,这个对象的作用域范围不确定,
test2方法中的user对象我们可以确定当方法结 束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内 存一起被回收掉。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配)。
JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
标量替换:
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,
JVM不会创建该对象
,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。
开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启。
标量与聚合量:
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。