JVM

简介: jvm是Java Virtual Machine (Java虚拟机) 的缩写,jvm是一种用于计算设备的规范,它是一个虚拟出来的计算机,是通过再实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入了Java虚拟机后,Java语言在不同平台上运行时不需要重新的编译。

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);
    }
}




目录
相关文章
|
3月前
|
存储 Java Unix
深入理解JVM(三)
深入理解JVM(三)
|
7月前
|
Java
|
7月前
|
存储 算法 Java
|
7月前
|
存储 算法 Java
|
存储 缓存 安全
JVM的组成
JVM(Java虚拟机)是Java程序运行的核心组件,它负责将字节码文件解释成可执行代码并提供运行时环境。
119 0
|
7月前
|
算法 Java Linux
深入理解JVM - Shenadoah
深入理解JVM - Shenadoah
86 1
|
7月前
|
存储 Java Linux
|
7月前
|
存储 Oracle Java
一文带你认识JVM
一文带你认识JVM
102 0
|
存储 缓存 算法
JVM初探
JVM初探
110 1
|
存储 Java
Jvm基本组成
了解jvm基本组成
92 0