目录
一、概述
java是一种面向对象思想的编程语言,具有跨平台的特点,即一次编译到处运行的特点,得益于java程序编译后运行在JVM(java虚拟机)上,由JVM屏蔽了底层操作系统的细节。
java程序开发者需要安装JDK(java开发工具包),这里面包含了JRE(java运行环境),java程序的运行必须拥有jre, 而jre中包含JVM
经过加载,类信息被放入JVM当中,并且在JVM中内存是被划分为不同的部分,这些部分组成了运行时数据区
二、类的生命周期
整个类的加载过程需要经过 装载 链接 和 初始化;其中链接又分为验证,准备和解析
2.1 装载
装载过程就是将.class文件转换成JVM内存对象的过程
1. .class文件是一种文本信息,而内存中的数据以流的形式存在;因此将.class文件转换成流然后通过类加载机制加载到JVM内存当中
2. 将字节流的静态存储结构转换成方法区中的数据结构
3. 在堆成生成对应的java.lang.Class对象,作为访问这个类信息的入口
2.2 链接
1. 验证:检查文件的内容是否出错,版本,符号引用,字节码,元数据等
2. 准备:为类的静态变量分配内容,并根据相应的数据类型进行初始化操作,使用默认值;
如:int 默认值为 0, boolean默认值为false, String默认值为Null
3. 解析:从运行时常量池的符号引用动态确定具体值的过程;即 符号引用 -》 直接引用
常量池中的在编译器间就是一种符号引用,然后在程序运行时会指向真正的位置
2.3 初始化
通过Clinit 初始化静态变量,初始化代码块,初始化父类的过程
三、类加载器
ClassLoader, 能通过类的“全限定类名”获取类的二进制字符流,将字节码文件加载到内存中,在heap中创建java.lang.Class对象,将对象封装到方法区中的数据结构
ClassLoader是抽象类, 有若干的具体实现类;每种类加载器负责从不同的classpath中加载字节码文件
java中核心有三种类加载器:
3.1、双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派机制的好处:
防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
自定义String类
package classloader.java.lang;
public class String {
static {
System.out.println("我是自定义的String类");
}
@Override
public java.lang.String toString() {
return "???";
}
}
在主程序中测试
package classloader;
public class demo01 {
public static void main(String[] args) {
String s = new String();
System.out.println();
}
}
结果显示中并没有打印自定义类中的static静态代码块信息;原因在于加载类时由于双亲委派机制会向上发送加载请求,在BootstrapClassLoader(核心类库)发现java.lang.String, 那么就加载这个类,因此自定义的类无法加载。保证与核心类库同名的自定义类无法干扰内置类。
3.2 双亲委派源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
在类的加载中,除了双亲委派机制,还包括内存加载机制;首先会检查内存中是否存在这个类对象,如果有会直接加载;否则,判断当前类加载器的父类是否为null, 如果为Null, 说明已经是Bootstrap类加载器了。
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
3.3 如何打破双亲委派机制
1. 重写:protected Class<?> loadClass(String name, boolean resolve) 方法,然后不加 if (parent != null) 的判断条件
2. spi机制
四、运行时数据区
类加载器:根据全限定类名装载class到运行时数据区域
运行时数据区域:JVM内存
执行引擎:编译后的.class通过执行引擎转换成系统可以识别的指令
本地库接口:与其他编程语言交互的接口
4.1 执行流程
经过编译后的java文件生成.class文件,通过类加载器将.class装载到内存中的方法区。由于.class也是一套指令集规范,所以需要通过执行引擎将字节码文件转换成机器可以识别的指令,在交由CPU执行,这个过程需要其他编程语言参与,所以需要本地库接口
类加载后在内存中生成相应的java.lang.Class对象,封装在内存中的数据结构当中
4.2 运行时数据区域组成
整个运行时数据区分为五个部分:
其中方法区和java堆被线程共享;而虚拟机栈,本地方法栈和程序计数器线程间独立
方法区:存储虚拟机加载后的类信息,常量,静态变量
堆:为对象实力分配内存
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
五、java创建对象的方式
使用new创建对象
使用反射机制,Class.newInstance()
使用Object方法clone
使用反序列化
ioc机制控制反转由容器创建对象
5.1 为对象分配内存的方式
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。(连续的)
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。(离散的)
Java 堆是否规整又由所采用的垃圾收集器是否带有压缩(操作系统中的紧缩?)整理功能决定
5.2 java对象内存布局
其中:
Class Pointer(类型指针)
类型指针指向实例对象对应的类信息的内存地址,其占用的内存大小有两种情况:
当开启了指针压缩(64位系统默认开启),内存大小是4字节,不开启指针压缩,内存大小是8字节
对齐填充的作用: 字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中,来提升效率。
如图:假设在64bit的系统中,每次读取8字节的数据;如果不进行补齐,那么读取一个long类型的数据需要读取两次;而如果补齐后long类型数据被存储在红色部分,那么只需要读取一次,提高了CPU读取的效率。