目录
1、类加载的详细过程
类被加载到内存开始到写出内存,整个生命周期分为7个阶段:
加载、验证、准备、解析、初始化、使用、卸载
1.1、 分别介绍几个过程的功能:
加载:
在加载阶段,虚拟机需要完成3件事
1.1.1、通过一个类的全限定名获取定义此类的二进制文件。
1.1.2、将加载的二进制文件存储在方法区。
1.1.3、在内存(堆空间)中生成一个代表这个类的java.lang.Class对象,作为访问方法区中该类的各种数据的访问入口。
连接:
连接阶段分为三部分:验证、准备、解析
验证:为了确保class文件的字节流中包含的信息符合虚拟机的要求,保证加载字节码文件不会危害虚拟机的安全,验证包括:文件格式验证、元数据验证、字节码验证、符号引用验证
准备:主要为static类型的变量分配内存并设置初始值,变量所使用的内存在方法区分配
赋初始值:基本的数据类型0,false,引用类型:null
解析:将符号引用替换为直接引用 ,主要针对类或者接口,字段、方法,等
符号引用:可以理解就是字符串,比如引用一个类,java.util.ArrayList这个就是一个符号引用,符号引用不一定被加载到内存
直接引用:指针或者地址偏移量,引用对象一定存在于内存(已经被加载)
初始化:
主要工作是对静态变量赋值设定初始值
执行静态代码块
例如:static int i= 10;
在准备阶段为i分配空间,给定默认值0;初始化阶段将i=10进行赋值
1.2、举个使用类加载的例子:
单例模式
在一个JVM实例中只存在一个对象实例
单例实现要点:
1、将类的构造器私有化
2、提供一个public方法获取当前对象实例
public class Single{
private static final Single s = new Single();//3、由于不让外界访问,所以用private修饰,需要在2中被返回使用,所以用static修饰。
private Single(){}//1、构造器私有化,为了确保其他类不再创建该类的对象
public Static Single getInstance(){//2、给外界提供pulbic修饰的可访问的方法,由于外界没有对象实例,不能通过对象调用,只能使用类名调用,所以该方法为static修饰的方法
return s;
}
}
//调用
Single.getInstance();//直接调用
3、可以满足单例,原因如下
当调用Single.getInstance()方法时,如果第一次使用该类,按照类的加载时机,当前Single类会被加载、被完成连接、初始化等阶段,其中s变量是一个静态变量,这在准备及初始化的阶段就会对静态变量做分配内存及赋值等过程;而JVM加载机制保证了一个类只会被加载一次,即s的分配内存及赋值的过程只会在加载时被处理一次,因此该单例是符合要求的,单例的实现是由JVM的类加载机制保证的。
2、JVM的内存模型
2.1、Java的内存布局
在java中,内存布局分了5大块,分别是堆区,虚拟机栈、本地方法栈、方法区、程序计数器
按照线程是否共享,绿色区域是线程共享区域、红色区域是线程私有空间
内存区域的划分标准:
介绍、作用、是否线程共享、生命周期、异常问题(OOM,内存不足)
2.2、程序计数器
程序计数器是一个较小的内存空间区域,是用来表示当前线程执行的字节码的行号指示器。
由字节码解释器通过改变计数器的值来选择下一条要执行的字节码。
对于分支、循环、异常等都需要依赖计数器来指定执行位置,该区域是线程独有的,每一个线程都有单独的计数器。线程私有空间
作用:
2.2.1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如,顺序执行,跳转等
2.2.2、在多线程下,程序计数器用于记录当前线程的执行位置,从而当线程切换的时候能知道线程上次执行在哪里
程序计数器是唯一一个不会出现OutOfMemoryError(OOM)的内存区域
他的生命周期随着线程的创建而创建,随着线程的结束而消亡
2.3、虚拟机栈
虚拟机栈主要是描述Java方法的执行的内存模型,每个方法被执行的时候都同时创建一个栈帧,用于存放局部变量表,操作数栈,动态链接等,每一个方法被调用直至执行完成的过程,对应一个栈帧在虚拟机栈中从入栈到出栈过程
java代码的执行就是方法调用方法
每一个栈帧中存储局部变量表、操作数栈、动态链接、方法出口信息
局部变量表中存放八大原始类型,对象引用(可能是指向对象的起始地址的引用指针,也可能是一个代表对象的句柄或者其他与他相关的位置信息),局部变量表所需要的内存在编译阶段完成分配,在方法的执行期间不会改变局部变量表的大小
虚拟机栈详细图解
Java虚拟机栈会出现两种异常StackOverFlowError和OutOfMemoryError
StackOverFlowError:若java虚拟机栈的内存大小不允许动态扩展,当线程请求的栈的深度超过当前虚拟机栈最大的深度的时候就会抛出StackOverFlowError。
OutOfMemoryError:若java虚拟机栈的内存大小允许动态扩展,但当线程请求栈内存用完了,无法进行动态扩展,此时抛出OutOfMemoryError异常
在JVM中设置虚拟机栈的大小设置:
-Xss:用于设置栈的大小,栈的大小决定了方法的调用深度
例如:-Xss512k
Java虚拟机栈是线程私有的
生命周期随着线程创建而创建,随着线程的消亡而死亡
2.4、本地方法栈
和虚拟机栈的作用是非常类似的
区别:虚拟机栈是为虚拟机提供Java方法(字节码)的服务,而本地方法栈则为虚拟机使用到的native方法提供服务
也会出现两种异常StackOverFlowError和OutOfMemoryError
线程私有的
生命周期随着线程创建而创建,随着线程的消亡而死亡
2.5、堆
堆是虚拟机栈所管理的内存最大的一块,也是线程共享的内存区域,在虚拟机启动的时候创建、随着虚拟机消亡而死亡。
当堆内存空间不足时,会抛出OutOfMemoryError异常
作用:
堆是用来存放java对象和数组的、GC操作主要关注的是堆空间
根据垃圾回收算法,堆空间划分:新生代和老年代(永久代)
新生代细化分:Eden空间,from Survivor和to Survivor,为了更好的回收内存
对于堆空间的参数设置
设置堆初始容量 -Xms1024m
设置堆空间存储最大值 -Xmx1024m
设置新生代堆的大小 -Xmn512m
永久代:
JDK1.6及之前,存在永久代
JDK1.7:有永久代,但已经逐步“去永久代”
JDK1.8及之后,移除了永久代,新增了一个元空间(MetaSpace)的内存区域(元空间使用物理内存,直接受本机物理内存限制)
2.6、方法区
方法区和堆空间一样,是线程共享的区域,作用是存放类的信息,静态变量等
在一定的条件下会被GC操作,方法区使用的内存超过其允许的大小时,会抛出OutOfMemoryError的异常
HotSpot虚拟机中方法区也称之为“永久代”,设计时用永久代来实现方法
方法区和永久代:
方法区在不同 JDK 版本的变化
运行时常量池:String s="hello"
常量池:用于存放编译期生成的各种字面量和符号引用
通过上图可知,在JDK1.6及之前存放在方法区,在JDK1.7之后放在老年代
方法区和元空间的区别:
通过new Object()过程
虚拟机如何创建对象,堆空间的分配,如何查找对象
1:对象的创建
先在虚拟机栈中创建栈帧、栈帧内创建对象的引用、在方法区进行类的加载,然后在Java的堆区进行分配空间并内存初始化,在回到栈帧中初始化对象数据,完成对象的创建
2、堆空间对象空间分配
堆空间是线程共享的,需要解决并发问题,在堆中并发问题解决方式有两种,同步方式和TLAB方式
同步方式:JVM为了效率采用CAS方式
CAS:比较并交换,实现无锁形式的原子指定
TLAB方式:(Thread Local location Buffer),每个线程在堆中预先分配一小块内存,叫做本地线程分配缓冲区,
哪个线程需要分配内存先去各自的TLAB分配,但TLAB比较小,为了加速对象的分配,在线程使用完TLAB空间之后在到堆中分配内存,此时考虑同步机制
3、对象的访问
句柄访问
优点:在垃圾回收对象时需要经常转移对象,即对象地址会发生改变,这时只需要改变句柄池中执行对象实例的数据的指针即可(不需要修改reference)
缺点:查找对象需要两次指针查找
直接访问
优点:
相对于句柄访问定位,减少了一次指针定位的开销(也减少了句柄池池占用的存储空间)
Hotspot虚拟机设置采用的其实直接访问形式查找对象
缺点:当对象被GC操作发送改变,所有调用该对象的虚拟机栈中的引用也要被改变。