一、jvm运行时内存区域划分:
看图说话,JVM大致分为五个区域:方法区、堆区、程序计数器、虚拟机栈和本地方法栈。其中方法区和堆区是线程共享,剩下三者则属于非线程共享。
1.1、程序计数器(线程私有,生命周期与线程相同)
一块较小的内存区域,用于存储当前线程指令的地址,通过改变程序计数器的值来执行下一条需要执行的指令。因为一个处理器同一时间只能处理一条线程种的指令。所以为了线程恢复后,线程能从正确的位置开始执行,必须要每条开辟一个独立的程序计数器记录。所以:该内存属于“线程私有”内存。
1.2、Java虚拟机栈(线程私有,生命周期与线程相同)
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的内存空间在编译器完成分配,当进入一个方法时这个方法需要在帧中分配多大的内存空间是完全确定的,运行期间不会改变局部变量表的大小。(64为长度的long和double会占用两个局部变量空间,其他的数据类型占用一个)
Java虚拟机栈可能出现两种类型的异常:1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。2.虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
1.3、本地方法栈(线程私有,生命周期与线程相同)
发挥的作用和虚拟机栈类似,只不过它是为虚拟机的Native方法服务。(一个Native方法就是一个java调用非java代码的接口。目的是:使用其它语言,调用与更底层进行直接交互。跳过中间步骤,提高程序的执行效率)
1.4、堆区(线程共享,生命周期与jvm存亡)
Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象内存不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。
Java堆是垃圾收集器管理的主要区域,所以也称为“GC堆”。由于现在的垃圾收集器基本上都是采用分代收集算法,所以Java堆还可细分为:新生代和老生代。在细致一点可分为Eden空间,From Survivor空间,To Survivor空间。如果从内存分配的角度看,线程共享的Java堆可划分出多个线程私有的分配缓冲区。
1.5、方法区(线程共享,生命周期与jvm存亡)
方法区也是线程共享的区域,用于存储已经被虚拟机加载的类信息,常量,静态变量和即时编译器(JIT)编译后的代码等数据。
1.6、运行时常量池
运行时常量池是方法区的一部分。CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
class文件中常量池的内容
二、各个区域的内存溢出问题
内存溢出理解:当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出OutOfMemoryError,这就是内存溢出。
2.1、程序计数器是jvm唯一一块区域不存在内存溢出问题的。
2.2、虚拟机栈和本地方法栈内存溢出:如果某线程请求的栈帧深度大于虚拟机允许的最大深度,将抛出StackOverflow异常。
2.3、方法区内存溢出:根据它存储的Class相关信息(类名、常量池、字段描述、方法描述)来看。当大量的类占用填满方法区时,自然会抛出OutOfMemoryError异常。
2.4、堆区内存溢出:当对象实例大量存在,并超出虚拟机设置对内存设置上限,自然会抛出OutOfMemoryError异常。
三、内存溢出产生的一个不可忽视原因——内存泄漏
3.1、内存泄漏理解:已经申请的内存,但无法被释放。这一部分内存空间就会一直占用着,当越来越多的内存泄漏,随之而来的就是内存空间不足,抛出OutOfMemoryError异常。
3.2、jvm对对象释放的处理算法:可达性分析算法。
这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
GC Root在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈的本地变量表中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
四、常见内存泄漏场景
4.1、静态集合类引起内存泄漏:
像HashMap、Vector等这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象也不能被释放,因为他们也将一直被Vector等引用着。
4.2、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4.3、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
4.4、单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏.
问答环节。
一、为什么需要了解jvm内存分配呢?平常码代码,我只要规范使用java手册,似乎问题照样能做好需求。
了解内存分配主要是为了,当出现内存泄漏和内存溢出时,javaer能更好的排查错误。也是为了能更好的提升java的性能,对其调优。规范是别人总结的经验,遵守它,能尽可能避免异常问题;好奇它,了解它,我们更是能在出现问题时,自己解决它。岂不美滋。
网友评论