JVM 内存分配
本笔记基于《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》及部分在线博客整理。
JVM:java virtual machine,一个 java 程序(进程)拥有一个 jvm 实例
内存
JVM 区域总体分两类,heap 区和非 heap 区:
heap 区:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代 - 养老区)。
非 heap 区:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(Java 虚拟机栈)、Local Method Statck(本地方 法栈)。
内存划分
1.head
堆,所有线程共享,存放所有对象实例、数组,GC 主要场所,会 OOM
分类
1.新生代
eden 刚刚创建的对象优先
s1 经历几次 GC
s2 经历几次 GC
2.老年代
存活时间长的老年对象
大对象,如数组,大 String...
2.stack
栈,线程私有,存放基本数据和对象的引用,LIFO,会 OOM,StackOverflow
java virtual machine stack
线程请求的栈深度大于 JVM 允许的深度会导致 Stack Overflow
在编译期完成内存分配,如果虚拟机栈可以动态扩展,但是当拓展时无法申请到足够内存时会导致 OutOfMemory
stack frame
stack frame:栈帧,每执行一个方法就会产生一个栈帧并压入栈中
局部变量表
- 基本数据类型
- 对象引用
- returnAddress 类型,指向了一条字节码指令的位置
操作数栈
动态链接
方法出口等
native method stack
与 java 虚拟机栈作用类似,不过 native method stack 是为 native 方法服务。
jvm 可以自由实现它,甚至在 sun HotSpot VM 中将他与虚拟机栈合并
会 OOM,stackOverflow
3.method area
方法区,线程共享,存放类信息,常量,静态变量,即时编译器编译后的代码,会 OOM
运行时常量池
类加载后,编译器生成的各种字面量和符号引用会放到方法区的运行时常量池中,会 OOMString.intern()
,有该 string 对象则返回,无则创建并返回
String.intern()
方法的注意事项:JDK1.6 及以下:将首次出现的对象实例复制到永久代,返回其引用
JDK1.7 及以上:只会记录下首次出现的实例的引用,返回其引用
所以:
String s2 = "java"; System.out.println(s2.intern() == s2);
在 JDK1.6 及以下输出
false
,在 JDK1.7 及以上输出true
此外,由于
String
类是final
的,每次new String("str")
会产生两个对象:一个是字符串str
本身,一个是值为str
的字符串。
以String s = "Hello";
为例,解释几个概念:
字面量 源码中表示具体的值,如Hello
符号引用 用来指代某种值得符号,如s
直接引用 可以定位到内存中的(类、对象、方法、变量)等的具体地址
4.program count
程序计数器,线程私有,占用内存小,当做当前线程执行字节码的行号指示器。
若执行 java 方法,计数器记录的是正在执行的虚拟机字节码指令的位置
若执行的是 native 方法,则计数器为空 undefined。
此内存区域是唯一一个在 java 虚拟机规范字没有规定任何 OOMError 的区域
内存溢出
以 Sun HotSpot VM 为例
1.java 堆溢出
对象过多导致 head 内存溢出
- 是内存泄漏 memory leak,定位泄露对象
- 是内存溢出 memory overflow,检查虚拟机堆参数是否可以调大;去除非必须的生命周期长的对象
2.虚拟机栈和本地方法栈溢出
单线程,Stack Overflow
单线程下,栈帧过大或者虚拟机栈容量太小,当内存无法分配时都会导致 Stack Overflow 异常
多线程,
多线程时,每个线程栈分配的内存越大,越容易尝试内存溢出 OOM
原因:虚拟机最大内存一定的情况下,去掉共享的 Head 和 MethodArea 占的内存,剩下的内存/单个线程最大栈内存=最大线程数量,当单个线程最大栈内存增加时,可以产生的线程数就会越少
3.运行时常量池溢出
运行时常量池属于方法区,当常量过多时会导致 OOM,可以用 String.intern() 方法尝试
4.方法区溢出
经常动态生成大量 Class 的应用,如 Spring 等框架,需要注意 OOM
5.本地直接内存溢出
原生方法直接操作物理内存时导致物理内存不够,产生 OOM
GC 垃圾回收
JVM 中 GC 会根据不同情况采取以下一系列算法组合进行内存回收
回收算法
1.复制算法
原理:内存一分为二,只使用一半;GC 时将存活对象复制到另一半内存,剩下的则清空
优缺点:1.无 STW,但不适合对象过多的情况;2.内存利用效率低
2.标记清除法
原理:从 GC Roots 开始遍历,可达标记存活,不可达则未标记
java 中,GC Roots 可以是以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
优缺点:1.要 StopTheWorld 防止标记的时候新 new 的对象未被标记而出错;
2.清除对象后内存不连续,会有一定的浪费
3.标记压缩法
原理:类似【标记清除法】,但会对标记进行压缩,如 a->b->c,会被压缩为 a->c,具体试讲所有存活的对象都向一端移动,直接清理掉端边界外的内存
优缺点:1.也要 StopTheWorld
4.引用计数算法
原理:引用 +1,不引用 -1,为 0 则删除,但是会有相互循环引用的问题,java 未使用
优缺点:相互循环使用:
a = b
b = a
除此之外再没有用到 a,b 的地方,但是由于 a,b 的引用不为 0 所以无法被回收,导致内存浪费
回收过程
一个不可达对象在“死缓”到“执行死刑”前至少经历两个标记过程
第一次标记,筛选:是否有必要执行 finalize() 方法,若是则放到 F-Queue 队列中【触发】该方法,但不保证执行完该方法。
可以在 finlize() 方法中自救一次:在该方法中将自身 this赋值给其他变量,这样在第二次标记时会被移出即将回收集合;但是由于 finlize() 方法只会被调用一次,所以只能自救一次。并不推荐该方法,该方法所有可以做的工作,可以用try...finally或者其他方法更好的实现
第二次标记,若 finalize() 方法以及调用过,或者为重写该方法,则“没必要执行”,可以回收
对象
对象引用
强引用 StrongReference
People p = new People();哪怕抛出 OOM 也不会被 GC 回收的对象
软引用 SoftReference
SoftReference sf = new SoftReference(p);只要有足够内存就不会被 GC 回收,若内存不够则会被 GC 回收,常用作服务器缓存
弱引用 WeakReference
在下次 GC 回收之前都存在,用作 android 等内存紧张的设备中的缓存
虚引用 PhantomReference
无法影响其生存时间,也无法通过虚引用获取其实例,设置虚引用只是为了在对象被 GC 回收时获取系统通知