仅记录学习笔记,如有错误欢迎指正。
最近打算重新整理一下笔记,好好回顾一下之前学的东西。争取在6月份之前整理完毕,加油加油。
1.加载: 将class字节码加载到内存中(二进制流文件),并将这些数据转换为方法区中的运行时数据(静态变量、静态代码块、常量池),在堆中生成一个Class类对象代表这个类(反射实现),作为方法区类数据的访问入口。 2.链接: 将java类的二进制代码合并到jvm的运行状态中。
- 验证:确保类信息符合jvm规范,没有安全问题。 文件格式验证、元数据验证、字节码验证和符号引用验证准备:为类变量(static)分配内存并设置类初始变量。给它分区,此时的值为默认值,赋值将在初始化阶段。
(这时候进行内存分配的仅包括类变量,如果类变量加了final ,此时的初始值为给定的值;实例变量将会在对象实例化时随着对象一起分配在Java堆中)解析:虚拟机常量池内的符号引用替换为直接引用。(如果有了直接引用,那么引用的目标必定已经在内存中存在)(解析可以在验证的时候开始)
直接引用:指针或者句柄
符号引用:一组符号来描述所引用的目标
前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。是类执行构造器(clinit)的阶段。
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。
clinit():类构造器,是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的.
- 所以clinit只负责静态变量的赋值和静态块中的赋值。若是类中没有静态变量或者静态代码块则不产生clinit方法。只在类加载的时候执行一次,且必须先执行父类的clinit方法。
init():实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
- 若类中无成员对象和代码块,不产生init()方法。每次对象实例化一次,init()加载一次。必须先执行父类的init()方法。
不多赘述,创建一个类和new一个对象。
5.卸载:- 执行了System.exit()方法.程序正常执行结束程序在执行过程中遇到了异常或错误而异常终止由于操作系统出现错误而导致Java虚拟机进程终止
- new 一个对象调用某个类的静态成员变量(出来final和常量)的静态成员方法。使用java.lang.reflect包的方法对类进行反射调用。启动程序所使用的main方法所在类当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。(多态?)定义对象数组和集合,不会触发该类的初始化(数组和集合的空间在堆里面)类A引用类B的static final常量不会导致类B初始化。(常量在编译阶段就存入调用类的常量池中了)
(1)引导类加载器(bootstrap classloader)
是拓展类的父类
它用来加载 Java 的核心库到jvm,是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。
(2)扩展类加载器(extensions class loader)
是application classload的父类
用来加载 Java 的扩展库,Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。
(3)应用程序类加载器(application classloader)
它根据 Java 应用的类路径来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
(4)自定义类加载器
开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求
代理模式即是将指定类的加载交给其他的类加载器。常用双亲委托机制
例如,用户定义了java.lang.String,那么加载这个类时最高级父类会首先加载,发现核心类中也有这个类,那么就加载了核心类库,而自定义的永远都不会加载。
五.获取一个类的3种方式: 六.对象的加载机制:遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经 被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规 整’或‘空闲列表-内存交 错’的分配方式)。(分配内存时先看数据类型,int 分配 4个字节空间)
指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器 向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。(规整就在空闲区域与直接分配)
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个链表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。(不规整就需要jvm找空间去分配)
内存空间分配完成后会将值初始化(设置默认值)为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头(MarkWord)。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成(赋初值)。把对象引用指向内存空间。
在虚拟机中, 对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充
对象头:
Mark Word(运行时数据):用于存储对象自身运行时的数据,如哈希码(hashCode)、GC分代年龄、线程持有的锁、偏向线程ID 等信息。在32 位系统占4字节,在64位系统中占8字节;Class Pointer(类型指针)::用来指向对象对应的Class对象(其对应的元数据对象)的内存地址;Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;
实例数据:
实例数据 是对象真正存储的有效信息,无论是从父类继承下来的还是该类自身的,都需要记录下来,而这部分的存储顺序受虚拟机的分配策略和定义的顺序的影响。
对齐填充:
无特殊含义,不是必须存在的,仅作为占位符。
对象的访问定位:(引用在栈中)Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
句柄访问:Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
(相当于 一个 string s = ‘java’ 句柄池里有一个句柄,里面包含两个指针,一个指向string 类型,一个指向 s)
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。



