文章目录
Java内存区域与内存溢出异常
运行时数据区域
程序计数器(线程私有的)
程序计数器:是一块比较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
由于Java虚拟机的多线程是通过线程轮转并分配处理器执行时间的方式来实现的。为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,每个线程的计数器互不影响,独立存储
程序计数器是唯一一块不会发生OutOfMenoryError(内存溢出)的区域
Java虚拟机栈(线程私有的)
生命周期与线程相同
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧
栈帧存储 局部变量表、操作数栈、动态链接、方法出口
tip:局部变量表所需的空间是完全确定的,在进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常
- 如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈(线程私有的)
本地方法栈(Native Method Stack)与虚拟机栈所发挥作用是非常相似的,他们的区别不过是
虚拟机栈为虚拟机执行Java方法(字节码)服务
本地方法栈则为虚拟机使用到的Native方法服务
同样也会抛出 StackOverflowError、OutOfMemoryError异常
Java堆(线程共享)
Java堆(Java Head)是Java虚拟机所管理的内存最大的一块
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
几乎所有的对象都堆中分配
Java堆唯一的目的就是存放对象实例
Java堆是垃圾收集器管理的主要区域(也称"GC堆")
现在收集器基本采用分代收集算法
Java堆:新生代和老年代 Eden空间、From Survivor空间、To Survivor空间
从内存分配角度:线程共享的Java堆可能划分出多个线程私有的分配缓冲区,
划分目的:更好的回收内存,或者更快的分配内存
tip:Java堆可以处于物理上不连续的内存空间,只要逻辑是连续的即可,在实现时,既可以显示成固定大小的,也可以是扩展的(通过 -Xms和-Xms控制),如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出cc异常
方法区(线程共享)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码
JDK1.7以前称之为“永久代” JDK1.8 元空间
tip:永久代有 -XX:MaxPermSize的上限,容易遇到内存溢出的问题,当方法区语法满足内存分配的需求是,将会抛出OutOfMemoryError异常
运行时常量池
运行时常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口、等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
tip:运行时常量池是方法区的一部分,自然收到方法区的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常
直接内存(本地内存)
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域,但是这部分内存也被频繁的使用,也会导致OutOfMemoryError异常
NIO基于通道(channel)与缓冲区(Buffer)的I/O方法,直接使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,避免了Java堆和Native堆中来回复制数据
本机直接内存不会收到Java堆大小的限制,但是收到本机总内存的限制,从而导致动态扩展时出现OutOfMemoryError异常
深入分析栈和堆
功能
- 以栈帧的方法存储调用的过程,并存储方法调用过程中基本数据类型的变量(int,short、long、byte、float、double、boobean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放
- 堆内存用来存储Java对象,无论是成员变量、局部变量、还是类变量,它们指向的对象都存储在堆内存中
线程共享还是线程私有
- 栈内存归属于单个线程,每个线程都有一个栈内存,其存储的变量只能在其所属的线程中可见,即栈内存可以理解成线程私有的内存
- 堆内存的对象对所有线程可见,堆内存中的对象可以被所有线程访问
空间大小
- 栈内存远远小于堆内存,栈的深度是有线程的,可能发生StackOverFlowError错误
HotSpot虚拟机对象探秘
对象的创建
虚拟机在遇到一条new指令,首先将去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用的类是否已被加载、解析和初始化。如果没有,那必须执行相应的类加载过程
[外链图片转存失败(img-jfIri5C3-1564728711177)(C:\Users\thinkpad\Desktop\对象创建.jpg)]
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分成3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向 对象头的另一部分就是类型指针,即对象指向它类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
tip:并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查询对象的元数据信息并不一定要经过对象本身,如果对象是一个Java数组,那在对象头还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象大小,但是从数组的元数据却无法确定数组的大小
实例数据
实例数据部分是对象真正存储的有效数据,也是在程序代码中所定义的各种类型的字段内容。
tip:HotSpot虚拟机默认的分配策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是分配到一起
对齐填充
对齐填充并不是必然存在的,也没有什么特别的含义,它仅仅只是启着占位符的作用,由于HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍,因此当对象实例数据没有对齐时,就需要通过对齐填充来补全
内存溢出和内存泄露
内存溢出
内存溢出 out of memory :指程序在申请内存时,没有足够的内存空间供气使用,出现out of memory
内存泄露
内存泄露 memory leak :指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露可以忽略,但是内存泄露堆积的后果很严重,无论多少内存,最终都会被耗尽
memory leak会最终导致 out of memory
内存泄露按发生的方式可以分为4类
- 常发性内存泄露:发生内存泄露的代码会被多次执行到,每次被执行的时候都会导致一块内存泄露
- 偶发性内存泄露:发生内存泄露的代码只会在某些特定的环境或者操作过程下才会发生,常发性和偶然性是相对的。对于特定的环境,偶发性 的业也许就编程常发性,的,所以测试环境和测试方法对检测内存泄露至关重要
- 一次性内存泄露:发生内存泄露的代码只会执行一次,或者由于算法上的缺陷,导致总会有一块且仅且一块内存发生泄露,比如,在类的构造函数中分配内存,在析构造函数却没有释放改内存,所以内存泄露只会发生一次
- 隐式内存泄露:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存,严格来说这里并没有发生内存泄露,因为最终程序释放所有申请的内存,但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存,也可能导致最终耗尽系统的所有内存,所有,我们称这类内存泄露为隐式内存泄露
从用户使用程序的角度来看,内存泄露本身不会产生设么危害,作为一般的用户,根本感觉不到内存泄露。真正有危害的是内存泄露的堆积这会最终耗尽系统所有的内存,从这个角度来说,一次性内存泄露并没有什么危害,因为它不会堆积,而隐式内存泄露危害性则非常大,因为较之常发性和偶发性内存泄露它更难被检测到
Java 内存回收机制
不论那种鱼呀的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针的内存块的首地址,Java中的对象都是采用new或者反射的方法创建的,这些对象的创建都是在堆(Head)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的,GC为了正确释放对象,会监控每个对象的运行状态,对他们的申请,引用、被引用、赋值等状态进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以到达,如果不可到达,则就将其回收
Java内存泄露引起的原因
内存泄露是指无用对象(不再使用的对象)持续占有内存或者无用对象的内存得不到及时的释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道内存泄露,但有时也会很严重,会提示你 Out of memory(内存泄露的最终结果out of memory,导致程序不可用)
引起Java内存泄露的根本原因是什么?
长生命周期的对象持有短生命周期的易用就很可能发生内存泄露,尽管短生命周期对象以及不在需要了,但是长声明周期的对象持有它的引用,而导致不能被回收,
静态集合类引起的内存泄露
像HashMap、Vector等使用最容易发生内存泄露,这些静态变量的声明周期和应用程序一样,他们所引用的所有对象Object也不能被释放,因为他们也将一直被Vector等引用着
Static Vector v=new Vector(10); for(int i=0;i<100;i++){ Object o=new Object(); v.add(o); o=null; }
在这个例子中,需要申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放资源(Object对象)本身(o=null),那么Vector集合仍然引用该对象,所有这个对象对GC来说是不可回收的,因此,如果对象加入到Vector后,还必须将从Vector中删除,最简单的方法就是将Vector对象设置为null
当集合里面的对象属性被修改后,再调用remove()方法时不起作用
public static void main(String[] args) Set<Person> set = new HashSet<Person>(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孙悟空","pwd2",26); Person p3 = new Person("猪八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉,造成内存泄漏 set.add(p3); //重新添加,居然添加成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set) { System.out.pritln(person); } }
监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会
各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC
回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection
在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement
对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement
对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement
对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。单例模式
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露