文章目录
- 前言
- 一、程序计数器(PC寄存器)
- 程序计数器的作用
- 程序计数器的特点
- 二、Java虚拟机栈
- 定义
- 压栈和出栈过程
- 局部变量表
- 操作数栈
- 方法的调用
- 静态链接
- 动态链接
- 方法绑定
- Java 虚拟机栈的特点
- 三、本地方法栈
- 四、堆
- 堆的特点
- 新生代于老年代
- 对象的分配过程
- Minor GC、Major GC、Full GC的区别
- 定义
- 触发机制
- 逃逸分析
- TLAB(Thread local allocation buffer)
- 四种引用方式
- 方法区
- 方法区定义
- 方法区特点
- 运行时常量池
- 总结
前言
文章为温习Java虚拟机所作笔记。
Java虚拟机的内存空间分为五部分:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 堆
- 方法区
一、程序计数器(PC寄存器)
程序计数器是一块较小的内存空间,是存储当前线程所执行的字节码指令地址,如果线程执行的是本地方法(C/C++方法)那么程序计数器为未定义
程序计数器的作用- 能够让字节码解释器改变程序计数器来依次读取指令,也就是代码的流程控制。
- 在多线程的情况下,程序计数器就是记录当前线程执行的位置(可以理解为执行到哪个方法了),假设当前线程为A,那么当从线程B切换回A时,现在A就可以通过程序计数器知道上次执行到哪里了。
- 是一块较小的内存空间;
- 生命周期:随着线程创建而创建,随着线程销毁而销毁;
- 线程私有,每个线程都有一个程序计数器;
- 唯一一个不会出现OutOfMemoryError错误的内存空间。
Java虚拟机栈是描述Java方法运行过程的内存模型
定义会每一个即将运行的方法创建一个栈帧区域,用于存放方法运行过程中的一些信息,如:
- 局部变量表;
- 操作数栈;
- 动态链接;
- 方法出口信息等。
- 在方法执行过着需要创建局部变量时,将局部变量存入栈中局部变量表中。
- Java虚拟机栈的栈顶的栈帧是正在执行的活动栈帧,也就是当前执行的方法,PC寄存器也会指向这个地址,只有活动栈帧的本地变量可以被操作数栈使用,如果活动栈帧中调用另一个方法,那么与之对于的栈帧会被创建并被压入栈中(栈顶),成为新的活动栈。
- 方法结束这个栈被移出,这个栈帧的返回值会变成新活动栈中操作数栈的一个操作数,如果栈帧没有返回值则,新的活动栈中操作数栈的操作数没有变化
局部变量表被定义为一个数组,主要存储方法的参数,对象的引用,定义在方法体内部的局部变量,各类基本数据类型,和返回地址类型
局部变量表的大小是在编译器确定下来的,基本储存单元是slot,32位占用一个,62位占用两个,
- Java虚拟机栈会为每个slot分配一个索引,通过所以可以访问到局部变量表中指定的局部变量值;
- 如果栈帧是一个实例方法或构造方法,那么局部变量表中索引为0的slot会存放这个对象的this地址,其他参数按顺序存放
- 局部变量表是可以复用的,如果一个局部变量过了作用域,那么新的局部变量会用这个slot,目的就是节省资源
- 在栈帧中与性能调优最密切相关的就是局部变量表,局部变量是重要的垃圾回收根节点,只要被局部变量直接或间接引用的对象都不会被回收
操作数组是存储在内存中的,频繁存储读写会影响性能,他会有一个明确的深度在编译器就会被确定,详细了解可以自行百度
方法的调用主要由以下三个点组成:
静态链接字节码文件被加载进jvm内部的时候,如果被调用的方法在编译器可以知道,并且被调用方法在运行的时候不变,就可以将调用方的符号引用改为直接引用的过程称为静态链接,因为他是不变的
动态链接如上,字节码文件被加载进jvm内部的时候,被调用的方法没办法在编译器知道,只能在运行时将调用方的符号引用改为直接引用的过程叫动态链接,因为运行时才知道这个过程是动态的
方法绑定- 早期绑定
被调用目标方法在编译器可知,且运行不变。静态链接属于这类 - 晚期绑定
被调用的目标方法在编译器不可知,只能在运行时根据实际类型绑定相关方法,动态链接属于这类
- 运行速度非常快仅次于PC寄存器
- 局部变量表随着栈帧的创建而创建,在编译器确定大小,创建时只需要分配在编译器确定的大小即可,在方法运行过程中局部变量表的大小不会改变
- Java虚拟机栈有两种异常:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError :若Java虚拟机大小不允许动态拓展,则当前线程请求的深度超过当前Java虚拟机栈的最大深度时,抛出这个异常,不停递归或调用方法超过Java虚拟机栈规定的深度会抛出该异常。
OutOfMemoryError :若运行动态拓展,当前线程请求栈内存用完了,无法在动态拓展就会抛出OutOfMemoryError 异常。
不允许动态拓展时超过Java虚拟机栈最大深度就会抛出StackOverFlowError 异常,允许动态拓展但当前线程分配的栈内存用完了会抛出OutOfMemoryError 异常。 - Java虚拟机栈是线程私有的,随着线程创建而创建,线程结束而销毁。
- 出现StackOverFlowError 异常时,内存可能还有很多,因为只是超过虚拟机栈规定深度而不一定是内存不足。
常见的运行时异常有:
- NullPointerException - 空指针引用异常
- ClassCastException - 类型强制转换异
- IllegalArgumentException - 传递非法参数异常
- ArithmeticException - 算术运算异常
- ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
- IndexOutOfBoundsException - 下标越界异常
- NegativeArraySizeException - 创建一个大小为负数的数组错误异常
- NumberFormatException - 数字格式异常
- SecurityException - 安全异常
- UnsupportedOperationException - 不支持的操作异常
本地方法栈是JVM为执行本地方法准备的空间,由于很多Native方法都是由C语言实现的,通常又叫C栈,它于Java虚拟机栈实现的功能类似,只不过是描述本地方法运行过程的内存模型。
本地方法执行,也会在本地方法栈创建一块栈帧,用于存放该方法的局部变量、操作数栈、动态链接、静态链接、方法出口信息等,方法执行结束后相应的方法也会出栈,并释放内存空间,也会抛出StackOverFlowError 和 OutOfMemoryError异常。
堆是用来存放对象的,几乎所有的对象都存在堆中。
堆的特点- 线程共享的,整个Java虚拟机只有一个堆,所有线程都访问同一个堆,而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个。
- 堆在虚拟机启动时创建,是垃圾回收的主要场所。
- 堆可分为新生代和老年代
- Java虚拟机也规定,堆可以处于物理上不连续的内存空间,但逻辑上应该当作连续的。
新生代于老年代Java堆使用的内存不需要保证连续性,但是由于堆是被线程共享的,所以访问的时候需要注意同步的问题,方法和属性都要保持一致性。
- 老年代比新生代生命周期要长
- 新生代与老年代默认空间比例是1:2,新生代占1、老年代占2,新生代占整个堆的1/3。
- 新生代中Eden 空间和另外两个 Survivor 空间缺省所占的比例是:8:1:1。
- 几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不了的大对象,就直接进入老年代了。
这部分描述的不够详细,可以自行百度查看更详尽的回收流程
- 创建的对象先放在eden区,大小有限制。
- 创建新对象时Eden区满了会触发Minor GC,将Eden区不在被其他对象引用的对象进行销毁,在加载新- 的对象到Eden区,这里要注意的是Survivor区满了不会触发Minor GC,而是Eden空间满了,Minor Gc才顺便清理Survivor0区。
- 将上述Eden 中剩余的对象移到 Survivor0 区
- 再次触发垃圾回收,上次Survivor下来的,在Survivor0 区,如果没有回收,会被复制到Survivor1区
- 再次经历垃圾回收,又回将幸存者放到Survivor0 区,以此类推
Minor GC:清理年轻代空间(包括Eden和survivor)释放在Eden区域中不活跃的对象,若释放后Eden空间还是放不下新对象就会将活跃的对象放入survivor区,survivor作为Eden和老年代的中间交换区域,当老年代空间足够时,在survivor区被循环超过15次的对象将会放入老年代中。survivor1向survivor2复制时对象大小大于survivor2的可用内存,则直接把该对象转入老年代中。
Majro GC:清理老年代空间,当老年代空间不足时,JVM会在老年代进行Major GC。
Full GC: 清理整个堆空间包括年轻代和老年代。
触发机制Minor GC:年轻代满时会触发,指的是Eden区满会触发,survivor满不会触发。
Full GC:
- 老年代空间不足
- 方法区空间不足
- 通过minor GC 进入老年代的平均大小>老年代的可用内存会触发
逃逸分析(从JDK1.7开始默认开启)
Java中的对象都是在堆中分配吗?说明为什么!
逃逸分析就是 分析Java对象的动态作用域
一个对象被定义之后,如果能被外部对象引用,就称之为方法逃逸,如果被其他线程引用称之为线程逃逸
例如下面这段代码,s是一个内部变量,直接将s返回这个StringBuffer 对象可能在其他地方被修改导致作用域就不单单是在方法内部 ,就算是一个局部变量,但是逃逸到了方法外,这就是所谓的方法逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer s = new StringBuffer();
s.append(s1);
s.append(s2);
return s;
}
修改后:
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer s = new StringBuffer();
s.append(s1);
s.append(s2);
return s.toString;
}
使用逃逸分析,编译器可以对代码做如下优化:
- 同步省略:如果一个对象被发现只能从一个线程访问,对于这个对象的操作可以不考虑同步问题。
- 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
在编译期间,如果经过JIT逃逸分析,发现对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。
TLAB(Thread local allocation buffer)中文意思是:线程本地分配缓存区
- 是属于Eden区的,是线程专用的内存分配区域,线程私有,默认开启(不是绝对,看虚拟机类型)占用Eden区域的1%,大对象无法分配给TLAB。
- 堆是全局共享的,同一时间,可能会有很多线程在堆上申请空间,这种情况下要加锁处理,效率自然降下来了
- 所以使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用直接的TLAB,可以使线程同步,提高分配效率。
- 并不是所有的对象都可以在TLAB分配内存成功。
- TLAB中内存不足时,按照比例值处理,当TLAB小于比例值时,放弃当前TLAB重建一个来分配,默认比例是1/64,收到分配请求后TLAB如果没有足够的空间,就判断是否小于1/64,则重新创建TLAB。
- 如果重新创建的TLAB空间还是放不下这个对象,则直接去Eden创建。
- 强引用:创建一个对象,赋值给一个引用变量,普通new出来的对象都是强引用,有引用变量指向时永远不会被垃圾回收,即使抛出oom,解决办法是将引用赋值为null,这个时候指向的对象就会被垃圾回收
- 软引用:如果一个对象具有软引用,内存空间足够不会回收,内存空间不够就会回收,只有没有被回收就可以被使用。
- 弱引用:非必须对象,jvm垃圾回收时,无论内存是否充足都会回收弱引用关联的对象
- 虚引用:并不会觉得对象的生命周期,如果一个对象持有虚引用,跟没有引用一样,随时可能会被回收
Java虚拟机规范中定义方法区是堆的一个逻辑部分
方法区存放以下信息:
- 已经被虚拟机加载的类信息
- 常量
- 静态变量
- 即时编译后的代码
- 线程共享:方法区是堆的一个逻辑部分,因此和堆一样被线程共享。整个虚拟机只有一个方法区
- 永久代:方法区的信息一般是长期存在,又是堆的逻辑分区,在堆中被划分为永久代
- 内存回收效率低,在方法区中信息一般是长期存在,回收一遍后可能只有少量无效信息,主要回收目标是对常量和类型的卸载。
- Java虚拟机对方法区的规定比较松,和堆一样运行固定大小和动态拓展,也允许不实现垃圾回收
方法区中存放:类信息,常量,静态变量,即时编译后的代码,常量存放在常量池中。当类被java虚拟机加载后.class文件中的常量就存放在方法区的运行时常量池中,在运行时可以向常量池添加新的常量,String 类的 intern() 方法就可以向常量池添加字符串常量
总结到这里文章已经结束了,主要结合其他文章说明了自身对java虚拟机的五个部分的理解。
参考文章如下:
参考:Java中的逃逸分析,通俗易理解
参考:【JVM】 TLAB到底是干什么的
参考:JVM 底层原理最全知识总结
参考:Minor GC、Major GC和Full GC之间的区别



