虚拟机可分为:系统虚拟机和程序虚拟机
系统虚拟机:系统虚拟机是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。例如:Visual Box、VMware 就属于系统虚拟机。
程序虚拟机:程序虚拟机是专门执行单个计算机程序的虚拟机。例如:Java 虚拟机。
Java 虚拟机是一台执行 Java 字节码的虚拟计算机,它拥有独立的运行机制,其运行的 Java 字节码也未必由 Java 语言编译而成。意思是任何语言经过编译后,只要生成的是 .class 文件,就会被 JVM 解析执行。
Java 虚拟机的特点:一次编译,到处运行,自动内存管理,自动垃圾回收。
一次编译,到处运行:.java 文件被编译成 .class 文件后,既可以在 Windows 操作系统上运行,也可以在 Linux 操作系统上运行,还可以在 Mac 操作系统上运行,只要它们的系统上安装了对应版本的虚拟机。体现了 JVM 的跨平台性。
类的加载过程就是把 .java 文件编译后生成的 .class 文件加载到 JVM 的过程。
x.java 通过 javac 命令编译后生成 x.class 文件,x.class 文件通过类加载器(ClassLoader)加载到 Java 虚拟机。加载的过程分为 加载(Loading)、链接(Linking)、初始化(Initialization) 的过程,其中链接(Linking)又分为 验证(Verification)、准备(Preparation)、解析(Resolution) 三个部分。
加载(Loading):
将 .class 文件从磁盘读取到内存,并在内存中构建出 Java 类的原型——类模板对象。
在加载类时,Java 虚拟机必须完成以下 3 件事情:
数组类本身不通过类加载器创建,它是由 Java 虚拟机直接在内存中动态构造出来的,但数组类的元素类型还是要靠类加载器来完成加载:
链接(Linking):
初始化(Initialization):
为静态变量赋初始值。初始化阶段就是执行类构造器方法
的过程(
类构造器方法和
构造函数并不是同一个方法)。
不是程序员在 Java 代码中直接编写的方法,是 Javac 编译器的自动生成物。
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的。
1)由父及子,静态先行:Java 虚拟机会保证在子类的
方法执行前,父类的
方法已经执行完毕(父类的
方法先执行,但并不是执行子类
方法会调用父类的
方法,区别于构造函数)。由于父类的
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的赋值变量操作。
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
2)
方法对于类或接口来说并不是必需的。不会生成
方法的情况:
public class InitializationTest1 {
// 场景1:对应非静态的字段,不管是否进行了显示赋值,都不回生成 () 方法
public int num = 1;
// 场景2:静态的字段,没有显示的赋值,不会生成 () 方法
public static int num1;
// 场景3:比如对于声明为 static final 的基本数据类型的字段,不管是否进行了显示赋值,都不会生成 () 方法
public static final int num2 = 1;
}
3)
方法的线程安全性:虚拟机会保证一个类的
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
方法。其他线程都需要阻塞等待,直到活动线程执行
方法完毕。正是因为函数
带锁线程安全的,因此如果在一个类的
方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁,并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
方法是 static 修饰的,但此方法并没有被 synchronized 修饰,所以出现死锁很难被发现。
Java 虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己去决定如何获取所需的类。实现这个动作的代码被称为类加载器。意思是 Java 虚拟机中并不包含类加载器。
JVM 判断两个 class 对象是否为同一个类的两个必要条件:
在 JVM 中,即使这两个类对象来源于同一个 Class 文件,被同一个虚拟机所加载,但是加载他们的 ClassLoader 不同,这两个类对象就不相等。比如我自己写一个String,包名为 java.lang.String,我自己写这个 String 是通过 User ClassLoader 加载的,而官方的 String 是通过 Bootstrap ClassLoader 加载器加载的,虽然他们的包名和类名一致,但他们也不是同一个类对象。
启动类加载器(Bootstrap Class Loader,也叫引导类加载器):负责加载 JRE 的核心类库,如 JRE 目标下的 jre/lib/rt.jar、chsrsets.jar、resources. jar等。启动类加载器是由 C/C++ 语言实现的。所以调用它的 getClassLoader() 方法返回 null。
扩展类加载器(Extension Class Loader):负责加载 JRE 扩展目录 ext 中的 jar 类包。
应用程序加载器(Application Class Loader,也叫系统类加载器):负责加载 ClassPath 路径下的类包。
自定义类加载器(User Class Loader):我们理解的用户自定义类加载器,是用户自己定义的,实际上官方对自定义类加载器的定义是:所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。也就是说:所有直接或间接继承 ClassLoader 的类都叫自定义类加载器,Ext ClassLoader 和 App ClassLoader 也是间接继承了 ClassLoader,他们也算是自定义类加载器。
如果一个类加载器收到了类加载请求,它不会自己先去加载,而是向上委托,让父类加载器去加载,一直委托到 Bootstrap ClassLoader,如果 Bootstrap ClassLoader 能加载则加载这个类,如果不能加载则让子类加载器去加载,如果所有的子类加载器都不能加载这个类,则会抛出 ClassNotFound 异常。
为什么要搞双亲委派机制?
如果没有双亲委派机制,我可以自己写一个 java.lang.String 类覆盖 Oracle 的 String,由于用户输入的密码大部分都是 String 类型,所以我就可以通过自己写的 String 来获取用户的密码。
1)继承 ClassLoader 接口,Tomcat 中的 WebappClassLoader 继承 ClassLoader 的子类 URLClassLoader。
2)重写 loadClass 方法,实现自己的逻辑,不要每次都先委托给父类加载,例如可以先在本地加载,这样就破坏了双亲委派模型了。
Java 提供了抽象类 java.lang.ClassLoader
,所有用户自定义的类加载器都应该继承 ClassLoader 类。
在自定义ClassLoader 的子类时候,我们常见的会有两种做法:
方式一:重写 loadClass() 方法
方式二:重写 findClass() 方法
这两种方法本质上差不多,毕竟 loadClass() 也会调用 findClass(),但是从逻辑上讲我们最好不要直接修改 loadClass(),建议的做法是只在 findClass() 里重写自定义类的加载方法,根据参数指定类的名字,返回对应的 Class 对象的引用。。
loadClass() 这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass() 方法的过程中必须写双亲委派的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
public class MyClassLoader extends ClassLoader{
private String byteCodePath;
public MyClassLoader(String byteCodePath){
this.byteCodePath=byteCodePath;
}
public MyClassLoader(String byteCodePath, ClassLoader parent){
super(parent);
this.byteCodePath=byteCodePath;
}
@Override
protected Class<?> findClass(String className)throws ClassNotFoundException{
String fileName = byteCodePath + className + ".class";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis= null;
try {
bis = new BufferedInputStream(new FileInputStream(fileName));
int len;
byte[] data=new byte[1024];
// 通过循环操作,把对应的文件(fileName)数据写到内存对应的 ByteArrayOutputStream 对应里面的字节数组中
while ((len=bis.read(data))!=-1){
baos.write(data,0,len);
}
// 获取内存中的完整自己数组数据
byte[] byteCodes = baos.toByteArray();
// 调用defineClass,将字节数组的数据转化为class的实例
Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (baos!=null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis!=null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
测试类:
public class MyClassLoaderTest {
public static void main(String []args){
MyClassLoader loader=new MyClassLoader("d://");
Class clazz= null;
try {
clazz = loader.loadClass("Demo1");
System.out.println("加载此类的类加载器为:" + clazz.getClassLoader().getClass().getName());
System.out.println("加载当前 Demo1 类的类加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
结果:
加载此类的类加载器为:sun.misc.Launcher$AppClassLoader
加载当前 Demo1 类的类加载器的父类加载器为:sun.misc.Launcher$ExtClassLoader
Java 安全模型的核心就是 Java 沙箱(sandbox)。什么是沙箱?沙箱是一个限制程序运行的环境。沙箱安全机制的作用:
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,那系统资源包括什么? CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的 Java 程序运行都可以指定沙箱,可以定制安全策略。JDK1.6时期,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(ProtectedDomain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限。