JVM~class文件加载机制及其JVM内存模型

一、首先简单说说类加载的时机,编译所生成的.class文件都会直接加载到JVM当中的吗?

只有在以下6种情况下,才会对类立即进行初始化操作:(.class文件加载到JVM当中)主动初始化的6种方式

  1. >创建对象实例:new 对象的时候,会依法类的初始化,前提这个类没有被初始化
  2. >调用类的静态属性或为静态属性赋值
  3. >调用类的静态方法
  4. >通过class 文件反射创建对象。
  5. >初始化一个类的子类:使用子类的时候先初始化父类
  6. >Java虚拟机启动时被标记为启动类的类:比如main方法所在的类

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。目的是为了节省内存开销

不会进行初始化的情况:

  1. >在同一个类加载器下面只能初始化类一次,如果初始化了就不必要初始化了。
  2. >在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化。比如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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值