JVM是java虚拟机简称,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际计算机上仿真模拟各种计算机功能来实现的。也正式因为有了它,java才具有了跨平台特性,”一次编译,到处运行“,每一个操作系统都有对应的JVM,如windows版JVM、linux版JVM等。
我们用eclipse或者ideal编写的代码是.java文件,也是我们通常所称的源代码。源代码编写之后需要经过JVM进行编译,编译成.class文件,也称字节码。Java程序在运行的时候,JVM先将磁盘上的字节码加载到内存,然后再转换成对象也就是对象实例。所以一次完整的java应用程序开发、打包、运行会经过四个阶段。java源文件、jar包(字节码文件)、JVM加载到内存、java对象(对象实例),分别对应java代码编写阶段、java编译阶段、java运行阶段。
所有的.class文件的前4个字节都是魔数,魔数以一个固定值:0xCAFEBABE,放在文件的开头,JVM就可以根据这个文件的开头来判断这个文件是否可能是一个.class文件,如果是以这个开头,才会往后执行下面的操作,这个魔数的固定值是Java之父James Gosling指定的,意为CafeBabe(咖啡宝贝)。紧随着魔数之后的4个字节就是版本号,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version),如52则标识JDK8。再然后就是常量池、访问标志等。
java虚拟机是基于栈的。java字节码是由单字节指令构成,理论上最多256个,但实际应用大概只有200个左右。操作一般分为四大类型。
java虚拟机在运行一段代码的时候,会把所有的用到的变量存到本地变量表也叫局部变量表,需要使用的时候就通过load指令加载到栈上来,运算结束后再存到局部表量表。如果需要把class的文件常量池打印出来就需要通过javap-verbose xx.class,打印出常量池。局部变量名称可能在编译的时候就全部丢失掉了,所以反编译的时候经常会看到局部变量名称是v1、v2这样,如果想要保留局部变量名称,则需要在编译的时候加一个参数即javac-g xx.java。这样在javap -verbose xx.class查看class文件时就能看到局部变量真实的名称了.
JVM是一台基于栈的计算机器。每个线程都有独属于自己的线程栈(JVM Stack),用于存储栈帧(Frame)。每一次方法的调用,JVM都会创建一个栈帧。栈帧时由操作数栈、局部变量数组及一个class引用组成。class引用指向当前方法在运行时常量池中对应的class。JVM方法调用的指令主要有四种:
类加载器体现的类的加载和卸载。类的生命周期分为加载(找class文件,由类jiava载器完成)、验证(验证格式、依赖)、准备(静态字段、方法表)、解析(符号解析为引用)、初始化(构造器、静态变量赋值、静态代码块)、使用、卸载七个步骤。前5个步骤合起来就是类的加载过程,把二三四阶段统称为链接阶段。
类加载器主要就是负责类的加载职责,对于任意一个class,都需要由加载它的类加载器和这个类本身确立其在JVM中的唯一性。JVM为我们提供了三大内置的类加载器,分别是启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader),不同的类加载器负责将不同的类加载到JVM内存之中,并且他们严格遵守父委托机制。加载器有三大特点:双亲委托、负责依赖、缓存加载。应用类公用的类可以放在扩展类加载器路径之上,这样应用在启动的时候就不需要重复添加依赖。
类加载器的第一个特点是双亲委托,指的就是当应用加载器加载一个类的时候,自己不加载,而是先委托给父类加载器进行加载,如果父类加载器也不加载就会一直向上委托直到启动类加载器,如果启动类加载器加载了就会直接将引用返回给子类。如果父类加载器都没有加载,则由子类加载器自己负责加载,并返回引用。
类加载器的第二个特点是负责依赖。例如类加载器在加载一个类的时候,发现这个类还依赖另外两个类,则该加载器也会加载依赖的两个类。
类加载器的第三个特点就是缓存加载。类加载的时候默认只会加载一次,加载之后就会缓存在内存里,下次使用的时候直接在内存拿就可以,就不会重复加载,提高效率。
最上一层的加载器是根加载器,又称为Bootstrap类加载器,该类加载器是最为顶层的加载器,其没有任何父加载器,它是由C++编写的,主要负责虚拟机核心库类的加载,比如说java.lang包就是由根加载器加载的。要验证根加载器加载了哪些jar包,可以通过如下代码实现。根加载器是获取不到引用的,所以第一行会输出null。
package base.classloader;
public class BootStrapClassLoader {public static void main(String args[]) {System.out.println("Bootstrap:"+String.class.getClassLoader());System.out.println(System.getProperty("sun.boot.class.path"));}
}
第二层的加载器是扩展类加载器,它主要用于加载JAVA_HOME下的jre\lib\ext子目录下的类库。扩展类加载器是纯java实现的,它是java.lang.URLClassLoader的子类,它的完整类名是sun.misc.Launcher$ExtClassLoader。扩展类加载器所加载的类库可以通过java.ext.dirs获得。我们也可以将自己的类打成jar包,放到扩展类加载器所在的路径,扩展类加载器就会负责加载我们所需要的jar包。
package base.classloader;
public class BootStrapClassLoader {public static void main(String args[]) {System.out.println(System.getProperty("java.ext.dirs"));}
}
第三层是系统类加载器也就是ApplicationClassLoader,其负责加载classpath下的类库资源。我们在进行项目开发的时候引入的第三方jar包,系统类加载器的父加载器是扩展类加载器,同时它也是自定义类加载器的默认加载器,系统类加载器一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path进行获取。
package base.classloader;
public class ApplicationClassLoader {public static void main(String args[]) {System.out.println(System.getProperty("java.class.path"));System.out.println(ApplicationClassLoader.class.getClassLoader());}
}
也可以通过一段代码,打印出根加载器、扩展加载器、应用加载器分别加载了哪些jar包。在实际业务开展过程中,如果我们发现系统加载的代码和我们预计的代码不一致,我们也可以通过下面这段代码,打印出我们根加载器、扩展加载器、应用加载器分别加载了哪些jar包,验证和预计结构是否一致。
package jeekdemo.part1;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
public class JvmClassLoaderPrintPath {public static void main(String args[]) {URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();System.out.println("启动类加载器");for(URL url:urls) {System.out.println("===>"+url.toExternalForm());} printClassLoader("扩展类加载器",JvmClassLoaderPrintPath.class.getClassLoader().getParent()); printClassLoader("应用类加载器",JvmClassLoaderPrintPath.class.getClassLoader());}
private static void printClassLoader(String string, ClassLoader parent) {System.out.println();if(null != parent) {System.out.println(string+" ClassLoader==>"+parent.toString());printURLForClassLoader(parent);}else {System.out.println(string + " ClassLoader ==> null");}
}
private static void printURLForClassLoader(ClassLoader classLoader) {
// TODO Auto-generated method stubObject ucp = insightField(classLoader,"ucp");Object path = insightField(ucp,"path");List paths = (List)path;for(Object p:paths) {
System.out.println("===>"+p.toString());
}
}private static Object insightField(Object obj, String fName) {Field f = null;try {if(obj instanceof URLClassLoader) {f = URLClassLoader.class.getDeclaredField(fName);}else {f = obj.getClass().getDeclaredField(fName);}f.setAccessible(true);return f.get(obj);}catch(Exception e) {e.printStackTrace();return null;}
}
}
场景一:我们实现的所有自定义类加载器都是ClassLoader的直接或间接子类,java.lang.ClassLoader是一个抽象类,它里面并没有抽象方法,但是有findClass方法,在实现自定义类加载器的时候就需要实现findClass方法。自定义类加载器的实现代码如下:
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/** 自定义的类加载器,在加载器里定义了加载的默认路径* 也支持用户自定义类文件夹路径*/
public class MyClassLoader extends ClassLoader{private final static Path DEFAULT_CLASS_DIR= Paths.get("E:\\classloader1");private final Path classDir;public MyClassLoader() {super();this.classDir = DEFAULT_CLASS_DIR;}public MyClassLoader(String classDir) {super();this.classDir = Paths.get(classDir);}public MyClassLoader(String classDir,ClassLoader parent) {super(parent);this.classDir = Paths.get(classDir);}@Overrideprotected Class> findClass(String name) throws ClassNotFoundException{byte[] classBytes = this.readClassBytes(name);if(null == classBytes || classBytes.length == 0) {throw new ClassNotFoundException("Can not load the class "+name);}return this.defineClass(name, classBytes, 0,classBytes.length);}private byte[] readClassBytes(String name) throws ClassNotFoundException{String classPath = name.replace(".", "/");Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));if(!classFullPath.toFile().exists()) {throw new ClassNotFoundException("The class "+name+" not found.");}try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){Files.copy(classFullPath, baos);return baos.toByteArray();}catch(IOException e) {throw new ClassNotFoundException("load the class "+name+" occurerror.",e);}}@Overridepublic String toString() {return "My ClassLoader";}
}
package base.classloader;
import java.lang.reflect.Method;
/** 由于双亲委托机制,* 如果base.classloader.HelloWorld在当前工作空间* 一定要删掉class和java文件,不然系统在运行的时候* 加载HelloWord的加载器将会是应用加载器,* 而不是自定义的类加载器*/
public class MyClassLoaderTest {public static void main(String args[]) throws ClassNotFoundException,Exception {MyClassLoader classLoader = new MyClassLoader();Class> aClass = classLoader.findClass("base.classloader.HelloWorld");System.out.println(aClass.getClassLoader());Object helloWorld = aClass.newInstance();System.out.println(helloWorld);Method welcomeMethod = aClass.getMethod("welcome");String result = (String) welcomeMethod.invoke(helloWorld);System.out.println("Result:"+result);}
}
package base.classloader;
public class HelloWorld {static {System.out.println("Hello World Class is Initialized");}public String welcome() {return "Hello World";}
}
场景二:自定义一个加载器加载加密后的class文件
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
public class CustomClassLoaderTest extends ClassLoader{public static void main(String args[]) throws Exception {String path = "E:\\cosmic40\\bos-dev-tool\\debug-service\\javademo\\target\\classes\\base\\classloader\\Hello.class";/*String str = ConvertClass2Str(path);byte[] fileByte = str.getBytes();*/byte[] fileByte = ConvertClass2Str(path);//这里可以增加一些代码解密的逻辑CustomClassLoaderTest test = new CustomClassLoaderTest();Class> aclass = test.defineClass("base.classloader.Hello",fileByte,0,fileByte.length);aclass.newInstance();System.out.println(aclass.getClassLoader());}public static byte[] ConvertClass2Str(String path) throws Exception {File file = new File(path);byte[] bfile = Files.readAllBytes(file.toPath());/*String str = new String(bfile);//转为数组return str;*///这里可以添加一些代码加密逻辑return bfile;}
}
package base.classloader;
public class Hello {static {System.out.println("hello world");}
}
jvm在加载类的时候会优先委托父类加载,如果父类不能加载的时候才是自己加载,这就是所谓的双亲委托机制,也称为父委托机制。当一个类加载器被调用了loadClass之后并不会直接将其加载,而是先交给当前类加载器的父类加载器尝试加载直到最顶层的父加载器,然后再依次向下加载。
如果想要用自定义类加载器加载class文件,又不希望在原工程里删除Hello的java文件和class文件,可以采用两种方式来解决。第一,将自定义的类加载器的父加载器指定为扩展类加载器;第二,将自定义类加载器的父加载器定义为null。
研究jdk源码,我们发现类加载器的父委托机制的逻辑主要是由loadClass来控制,有时候由于业务的需要也需要打破这种双亲委托的机制。例如我们想要在程序运行时进行某个模块功能的升级,甚至是在不停止服务的前提下增加新的功能,这就是我们常说的热部署。热部署首先要卸掉加载该模块所有Class类加载器,卸载类加载器会导致所有类的卸载,很显然不能对JVM三大内置的加载器进行卸载,我们只有通过控制自定义类加载器才能做到。前面介绍的用自定义类加载器加载HelloWord类的时候,采用的策略是绕过ApplicationClassLoader的方式去实现,但并没有避免一层一层的委托。实际上,双亲委托机制不是强制性的,我们可以灵活的破坏这种双亲委托机制。
package base.classloader;
import java.lang.reflect.Method;
/**双亲委托机制的破坏案例*/
public class BrokerClassLoaderTest {public static void main(String args[]) throws ClassNotFoundException,Exception {BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader();//Class> aClass = classLoader.findClass("base.classloader.HelloWorld");Class> aClass = classLoader.loadClass("base.classloader.HelloWorld");System.out.println(aClass.getClassLoader());Object helloWorld = aClass.newInstance();System.out.println(helloWorld);Method welcomeMethod = aClass.getMethod("welcome");String result = (String) welcomeMethod.invoke(helloWorld);System.out.println("Result:"+result);}
}
//破坏双亲委托加载器代码
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class BrokerDelegateClassLoader extends ClassLoader{private final static Path DEFAULT_CLASS_DIR= Paths.get("E:\\classloader1");private final Path classDir;public BrokerDelegateClassLoader() {super();this.classDir = DEFAULT_CLASS_DIR;}@Overrideprotected Class> findClass(String name) throws ClassNotFoundException{byte[] classBytes = this.readClassBytes(name);if(null == classBytes || classBytes.length == 0) {throw new ClassNotFoundException("Can not load the class "+name);}return this.defineClass(name, classBytes, 0,classBytes.length);}private byte[] readClassBytes(String name) throws ClassNotFoundException{String classPath = name.replace(".", "/");Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));if(!classFullPath.toFile().exists()) {throw new ClassNotFoundException("The class "+name+" not found.");}try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){Files.copy(classFullPath, baos);return baos.toByteArray();}catch(IOException e) {throw new ClassNotFoundException("load the class "+name+" occurerror.",e);}}@Overrideprotected Class> loadClass(String name,boolean resolve) throws ClassNotFoundException{if(name.equalsIgnoreCase("HelloWorld")) {System.out.println("demo");}synchronized(getClassLoadingLock(name)) {Class> klass = findLoadedClass(name);if(klass == null) {if(name.startsWith("java.") || name.startsWith("javax")) {try {klass = getSystemClassLoader().loadClass(name);}catch(Exception e) {}}else {try {klass = this.findClass(name);}catch(ClassNotFoundException e) {}if(klass == null) {if(getParent()!=null) {klass = getParent().loadClass(name);}else {klass = getSystemClassLoader().loadClass(name);}}}}if(null == klass) {throw new ClassNotFoundException("The class "+name+" not found.");}if(resolve) {resolveClass(klass);}return klass;}}
}
package base.classloader;
public class HelloWorld {static {System.out.println("Hello World Class is Initialized");}public String welcome() {return "Hello World";}
}
loadClass有几个核心要点:
每一个类加载器实例都有各自的命名空间,命名空间是由该加载器及其所有父加载器所工构成的,因此在每个类加载器中同一个class都是独一无二的,类加载器命名空间代码如下。
package base.classloader;
public class NameSpace {public static void main(String args[]) throws ClassNotFoundException {ClassLoader classLoader = NameSpace.class.getClassLoader();Class> aClass = classLoader.loadClass("base.classloader.HelloWorld");Class> bClass = classLoader.loadClass("base.classloader.HelloWorld");System.out.println(classLoader.toString());System.out.println(aClass);System.out.println(bClass);System.out.println(aClass == bClass);}
}
最后输出的结果是true,也就是不管load多少次,最后返回的都是同一份class对象,aclass和bclass只是对class的引用。但是,使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对象。所以,同一个class实例只能在同一个类加载器命名空间之下是唯一的。
在编码代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成。
由于运行时包的存在,JVM规定了不同的运行时包下的类彼此之间不可以进行访问。但实际上我们的业务代码是由应用加载器加载的,但类似String是由根加载器加载的,虽然是不同的类加载加载,但依然我们能够正常的引用。这是因为每一个类在经过ClassLoader加载之后,在虚拟机中都会有对应的class实例,如果某个类C被类加载器CL加载,那么CL就被称为C的初始类加载器。JVM为每一个类加载器维护了一个列表,该列表中记录了将该类加载器作为初始类加载器的所有class,在加载一个类时,JVM使用这些列表来判断该类是否已经被加载过,是否需要首次加载。根据JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始加载器,例如一个类经过自定义加载器、应用加载器、扩展加载器、根加载器,那这些类都是该类的初始类加载器,JVM会为每个类加载器的列表中添加该类class类型。所以,类似String对象,虽然是由根加载器加载,但我们也能正常的引用。
在JVM启动过程中,JVM会加载很多类,在运行期间同样也会加载很多类。JVM规定了一个Class只有满足三个条件才能被GC回收,也就是类卸载。
JDK的核心库提供了很多SPI(Service Provider Interface),常见的SPI包括JDBC、JCE、JNDI、JAXP和JBI等,JDK只规定了这些接口之间的逻辑关系,但不提供具体实现。具体的实现是由第三方厂商来提供。例如JDBC,java使用JDBC这个SPI完全透明了应用程序和第三方厂商数据库驱动的具体实现,不管数据库类型如何切换,应用程序只需要替换JDBC的驱动jar包以及数据库的驱动名称即可,而不用做任何的更新。
这样做的好处是JDBC提供了高度抽象,应用则只需要面向接口编程即可,不用关心各大数据库厂商提供的具体实现。但问题在于java.sql中的所有接口都是由JDK提供的,加载这些接口的类加载器是根加载器,第三方厂商提供的类驱动则是由系统类加载器加载的,由于JVM类加载器的双亲委托机制,比如Connection、Statement、RowSet等都是由根加载器加载,第三方的JDBC驱动包中的实现不会被加载。例如mysql的jdbc源码,我们可以看到mysql包中的Dirver实现了java.sql.Driver,而java.sql.Driver是由根加载器加载的,mysql的Driver是属于第三方服务,由应用加载器加载。因此,我们可以看到在mysql的Driver类的静态代码块,需要首先将Driver注册到DriverManager中,再调用DriverManager的getConnection等方法时,会调用当前的线程加载器去加载Driver,也就是说父委托变成了子委托,也就打破了双亲委托模型,也就相当于是绕了一个大圈。在实际开发过程中,一般不要轻易破坏双亲委托机制。
JVM虚拟机在运行的时候,每个线程都有自己独立的线程栈,并且只能访问自己的线程栈,每个线程都不能访问其他线程的局部变量(方法内变量)。所有原生类型的局部变量都存储在线程栈中,因此对其他线程都是不可见的。如果两个线程需要共享局部变量,线程可以将一个原生变量值的副本传给另外一个线程,但不能共享原生局部变量本身。堆内存中就包括了java代码中创建的所有对象,不管是哪个线程创建的,这其中也包括了包装类型如Byte、Integer、Long等。不管是创建一个对象并将其赋值给局部变量,还是赋值给另外一个对象的成员变量,创建的对象都是会保存在堆中。
如果局部变量是原生类型的,那么它的全部内容就全部保留在线程栈上。如果是对象的引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。针对对象本身来讲,对象的成员变量和对象本身都是一起存在堆上,不管其成员变量是原生数值还是对象的引用。此外,类的静态变量和类定义一样也是保存在堆中。
总之,方法中使用的原生数据类型和对象引用地址存储在栈上,对象、对象成员与类定义、静态变量在堆上。堆内存也称为共享堆,堆中的所有对象都可以被所有线程访问,只要他们能拿到对象的引用地址。如果一个线程可以访问某个对象,则也可以访问该对象的成员变量。如果两个线程同时调用某个对象的同一个方法,则他们都可以访问到对象的成员变量,但每个线程的局部变量副本是独立的。
JVM每启动一个线程,JVM就会在栈空间分配对应的线程栈,线程栈也叫java的方法栈。如果使用JNI方法,则会分配一个单独的本地方法栈(Native Stack)。线程栈会包括多个栈帧。线程执行过程中,一般会有多个方法组成调用栈,如A调用B,B调用C,每执行一个方法就会创建一个栈帧。栈帧是一个逻辑上的概念,包括局部变量表、操作数栈等,具体大小在一个方法编写完成后基本上就能确定,比如返回值需要有一个空间存放,每个局部变量都需要对应的地址空间,此外还有给指令使用的操作数栈,以及class指针(标识这个栈帧对应的是哪个类的方法,指向非堆里面的Class对象)。
堆内存是所有线程共享共用的内存空间,JVM将堆内存分为年轻代和老年代两个部分。年轻代划分为3个内存池,新生代(Eden区)和存活区(S0和S1),S0和S1总有一个是空的。
还有一块内存区域是非堆(Non-Heap),它本质上也是堆,只不过不归GC管,它里面主要包括三个部分分别是Metaspace(元数据区,包括常量池等)、CSS(存放class信息)、Code Cache(存放JIT编译器编译后的本地机器代码)。
JVM自己在运行的时候也需要占用一定的内存,所以在启动java进程调整内存的时候要把这一步空间预留出来,一般情况可以把JVM内存设置成物理内存的60%到80%之间,不然很容易发生OOM异常。
JMM主要是用于屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。JMM规范了Java虚拟机与计算机内存是如何协同工作的,规定了一个线程如何和何时可以看到由其他线程修改后的共享变量的值,以及在必须的时候如何同步的访问共享变量。
java进程是操作系统众多进程中的一个。JMM就是java进程的内存模型。JMM主要包括栈、堆、非堆、JVM本身运行所需内存四大部分。其中java堆是占比最大的,也是内存回收最频繁的区域。
JVM预置了很多的参数,大概有一千多个,为了使JVM在各种不同场景运行的更高效,我们可以调整JVM的启动参数,把JVM调校成适合当前程序运行的状态。
-server
-Dfile.encoding=UTF-8
-Xmx8g
-XX:+UseG1GC
-XX:MaxPermSize=256m
JVM启动参数一般分为一下以下几种:
JVM启动参数按作用域的范围也可分为6种,分别是系统属性参数、运行模式参数、堆内存设置参数、GC设置参数、分析诊断参数、JavaAgent参数。
系统属性参数主要是给程序提供一些环境变量和传递一些系统内需要使用的一些开关或者数值。系统参数是以-D开头,系统参数的的指定跟在操作系统上配置的环境变量是等价的,如果指定了JVM启动系统属性参数,又配置了环境变量,则读取JVM配置的系统属性参数。但系统配置的环境变量是对所有java进程生效,但系统属性参数只有当前java进程有效。也可以通过系统属性参数给启动的进程传参数,系统在运行的时候可以读取到对应参数。
JVM运行模式参数主要有以下几种
GC参数是最多的,也是最复杂的,比较常规的有以下几种
JVM运行异常时,需要记录异常数据便于分析。
Agent是JVM的一项特性,可以通过无侵入式方式来做很多事情,如注入AOP代码,执行统计等,权限比较大。