Java8内存结构
本文基于Jdk8
正文前先看一张图(来自美团2020年技术年货):
栈主要包括 程序计数器,本地方法栈(HotSpot和虚拟机栈合二为一)和虚拟机栈
程序计数器:
线程私有的一小块内存,可以看做当前线程执行的字节码行号指示器,为线程切换恢复到正确的执行位置,是Java虚拟机中唯一一个没有 OutofMemoryError 的地方
Java虚拟机栈 :
每个Java线程都有一个私有Java虚拟机栈,与该线程同时创建。 在虚拟机栈内,每个方法会生成一个栈帧,用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。一个方法的调用到执行完成的过程,代表栈帧从入栈到出栈的过程
局部变量存放 八大基本类型和对象引用(reference,可能是一个指向起始地址的引用指针,也可能是指向一个代表对象的句柄)和returnAddress类型(指向字节码指令的地址)
64位长度的long和double类型的数据会占用2个局部变量空间,其他类型只占用一个。局部变量所需要的内存在编译期间已经分配完成,在方法运行期间不会改变局部变量表的大小
如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError 异常
如果扩展时无法申请到足够的内存,抛出 OutofMemoryError 异常
本地方法栈
本地方法栈和虚拟机栈的作用相似,不过本地方法栈作用于Native方法,也会出现 StackOverflowError 和 OutofMemoryError 异常
Java堆
Java堆是所有线程共享的一块内存区域。几乎所有的对象实例都在堆上分配内存。但是JIT编译器的发展和逃逸分析技术成熟,栈上分配和标量替换优化技术会导致一些变化。
Java 堆 可以细分:新生代(Eden(TLAB)/From Survivor/To Survivor),老年代
堆内存不够时会出现 OutofMemoryError 异常
TLAB(Thread Local Allocation Buffer)
TLAB是一小块线程独享的内存区域,避免了对象分配时的竞争。TLAB占用的是eden区的空间。根据虚拟机参数 -XX:UseTLAB进行启用(默认启用)。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
TLAB通常很小,所以放不下大对象,大对象在Eden区分配内存
逃逸分析
逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针
JVM判断新创建的对象是否逃逸的依据有:
1、对象被赋值给堆中对象的字段和类的静态变量。因为对象呗放入堆中,其他线程可以访问,编译器无法再进行追踪对象使用情况
2、对象被传进了不确定的代码中去运行。因为编译器是不能事先完整知道这段代码会对对象做什么操作
确定对象不能逃逸时,则可以进行一些优化:
1.栈上分配:在new语句方法退出时,通过弹出当前方法的栈帧来自动回收分配的内存,就无需GC回收了,HotSpot采用标量替换代替了栈上分配
2.标量替换:标量指仅能存储一个值得变量,例如基本类型,相反可以存储多个值得变量为聚合量,编译器会将未逃逸的聚合量分解成多个标量
3.锁消除:不能逃逸,对该对象加锁,解锁就没有意义,因为线程不能获得该锁对象,次情况不多见
public static void main(String[] args) {
Test();
}
public static void Test(){
Write write = new Write();
Read read = new Read();
read.setWrite(write);
}
class Write {
}
class Read {
private Write write;
public void setWrite(Write write) {
this.write = write;
}
}
编译器可以通过逃逸分析
Read(
)对象不会逃出Test()
方法的调用,则Write()
对象也不能,因此编译器可以安全的在栈上分配两个对象
@AllArgsConstructor
class Hxx {
String sex;
int age;
}
public void lzx() {
Hxx hxx = new Hxx("n", 30);
test(hxx.age, hxx.sex);
}
进过逃逸分析
Hxx()
未能逃出lzx()
的调用,因此可以将聚合量Hxx()
替换成两个标量set和age
@AllArgsConstructor
class Hxx {
String sex;
int age;
}
public void lzx() {
int age = 30;
String sex = "n";
test(age, sex);
}
MeteSpace :
Metaspace用来取代Java8之前的方法区(永久代为HotSpot对方法区的实现)。方法区在启动时大小就已经确定,对一些动态类信息加载容易出现OOM异常,而且GC效率也低。
每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。 当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收
Metaspace由两大部分:Klass Metaspace和NoKlass Metaspace
堆外内存 :
堆外内存占用的是本地内存