一、JVM内存结构
二、类加载(classLoader)机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。
1、类加载的过程:
加载:通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个Class对象
验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,并且不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
如public static int value = 5;这里只将value初始化为0。
解析:虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用就是一组符号来描述所引用的目标,可以是任何字面量,
直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。
初始化:是执行类构造器<clinit>()方法的过程。
- 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类还没进行初始化,则需要先触发初始化。
常见java代码场景:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,在编译期已经被塞进常量池了)、以及调用一个类的静态方法的时候。
- 使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
- 初始化一个类的时候,如果发现其父亲还没有被初始化,则先去初始化其父类
- jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
被动引用:
- 子类引用父类的静态字段,不会导致子类初始化
public class SuperClass {
public static int value = 123;
static {
System.out.println("superClass init");
}
}
public class SubClass extends SuperClass {
static {
System.out.println("subClass init");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
- 通过数组定义来引用类,不会触发此类的初始化
public class SuperClass {
public static int value = 123;
static {
System.out.println("superClass init");
}
}
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
- 常量在编译阶段会存入调用类的常量池中,因此不会触发定义常量的类的初始化
public class ConstClass {
public static final String HELLOWORLD = "hello world";
static {
System.out.println("constClass init");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
使用
卸载
2、类加载器有哪些:
通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
1) 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
2) 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3) 系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4) 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
自定义类加载器:父类加载器肯定为AppClassLoader。
3、双亲委派机制:
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
4、沙箱安全机制
三、JVM运行时数据区:
java虚拟机主要分为以下几个区:
1) 方法区:
a. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载
b. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
c. 该区域是被线程共享的。
d. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
2) 虚拟机栈:
a. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
b. 虚拟机栈是线程私有的,它的生命周期与线程相同。
c. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
d. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
e. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
3) 本地方法栈:
本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。
4) 堆:
java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
5) 程序计数器:
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。
四、常见的垃圾回收算法:
1、引用计数:难以处理对象间的循环引用问题(JVM不使用)
2、复制算法:(新生代) :
- 复制:eden、survivorFrom复制到survivorTo,年龄+1
- 清空:清空eden、survivorFrom
- 互换:survivorFrom和survivorTo互换,交换15次,最终还存活的对象进入老年代。
优点:不会产生内存碎片
缺点:浪费空间
3、标记-清除:(老年代)
- 先标记出要回收的对象,然后统一回收这些对象
优点:节约内存空间
缺点:产生内存碎片
4、标记-整理:(老年代)
优点:不会产生内存碎片
缺点:移动对象需要成本
五、GC的作用域
java堆+方法区