Java内存区域
主要面试题:
介绍下Java内存区域
Java对象创建的步骤(五步,需要知道JVM分别做了什么)
对象访问定位的两种方式
不同的变量在Java内存区域中存放的位置(局部变量、静态变量、类的成员变量)
OOM异常
堆:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈:主要存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
方法区:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。
程序计数器作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
全局变量也就是类的成员变量在堆里
静态常量在方法区中
局部变量在虚拟机栈里
成员变量、类变量、局部变量的区别
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
内存溢出异常:
-
Java堆内存溢出
异常日志: java.lang.OutOfMemoryError: Java heap space
解决方法:首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在内存泄漏,也就是说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
内存泄漏与内存溢出
内存溢出:java.lang.OutOfMemoryError,是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。
产生原因
产生该错误的原因主要包括:
1.JVM内存过小。
2.程序不严密,产生了过多的垃圾。
内存泄漏:Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
1)首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
2)其次,这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
关于内存泄露的处理页就是提高程序的健壮型,因为内存泄露是纯代码层面的问题。
一个Java内存泄漏的排查案例
-
虚拟机栈和本地方法栈内存溢出
如果线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这两种异常存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
解决方法: 在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
在开发多线程应用的时候特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样 的,所以只能说大多数情况下)达到1000~2000完全没问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。 -
方法区内存溢出
异常日志: java.lang.OutOfMemoryError:PermGen space
解决方法:-XX:PermSize和-XX:MaxPermSize限制方法区的大小 -
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx)一样。虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
-
Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
1. 类加载检查
当JVM遇到一条new指令时,会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行类加载过程。
2. 分配内存
当在类加载完成之后,JVM就会为新生对象分配内存,对象所需内存的大小在类加载完成之后就可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有两种“指针碰撞”和“空闲列表”。选择哪种分配内存的方式由Java堆是否规整决定,而Java堆是否规整又由采用的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际的开发中,创建对象是很频繁的事情,对于虚拟机来说,必须要保证创建过程是线程安全的,通常来讲,虚拟机由两种方案来保证线程安全。
- CAS+失败重试:CAS是乐观锁的一种实现方式,所谓的乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS+失败重试的方式保证更新操作的原子性。
- TLAB:本地线程分配缓冲。为每一个线程在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB中分配,当对象大于TLAB中的剩余内存或者TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
3. 初始化零值
内存分配完成后,JVM需要将分配到的内存空间都初始化为零值(不包括对象头)
4. 设置对象头
对对象进行必要的设置
5. 执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
-
句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
-
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
HotSpot虚拟机使用的是直接指针的方式。