JVM
本文是自己站在安全学习者角度上去学习JVM的笔记,后续可能也会随着自己所需要用到再不断完善该篇文章。假如您是一个开发从业者或其他从业者需要学习JVM,本文可能不适合去系统学习JVM。
jvm是Java Virtual Machine (Java虚拟机) 的缩写,jvm是一种用于计算设备的规范,它是一个虚拟出来的计算机,是通过再实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入了Java虚拟机后,Java语言在不同平台上运行时不需要重新的编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成虚拟机上运行的目标代码(字节码),就可以在各种平台上不加修改的运行,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行,这就是Java的能够”一次编译,到处运行的原因”。
JVM位置及体系结构
JVM运行在操作系统之上,如下图
其体系结构如下
类加载器
类加载器用来加载class文件,把文件加载到内存中
我们可以通过类来创建对象,也能通过对象返回来获取类
//类只是一个模板,但是对象是具体的 public class Person { public static void main(String[] args){ //创建三个对象 Person person1 = new Person(); Person person2 = new Person(); Person person3 = new Person(); //分别输出 System.out.println("----------对象不是同一个----------"); System.out.println(person1); System.out.println(person2); System.out.println(person3); //通过对象获取类 System.out.println("----------类是同一个----------"); System.out.println(person1.getClass()); System.out.println(person2.getClass()); System.out.println(person3.getClass()); } }
对于虚拟机来说只有两类类加载器一种是Bootstrap ClassLoader是通过C++语言实现的,是虚拟机自身的一部分,另一类是其它类加载器,是使用Java语言实现的都继承自java.lang.ClassLoader
启动类加载器(Bootstrap CLassLoader):属于虚拟机的一部分,通过C++实现的,负责加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径存放的类
扩展类加载器(Extension ClassLoader):独立于虚拟机, 负责加载\lib\ext中的类库。java9之后由于模块化的需要被PlatFormClassLoader取代
应用程序类(Application ClassLoader):独立于虚拟机,主要负责加载用户类路径(classPath)上的类库,如果没有实现自定义类加载器,那么这个这个加载器就是我们程序的默认类加载器
自定义类加载器
其中加载器之间也存在父子级关系
public class Person { public static void main(String[] args){ Person person = new Person(); Class aClass = person.getClass(); //获取类加载器 ClassLoader classLoader = aClass.getClassLoader(); System.out.println(classLoader); //Application ClassLoader System.out.println(classLoader.getParent()); //Extension ClassLoader System.out.println(classLoader.getParent().getParent()); //Bootstrap CLassLoader(获取到的值为null,因为该加载器通过C++实现,java获取不到) } }
双向委派机制
一个类加载器在收到一个类加载请求的时候,它不会自动去加载这个类,而是把这个类委托给父类加载器去完成,每一层重复这样的操作,当父类加载器反馈自己无法完成该加载请求的时候,子类加载器才会去加载该字节码文件。
该机制避免了重复加载和核心类被修改
但是在java9以及以后的版本中,为了模块化系统的顺利实施,模块下的类加载器主要有几个变动:
1.扩展类加载器(Extension Class Loader)被平台类加载器(Platform ClassLoader)取代(java9中整个JDK都是基于了模块化的构建,原来的rt.jar和tools.jar都被拆分了数十个JMOD文件)。因为java类库可以满足扩展的需求并且能随时组合构建出程序运行的jre,所以取消了JAVA_HOME\lib\ext和JAVA_HOME\jre目录
2.平台类加载器和应用程序类加载器都不在派生自java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader,在BuiltinClassLoader中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。
就是说平台以及应用程序类加载器收到类的加载请求的时候,在委派给父类加载器家在之前,要先判断该类是否归属于摸一个系统模块,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
ClassLoader
所有的类加载器(除根类加载器)都必须继承java.lang.ClassLoader
loadClass
在ClassLoader的源码中,有一个方法loadClass(String name,boolean resolve),这里就是双亲委托模式的代码实现。从源码中我们可以观察到它的执行顺序。需要注意的是,只有父类加载器加载不到类时,会调用findClass方法进行类的查找,所以,在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法。
//ClassLoader类的loadClass源码 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
findClass
在自定义类加载器时,一般我们需要覆盖这个方法,且ClassLoader中给出了一个默认的错误实现。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
defineClass
该方法的解释如下。用来将byte字节解析成虚拟机能够识别的Class对象。defineClass()方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象。
defineClass(String name,byte[] b,int off,int len) throws ClassFormatError
resolveClass
连接指定的类,类加载器可以使用此方法来连接类。
URLClassLoader
在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。
构造方法
public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。
public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。
案例一:加载磁盘上的类
public static void main(String[] args) throws Exception{ //指定需要加载类的指定位置 File file = new File("d:/"); //获取对应的uri URI uri = file.toURI(); //获取对应的 url URL url = uri.toURL(); //构建URLClassLoader URLClassLoader classLoader = new URLClassLoader(new URL[]{url}); System.out.println(classLoader.getParent()); Class aClass = classLoader.loadClass("com.ceshi.Demo"); Object obj = aClass.newInstance(); }
案例二:加载网络上的类
public static void main(String[] args) throws Exception{ URL url = new URL("http://localhost:8080/examples/"); URLClassLoader classLoader = new URLClassLoader(new URL[]{url}); System.out.println(classLoader.getParent()); Class aClass = classLoader.loadClass("com.ceshi.Demo"); aClass.newInstance(); }
自定义加载器
我们如果需要自定义类加载器,只需要继承ClassLoader类,并覆盖掉findClass方法即可。
自定义文件类加载器
package com.itheima.base.classloader; import sun.applet.Main; import java.io.*; public class MyFileClassLoader extends ClassLoader { private String directory;//被加载的类所在的目录 /** * 指定要加载的类所在的文件目录 * @param directory */ public MyFileClassLoader(String directory,ClassLoader parent){ super(parent); this.directory = directory; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { //把类名转换为目录 String file = directory+File.separator+name.replace(".", File.separator)+".class"; //构建输入流 InputStream in = new FileInputStream(file); //存放读取到的字节数据 ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte buf[] = new byte[1024]; int len = -1; while((len=in.read(buf))!=-1){ baos.write(buf,0,len); } byte data[] = baos.toByteArray(); in.close(); baos.close(); return defineClass(name,data,0,data.length); } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws Exception { MyFileClassLoader myFileClassLoader = new MyFileClassLoader("d:/"); Class clazz = myFileClassLoader.loadClass("com.ceshi.Demo"); clazz.newInstance(); } }
自定义网络类加载器
package com.itheima.base.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; public class MyURLClassLoader extends ClassLoader { private String url; public MyURLClassLoader(String url) { this.url = url; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { String path = url+ "/"+name.replace(".","/")+".class"; URL url = new URL(path); InputStream inputStream = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int len = -1; byte buf[] = new byte[1024]; while((len=inputStream.read(buf))!=-1){ baos.write(buf,0,len); } byte[] data = baos.toByteArray(); inputStream.close(); baos.close(); return defineClass(name,data,0,data.length); } catch (Exception e) { e.printStackTrace(); } return null; } public static void main(String[] args) throws Exception{ MyURLClassLoader classLoader = new MyURLClassLoader("http://localhost:8080/examples"); Class clazz = classLoader.loadClass("com.ceshi.Demo"); clazz.newInstance(); } }
热部署类加载器
当我们调用loadClass方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载。如果同一个class被同一个类加载器多次加载,则会报错。因此,我们要实现热部署让同一个class文件被不同的类加载器重复加载即可。但是不能调用loadClass方法,而应该调用findClass方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署。
MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("d:/",null); MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader("d:/",myFileClassLoader1); Class clazz1 = myFileClassLoader1.loadClass("com.ceshi.Demo"); Class clazz2 = myFileClassLoader2.loadClass("com.ceshi.Demo"); System.out.println("class1:"+clazz1.hashCode()); System.out.println("class2:"+clazz2.hashCode()); 结果:class1和class2的hashCode一致 MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("d:/",null); MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader("d:/",myFileClassLoader1); Class clazz3 = myFileClassLoader1.findClass("com.ceshi.Demo"); Class clazz4 = myFileClassLoader2.findClass("com.ceshi.Demo"); System.out.println("class3:"+clazz3.hashCode()); System.out.println("class4:"+clazz4.hashCode()); 结果:class1和class2的hashCode不一致
类的显式与隐式加载
类的加载方式是指虚拟机将class文件加载到内存的方式。
显式加载是指在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name);this.getClass().getClassLoader().loadClass()加载类。
隐式加载指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。
线程上下文类加载器
在Java中存在着很多的服务提供者接口SPI,全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,这些接口一般由第三方提供实现,常见的SPI有JDBC、JNDI等。这些SPI的接口(比如JDBC中的java.sql.Driver)属于核心类库,一般存在rt.jar包中,由根类加载器加载。而第三方实现的代码一般作为依赖jar包存放在classpath路径下,由于SPI接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI的接口类是由根类加载器加载的,Bootstrap类加载器无法直接加载位于classpath下的具体实现类。由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载SPI的具体实现类。在这种情况下,java提供了线程上下文类加载器用于解决以上问题。
线程上下文类加载器可以通过java.lang.Thread的getContextClassLoader()来获取,或者通过setContextClassLoader(ClassLoader cl)来设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类或资源。
显然这种加载类的方式破坏了双亲委托模型,但它使得java类加载器变得更加灵活。
我们以JDBC中的类为例做一下说明。在JDBC中有一个类java.sql.DriverManager,它是rt.jar中的类,用来注册实现了java.sql.Driver接口的驱动类,而java.sql.Driver的实现类一般都是位于数据库的驱动jar包中的。
Java.util.ServiceLoader的部分源码截图:
沙箱安全机制
java安全模型的核心是Java沙箱,沙箱是一个限制程序运行的环境(沙箱主要限制系统资源的访问,如cpu,内存等等。不同级别的沙箱对这些资源的访问限制也不一样)。沙箱安全机制就是将java代码限定在虚拟机(jvm) 特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对系统造成破坏。
沙箱模型发展历程
Java1.0沙箱安全模型
但是该种机制导致了用户无法执行远程代码访问本地系统文件,所以在Java1.1版本中对安全机制做了如下改动
在Java1.2版本中再次改进了安全机制,增加了代码签名。都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间。
从Java1.6至今引入域的概念,虚拟机会把所有的代码加载到不同系统域或应用域。
沙箱的组成
1、字节码校验器 bytecode verifier
确保java类文件遵循java语言规范,帮助程序实现内存保护。并不是所有类都经过字节码校验器,如核心类。
2、类加载器 class loader
双亲委派机制、安全校验等,防止恶意代码干涉。守护类库边界。
3、存取控制器 access controller
它可以控制核心API对操作系统的存取权限,控制策略可以有由用户指定。
4、安全管理器 security manager
它是核心API和系统间的主要接口,实现权限控制,比存取控制器优先级高。
5、安全软件包 secruity package
java.secruity下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性。包括:安全提供者、消息摘要、数字签名、加密、鉴别等。
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码:用来存储指向一条指令的地址, 也即将要执行的下一条指令代码。
方法区
方法区:Method Area
方法区是被线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义。简单说,所有定义的方法信息都保存在该区域,此区域属于共享区域。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中和方法区无关。
栈
栈是一种FILO(先进后出)类型的数据结构。在虚拟机内存中有两个栈,一个是虚拟机栈,一个是本地方法栈。其中虚拟机栈是用来执行Java执行代码的。而本地方法栈则是为虚拟机使用到的Native方法服务。在java程序运行过程中就是采用栈这种数据结构,先执行main函数也就是main函数先入栈,然后根据调用函数在依次入栈,因为栈是先进后出的,所以最后程序会回到main函数。栈中包含的元素为栈帧,一个栈帧对应一个方法,且栈式线程独立的。
栈帧
栈帧(Stack Frame)是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack) 的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。典型的栈帧结构如图:
堆
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,Java堆区域在JVM启动的时候被创建,其空间大小也就确定了,其也是JVM管理最大的一块内存空间。类加载器读取类文件后,如类、方法、常量、变量、引用类型的真实对象就会被保存在堆中。
堆的分区
创建对象内存图
public class Student { public String name; public int age; public void study(){ System.out.println("在学习"); } public Student(){ } public static void main(String[] args) { Student student1 = new Student(); student1.name ="小明"; student1.age = 13; student1.study(); System.out.println(student1); } }