运行时数据区
- 堆:存放java对象实例,GC回收的地方,也称GC堆
- 方法区:堆的逻辑部分,存放静态变量,常量,类信息
- 虚拟机栈:存放运行时的java方法,局部变量,(栈帧形式)
- 本地方法栈:类似虚拟机栈,服务的时native方法
- 程序计数器:线程切换,异常处理,指示下一行执行的字节码序号
虚拟机栈:
栈的FILO符合java方法间的调用
栈帧:
每个方法在执行的时候会创建一个栈帧,栈帧是虚拟机栈的栈元素,是虚拟机用于进行方法调用和方法执行的数据结构;
- 操作数栈:字节码指令的写入和读取,用于运算和参数传递
- 局部变量表:用于存放参数和方法内部定义的局部变量
- 动态连接:
没看懂
- 方法出口:方法返回的字节码指令,返回到方法被调用的位置;
当一个方法被执行时,会创建一个栈帧(当前栈帧),这个栈帧处于栈顶,在编译时期,栈帧中需要的操作数栈和局部变量表的大小已经完全确定,栈帧的内存不会在运行时受到影响;
- 方法中的参数和局部变量会存放到局部变量表中,当这个方法是非static方法时,局部变量表中的第一个数据是这个方法的实例对象;如果局部变量是一个对象实例,就会在局部变量表中以
reference引用
出现,上图的obj就是一个reference引用,它的真是指向是堆中的Object对象; - 操作数栈:先将num入栈,再将1入栈,取出栈顶的两个元素,相加再入栈,此时栈中只剩下num+1了;再出栈,将num+1这个数赋值给局部变量表中的num;上面的操作就是
num = num + 1
的实现;
动态连接:
-
将虚方法的符号引用转为直接引用:
在类加载的解析阶段,会将运行时常量池的符号引用转换为直接引用,这个这个操作叫做静态解析
,但是它只会解析非虚方法(静态方法,私有方法,构造方法,final修饰方法)
的符号引用,而栈帧中的动态连接
就是在运行时将其余的符号引用转换为直接引用; -
分派(静态分派,动态分派):选择
多态
的执行版本
Person person = new Man();
在上述代码中,Person类是person对象的静态类型
,Man类是person对象的实际类型
,静态类型和实际类型都可以发生改变,通过强转改变静态类型,重新new一个对象改变实际类型,所以静态类型的确定是在编译期,实际类型的确定在代码运行时;
重载
是通过静态类型
来判断执行版本,这个称为静态分派
重写
是通过实际类型
来判断执行版本,这个称为动态分派
栈帧数据的通信:
方法和方法之间的数据交互是通过使用同一块内存完成的,即栈帧A的本地变量表可能是栈帧B的操作数栈,同一块内存对于不同的栈帧是不同的角色;
虚拟机对于栈的优化:方法内联
方法A调用方法B,将方法B的代码作为方法A的一部分来执行,从而避免创建栈帧,减少栈内存的消耗
深入理解Java虚拟机插图类加载过程
上面的类加载过程中,加载,验证,准备,初始化,卸载,这五个步骤的顺序是确定的,必须按照这个顺序进行,解析阶段在一些情况下可以在初始化之后进行;
加载:
- 通过类的全限定名获取定义这个类的二进制字节流
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个这个类的class对象,作为方法区的访问入口;
获取二进制字节流这一步是开发者在类加载中可控性最强的阶段(通过自定义classLoader)
验证(连接第一步):
目的:确保Class文件的字节流符合当前虚拟机的要求,不会对虚拟机的安全产生威胁;class字节码文件不一定通过.java源文件编译来,(动态代理原理中是通过特定接口生成,还可以通过网络<Applet>,其他文件<JSP>等方式生成class字节码),所以,通过其他途径生成的字节码文件,可以做java语言做不到的事情,这些可能导致系统的奔溃;
验证内容:
- 文件格式验证:字节流是否符合Class文件格式规范
- 元数据验证:字节码内容的元数据信息是否符合java语言规范
- 字节码验证:校验类的方法体在运行是会不会危害虚拟机
- 符号引用验证:对类自身以外的信息校验
准备(连接第二步):
为类的静态变量
分配内存(方法区
),并且初始化静态变量为0/false/null;
注意:这里是将static变量分配到方法区,而不是实例变量
,实例变量是在对象创建的时候分配在堆
中的;
解析(连接第三步):
将常量池中的符号引用转为直接引用
-
符号引用:一组描述引用目标的符号,存放在Class文件的常量池中,符号引用和虚拟机实现的内存布局无关,如果不经过转换无法得到真正的内存地址;
-
直接引用:能直接指向目标的指针或间接定位到目标的句柄,直接引用和虚拟机实现的内存布局相关,能直接找到内存中的目标;
https://cloud.tencent.com/developer/article/1450501
常量池种类:
-
class文件常量池:
编译后,class常量池存放 字面量 和 符号引用 -
全局字符串常量池:
类加载阶段,在堆中创建字符串对象,对象的真实引用放入全局常量池,1.7以后,全局字符串常量池被移动到了堆内; -
运行时常量池:
类加载阶段(二进制字节码代表的静态数据结构转为方法区的运行时数据结构),将class常量池的符号引用放入运行时常量池, 解析阶段,将运行时常量池的符号引用转为和全局常量池一致的直接引用 基本类型包装类对象常量池:
初始化:
执行类构造器clinit()
,初始化类变量和其他资源,在准备阶段,我们为类变量在方法区分配了内存,并且将类变量置为0,false,/null ,在初始化阶段会将类变量设置为代码中的实际值,如果类中有静态代码块也会在这一阶段执行;
注意:
- clinit()是由编译器自动收集的所有类变量赋值行为和静态代码块中的语句合并产生;
- 虚拟机保证子类的clinit()执行前,一定会先执行父类的clinit(),所以第一个执行的clinit()一定是Object类的;
- 如果一个类没有赋值操作和静态代码块,那就没有必要执行clinit(),虚拟机就不会执行
- 接口执行clinit()前不需要执行父类的clinit(),接口的实现类在初始化时也不会执行接口的clinit()
- 虚拟机保证clinit()执行时的线程安全(加锁,其他线程阻塞,释放锁)
对象创建过程
- 设置对象:对对象进行必要的设置,设置HashCode,GC分代年龄等
- init方法:初始化实例变量的值(代码中设置的值)
对象内存布局
- 对其填充其实是一个占位符没有特别的意义,对象大小必须是8字节的整数倍,通过对其填充来确保是8的整数倍;
对象访问
句柄访问:
直接访问:
可达性分析法——判断对象是否需要回收
可达性分析法:
- 可达性分析:如果一个对象到GCRoot对象没有引用链,则表明不可达;
GCRoot对象:
-
Java虚拟机栈
中的引用对象 -
本地方法栈
中JNI(Native)引用的对象 - 方法区中引用的
常量
,静态属性
对象
public class OOM {
private int rootConstant = 1000; // 方法区常量对象GCRoot
private static Object rootStatic = new Object(); // 方法区静态对象GCRoot
private Object nonRoot = new Object(); // 类实例对象,非GCRoot
public void test (){
Object rootJavaStack = new Object(); // 虚拟机栈引用的对象GCRoot
// 可达,不会回收
Object obj0 = rootConstant;
Object obj1 = rootStatic;
Object obj2 = rootJavaStack;
// 不可达,回收
Object obj3 = nonRoot;
}
}
Java引用类型:
- 强引用 StrongReference:普通引用
- 软引用 SoftReference:内存不足时会回收软引用对象(展示图片使用内存)
- 弱引用 WeakReference:放生GC时会回收弱引用对象(ThreadLocal的Entry使用内存)
- 虚引用 PhantomReference:最弱的引用(日常开发用不到)
生存还是死亡:
其实在被认定了不可达以后,还需要经过两次标记筛选;
- 一次标记:如果一个对象到GCRoot没有引用,则需要进行一次标记:是否有必要执行finalize(),当对象没有覆盖finalize()或者finalize()已经被虚拟机执行过,则表明没有必要执行;
- 二次标记:如果对象有必要执行finalize(),则进行二次标记:将对象放入一个F-Queue队列,虚拟机创建一个优先级特别低的线程Finalizer执行队列的对象的finalize方法,在执行前会再次判断这个对象是否有到GCroot的引用链,如果还没有,则真正回收;
垃圾回收算法
复制算法:
复制算法特点:
- 只能使用一半内存
- 没有内存碎片
- 需要复制内存
标记清除:
标记清除算法特点:
- 效率低
- 会出现内存碎片
标记整理:
标记整理算法特点:
- 步骤较多
- 不会出现内存碎片
分代收集算法:
将内存分为新时代和老年代,新生代再分为Eden和Survivor(from to)区
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代:在Eden区中经过一次Minor GC仍然存活就转移到Survivor区,在对象头中存放的GC年龄+1,每经过一次Minor GC,From和To就会发生一次复制,对象的年龄就会+1,默认情况下,一个对象经过15此Minor GC仍然存活就移动到老年代;
- 动态对象年龄判定:如果在Survivor区中的同一年龄的对象的内存和大小大于Survivor(From/To)内存的一半,就直接移动到老年代;
GC发生的条件:空间不够(Eden/老年代满了,继续放对象时,会触发GC)
堆 | 收集行为 | 算法 | 特点 | 发生时刻 |
---|---|---|---|---|
新生代 | Minor GC | 复制算法 | 回收频率高,对象存活率低 | Eden区无可分配内存 |
老年代 | Full GC | 标记整理/ 标记清除 | 回收频率低,对象存活率高 | 老年代无可分配内存 |
常见的七种垃圾收集器垃圾收集器()
吞吐量 = 业务线程运行时间/总CPU时间
垃圾收集器 | 收集器类型 | 收集对象 | 算法 |
---|---|---|---|
Serial | 单线程 | 新生代 | 复制 |
ParNew | 并行的多线程 | 新生代 | 复制 |
Parallel Scavenge | 并行的多线程 | 新生代 | 复制 |
Serial Old | 单线程 | 老年代 | 标记整理 |
Parallel Old | 并行的多线程 | 老年代 | 标记整理 |
CMS |
并行并发的收集器 | 老年代 | 标记清除 |
G1 |
并发并行的收集器 | 新生代&老年代 | 复制+标记整理 |
CMS垃圾收集器:Concurrent Mark Sweep
步骤:
- 初始标记
Stop the world
- 并发标记
- 重新标记
Stop the world
- 并发清除
特点:
- 最短停顿回收时间:耗时最长并发标记和并发清除可以同用户线程一起运行,总体上可以说CMS收集器是同用户线程一起并发执行的
- 降低总吞吐量:并发标记和并发清除会占用CPU资源,降低吞吐量
- 无法清除浮动垃圾:在并发清除时用户线程产生的垃圾称为 "浮动垃圾"
- 堆内存不规整:标记清除会导致堆内存不规整;
G1收集器:Garbage First
最新、技术最前沿的垃圾收集器
步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
特点:
-
不降低吞吐量的情况下减少停顿时间:可预测停顿
-
分代收集:可以回收新生代和老年代
-
不会产生内存碎片
网友评论