JVM总结2--类加载与对象的创建

类加载

类加载过程

  类加载过程为:加载→连接→初始化。其中连接又可分为:验证→准备→解析 三步。这些行为都在虚拟机规范里有详细的定义。
在这里插入图片描述

  • 加载
    加载具体步骤:
    ①通过全类名获取此类的二进制字节流。
    ②将字节流所代表的的静态存储结构转换为方法区的运行时数据结构。
    ③在内存中生成一个该类的Class对象,作为访问类中数据的入口。
      一句话概括:加载就是把java字节码文件从不同的数据源读取到jvm,并映射成为jvm所认可的数据结构(Class对象)。
补充:
 - 这里数据源可以是jar,war,rar,其他文件(jsp)等等。
 - 加载阶段是用户参与,可控性较强。所以我们可以自定义类加载器,去实现自己的类加载过程。
  • 连接
    ①验证:虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
    ②准备:创建类或者接口中的静态变量,并设置静态变量的初始值。但这里的“初始化”和下面的显示初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。这里需要注意的是final;诶性的变量和非final类型的变量:
    比如一个成员变量这样定义:
public static long a = 1000;

  在上面的代码中,静态变量a在准备阶段的初始值是0,把a设置成1000是在后面对象初始化时完成。
但是这样定义:

public static final long a = 1000;

  此时,JVM在编译阶段后会为变量a生成其对应的ConstantValue属性,即虚拟机会在准备阶段就根据ConstantValue属性将a赋值为1000.
  综上:非final类型的静态变量在类加载时会赋值两次,一次是在准备阶段赋值为0,第二次是在初始化时赋值成程序里设定的值,而final static类型的变量在准备阶段会一次性赋值成程序中设定的值。其他非静类型则是在被调用时赋值。
③解析:将常量池中的符号引用(symbolic reference)替换为直接引用。在 Java 虚拟机规范中,详细介绍了类,接口,方法和字段等各方面的解析。

补充:
1.加载阶段和连接阶段的有些内容是交叉进行的,即加载没有结束,连接就可能已经开始了。
  1. 初始化
    初始化是真正去执行类初始化的代码逻辑主要通过执行类构造器的方法为类进行初始化,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑。
    JVM规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行。在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法(解释了非静态对象是调用的时候才会去初始化,静态对象在类加载的时候就初始化了)。

补充:
◎ 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类,因此不会触发该常量类的初始化。
◎ 在子类引用父类的静态字段时,不会触发子类的初始化,只会触发父类的初始化。◎ 定义对象数组,不会触发该类的初始化。
◎ 在使用类名获取Class对象时不会触发类的初始化。
◎ 在使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化。
◎ 在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化。
 -----摘自《offer来了》

类加载器

  JVM提供了3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。

  1. 启动类加载器(Bootstrap Classloader):最顶层的类加载器,由C++实现。主要负责加载%Java_HOME%/lib目录中的类库(jar包和类),或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。
  2. 扩展类加载器(Extention Classloader):由java实现。主要负责加载%Java_HOME%/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。
  3. 应用程序加载器(App Classloader):由java实现。是面向我们用户的类加载器。负责加载用户路径(classpath)上的类库。在这里插入图片描述

双亲委派模式

  java类加载使用双亲委派模式。双亲委派模式规定处理顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,但是这里的父子类关系不是继承,而是采用优先级来决定的。
双亲委派模式原理
  当一个类加载器收到了类加载请求,它首先不会自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次的加载器都是如此,因此最终所有的类加载请求都会到启动类加器中,当父类加载器反馈自己无法完成这个类加载请求(即在它的加载路径下没有找到所需要加载的Class)的时候,子类加载器才会尝试自己加载,以此类推。
在这里插入图片描述
为什么使用双亲委派模式
1.避免类的重复加载:当父类加载器已经加载过某个Class,子类就不会再加载,比如加载位于rt.jar包中的类java.long.Object , 对于这个类的加载请求,不管哪个类加载器加载,最终请求都会到启动类加载器。这样就保证了使用不同的类加载器最终得到的都是一个Class对象。

