文章目录
Java如何实现平台无关性
JVM是如何加载class文件的
Java虚拟机可以屏蔽底层操作系统的不同,并且减少基于原生语言开发的复杂性,JVM是一个内存中的虚拟机,也就意味着JVM的存储就是内存
即Java源码首先被编译成字节码,再由不同的JVM进行解析,Java语言再不同的平台上运行时不需要重新编译,Java虚拟机在执行字节码的时候,把字节码转化成具体平台上的机器指令。
- Class Loader:依据特定格式,加载class文件到内存
- Execution Egine(解释器):对命令进行解析
- Native Interface:融合不同开发语言的原生库为Java所用
- Runtime Data Area:JVM内存空间结构模型
关于反射
Java的反射机制是在运行状态中,对于任意一个类,都可以知道这个类的所有属性方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
package com.interview.javabasic.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class rc = Class.forName("com.interview.javabasic.reflect.Rebot");
Rebot r = (Rebot) rc.newInstance();
System.out.println("Class name is: " + rc.getName());
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
getHello.setAccessible(true);
Object str = getHello.invoke(r, "Bob");
System.out.println("getHello result is " + str);
Method sayHi = rc.getMethod("sayHi", String.class);
sayHi.invoke(r, "Welcome");
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r, "ZBW");
sayHi.invoke(r, "Welcome");
}
}
上述过程:
- 编译器将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转化为JVM中的Class对象
- JVM利用Class对象实例化为Robot对象
ClassLoader
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,主要的作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责将Class文件里的二进制数据装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
种类
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*(可以将自己自定义的Jar放在目录下,用这种类型的ClassLoader去加载),在进行加载时,不是一次性加载,而是用到才去加载
- AppClassLoader:Java编写,加载程序所在目录(加载classpath路径下的内容)
- 自定义ClassLoader:Java编写,定制化加载,关键函数为findClass(String name),包括怎么去怎么去读取二进制流以及怎么处理,进而返回一个Class对象,还有defineClass(byte[], int off, int len),这个函数,接收到字节流之后就可以定义,并返回Class对象
职责
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 父类委托:类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。父类委托机制是为了防止内存中出现多份同样的字节码,保证java程序安全稳定运行。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
类加载器的双亲委派机制(为了让不同的ClassLoader进行相互间的协作,各司其职)
原理图:
- 从下向上委派
- 从上向下委派
类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
为什么要使用双亲委派机制去加载类
- 避免多份同样字节码的加载
类的加载方式
- 隐式加载:new
- 显式加载:loadClass,forName等
二者的区别:
- Class.forName得到的class是已经初始化完成的
package com.interview.javabasic.reflect;
public class Rebot {
private String name;
public void sayHi(String helloSentence) {
System.out.println(helloSentence + " " + name);
}
private String throwHello(String tag) {
return "Hello" + tag;
}
static {
System.out.println("Hello Robot");
}
}
package com.interview.javabasic.reflect;
import java.awt.*;
public class LoadDifference {
public static void main(String[] args) throws ClassNotFoundException {
// ClassLoader cl = Robot.class.getClassLoader();
Class r = Class.forName("com.interview.javabasic.reflect.Rebot");
}
}
执行结果:
- Class.loadClass得到的class是还没有链接的
package com.interview.javabasic.reflect;
import java.awt.*;
public class LoadDifference {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader cl = Robot.class.getClassLoader();
// Class r = Class.forName("com.interview.javabasic.reflect.Rebot");
}
}
执行结果:
可见,静态代码块并没有执行。
关于类加载机制
Java中的类加载机制指虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。
- 加载:加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
- 验证:验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。
- 准备:准备阶段为类的静态变量分配内存,并将其初始化为默认值。假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。
- 解析:解析阶段将类中符号引用转换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
- 初始化:初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
JVM中的内存如何进行划分
JVM中的内存主要划分为5个区域,即方法区,堆内存,程序计数器,虚拟机栈以及本地方法栈。
- 方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
- 堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。
- 虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
- 程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 本地方法栈: 主要是为JVM提供使用native 方法的服务。