一、首先简单说说类加载的时机,编译所生成的.class文件都会直接加载到JVM当中的吗?
只有在以下6种情况下,才会对类立即进行初始化操作:(.class文件加载到JVM当中)主动初始化的6种方式
- >创建对象实例:new 对象的时候,会依法类的初始化,前提这个类没有被初始化
- >调用类的静态属性或为静态属性赋值
- >调用类的静态方法
- >通过class 文件反射创建对象。
- >初始化一个类的子类:使用子类的时候先初始化父类
- >Java虚拟机启动时被标记为启动类的类:比如main方法所在的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。目的是为了节省内存开销。
不会进行初始化的情况:
- >在同一个类加载器下面只能初始化类一次,如果初始化了就不必要初始化了。
- >在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化。比如final 修饰的静态变量。
类的实例化的初始化步骤
没有父类的情况
- 类的静态属性
- 类的静态代码块
- 类的非静态属性
- 类的非静态代码块
- 构造方法
有父类的情况
- 父类的静态属性
- 父类的静态代码块
- 子类的静态属性
- 子类的静态代码块
- 父类的非静态属性
- 父类的非静态代码块
- 父类构造方法
- 子类非静态属性
- 子类非静态代码块
- 子类构造方法
在多次类实例化中,类静态属性和方法只会实例化一次,也就是执行一次
二、如何将类加载到jvm
Java默认有三种类加载器:
各个加载器的工作责任:
1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader:负责记载classpath中指定的jar包及目录中class
工作过程:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
好处:防止内存中出现多份同样的字节码(安全性角度)
特别说明:类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
类加载详细过程
- 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象。
- 连接,连接又包含三块内容:验证、准备、初始化。
- 1)验证,文件格式、元数据、字节码、符号引用验证;
- 2)准备,为类的静态变量分配内存,并将其初始化为默认值;
- 3)解析,把类中的符号引用转换为直接引用
- 初始化,为类的静态变量赋予正确的初始值。
三、JVM内存模型
JDK 1.8同JDK 1.7 ,最大的区别是:元数据取代了永久代.元空间的本质和永久代类似,都是对JVM规范中的方法区的实现.其元空间和永久代之间的最大区别在于:元数据空间不在虚拟机中,而是在本地内存中。永久代的大小很难确定,而且永久代的数据可能会随着每一次Full GC而发生移动。而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。
(1)程序计数器(PC寄存器)
程序计数器是一块较小的内存空间,是当前线程正在执行的哪一条字节码指令的地址,若当前线程正在执行的是一个本地方法
- 生命周期:随着线程的创建而创建,随着线程的销毁而销毁
- 是一个唯一不会出现的OutOfMemoryError的内存区域
(2)Java虚拟机栈
描述Java方法运行过程的内存模型,保存局部变量、基本数据类型以及堆内存中对象的引用变量
随着线程创建而创建,随着线程的结束而销毁
会出现两种异常:StackOverFlowError和OutOfMemoryError
- StackOverFlowError若Java虚拟机栈的大小不允许动态扩展,那么当前线程请求的栈的深度超过当前的Java虚拟机栈的最大深度是,就会抛出此异常
- OutOFMemoryError,若允许动态扩展,那么当前线程的请求的栈内存用完了,无法再动态扩展时,抛出此异常
(3)本地方法栈
为JVM提供使用native方法的服务,本地方法栈描述本地方法运行过程的内存模型
也会抛出StackOverFlowError和OutOfMemoryError异常
(4) 堆
线程共享、垃圾回收的主要场地,在虚拟机启动的时候就被创建
堆这块区域是JVM中最大的,堆内存的大小是可以调节的
抛出OutOfMemoryError异常
在这里要特别说明一下堆的划分:新生代、老年代、永久带。那么堆为什么要划分新生代、老年代、永久代呢?
分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。不同的区域存放的不同生命周期的对象,这样可以根据不同区域使用不同的垃圾回收算法,更具有针对性
JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
为什么会有年轻代呢?
年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
(5)方法区
线程共享、 存储的是类信息+普通常量+静态常量+编译器编译后的代码等,常量池(Constant Pool)是方法区的一部分
会抛出的异常就是OutOfMemoryError