2.安全考虑,防止核心API类库被篡改:java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class
这时你可能会想,如果我们在classpath路径下自定义一个名为java.lang.xxx类,该类不是java中已存在的呢?此时,经过双亲委托模式,类加载请求传递到启动类加载器中,父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,以此类推,最终会通过应用类加载器或者自定义加载器加载该类。这时因为java.lang是核心API包,需要访问权限,强制加载程序将报错:

java.lang.SecurityException: Prohibited package name: java.lang

对象的创建

java对象的创建过程。

  java中通常用new关键字来创建对象,从jvm层面来看对象的创建过程:

  • 检查类是否加载
  • 为对象非配内存空间
  • 为对象字段初始化0值
  • 设置对象头
  • 执行构造方法

   第一步,检查类是否加载:java虚拟机在收到一条new指令时,首先去检查在常量池中是否能定位到该类的符号引用,在检查这个引用指向的类是否已经被加载、连接、初始化过,如果没有,先执行累的加载。
   第二步,为对象非配内存空间:检查完类加载以后,虚拟机为这个新的对象分配内存空间。也就是将一块确定大小的内存从堆内存中划分出来用于存放这个新的对象(因为对象所需要的的内存大小在里加载完成后就已经确定,所以是确定大小的。)。
内存的分配方式
  有两种,一种是指针碰撞,一种是空闲列表。具体选择哪种分配方式主要取决于堆内存是否规整,而堆内存是否规整又取决于垃圾收集器的垃圾回收算法是否有压缩整理的功能。

  • 指针碰撞:堆内存规整时使用,即没有内存碎片。此时的GC算法是标记-整理复制算法。原理:用过的内存全部被整合到一边了,没有用过的内存都在另一边,中间有一个分界指针,只需要向没有使用过的内存一边将指针移动新对象需要的内存大小位置即可。

  • 空闲列表:堆内存不规整时使用,即有内存碎片,GC算法是标记-清除法。原理:虚拟机维护一份列表,该列表记录的是那些内存块时可用的,在内存分配时,找一块足够大的内存块划分给新对象实例,最后更新列表中的记录。
    内存分配时的并发问题
      在创建对象时另一个问题就是线程安全问题。在实际开发中,创建对象频繁,而堆内存是线程共享的,存在线程安全问题,虚拟机采用两种方式来保证线程安全:

  • TLAB方式
       给每一个线程在Eden去分配一块内存,在对象内存分配时,每个线程在自己的TLAB里进行内存的分配,互不干扰。当TLAB中的剩余内存不足以满足对象所需要的的内存时,或者是TLAB中内存用完时,就会采用CAS+失败重试的方式。

  • CAS+失败重试方式
       CAS是乐观锁的一种实现方式,即不加锁,假定没有冲突去完成某项操作,如果因为冲突失败了就重试,直到成功为止。CAS+失败重试的方式保证了更新操作的原子性。
       第三步,初始化0:内存非配完毕后,需要对对象的字段进行零值初始化(对象头除外),零值初始化就是对对象的字段赋0值,或者null值,这也是为什么这些字段在没有初始化时候就能直接使用。
       第四步,设置对象头:对象头是比较复杂的。简单说一下,在初始化0值以后,虚拟机对对象进行一些必要的设置,对这个即将创建出来的对象进行信息标记。如:这个对象时哪个类的实例,对象的哈希码,元数据信息,GC分代信息等。这些信息存放在对象头中。
       第五步,执行init构造方法:上面四步完成后,从虚拟机角度来看对象已经创建完了,但是从程序角度来看,对象的init方法还没执行,所有的字段还是0值。此时执行init方法,把对象按照代码程序中的意思进行初始化,这才是程序员真正想要的操作。这些执行完成之后,一个新的对象真正产生了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值