主流虚拟机: Hotspot, JRockit(Oracle) 相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也目前使用范围最广的Java虚拟机。 J9, JikesRVM(IBM) J9是IBM开发的一个高度模块化的JVM。在许多平台上,IBM J9 VM都只能跟IBM产 一起使用。这不是技术限制,而是许可证限制。 Zulu, Zing (Azul) Zing VM是一个从Sun HoSpot VM fork出来的一个高性能JVM,可以运行在Linux/x86-64平台上。Azul为它重新写了一套GC,也修改了VM内的许多实现细节,所以从我们自己的角度看,与其说它是HotSpot VM的一个变种,还不如把它看作“一个全新的JVM、只是凑巧与HotSpot VM很像”更合适。 Darvik虚拟机和ART虚拟机(Android): Darvik虚拟机每次程序运行时对程序进行编译为字节码,特点安装快运行慢。 ART虚拟机安装时编译为字节码,特点安装慢,运行快 JVM三种主要的存储区 Runtime Data area(运行时数据区): Method area:存储对象描述符 Stack:存储方法帧和基本型数据 Heap:数据存储区 共享式存储区:method area、heap specific存储区:Stack Java堆内存设置: 默认情况下,java内存式1/4物理内存. 常看java进程id使用jps 查看指定java进程的堆内存信息使用:jmap -heap pid JVM参数设置:java -X查看 1.-Xmx //设置最大堆大小 java -Xmx512m Classname 2.-Xms //设置初始堆大小 java -Xms24m Classname 3.-Xss //设置 Java 线程堆栈大小 java -Xss2m ClassName
首先我想从宏观上介绍一下Java虚拟机的工作原理。从最初的我们编写的Java源文件(.java文件)是如何一步步执行的,如下图所示,首先Java源文件经过前端编译器(javac或ECJ)将.java文件编译为Java字节码文件,然后JRE加载Java字节码文件,载入系统分配给JVM的内存区,然后执行引擎解释或编译类文件,再由即时编译器将字节码转化为机器码。主要介绍下图中的类加载器和运行时数据区两个部分。
类加载指将类的字节码文件(.class)中的二进制数据读入内存,将其放在运行时数据区的方法区内,然后在堆上创建java.lang.Class对象,封装类在方法区内的数据结构。类加载的最终产品是位于堆中的类对象,类对象封装了类在方法区内的数据结构,并且向JAVA程序提供了访问方法区内数据结构的接口。如下是类加载器的层次关系图。
注意如上的类加载器并不是通过继承的方式实现的,而是通过组合的方式实现的。而JAVA虚拟机的加载模式是一种委派模式,如上图中的1-7步所示。下层的加载器能够看到上层加载器中的类,反之则不行。类加载器可以加载类但是不能卸载类。说了一大堆,还是感觉需要拿点代码说事。 首先我们先定义自己的类加载器MyClassLoader,继承自ClassLoader,并覆盖了父类的findClass(String name)方法,如下: public class MyClassLoader extends ClassLoader{ private String loaderName; //类加载器名称 private String path = ""; //加载类的路径 private final String fileType = ".class"; public MyClassLoader(String name){ super(); //应用类加载器为该类的父类 this.loaderName = name; } public MyClassLoader(ClassLoader parent,String name){ super(parent); this.loaderName = name; } public String getPath(){return this.path;} public void setPath(String path){this.path = path;} @Override public String toString(){return this.loaderName;} @Override public Class<?> findClass(String name) throws ClassNotFoundException{ byte[] data = loaderClassData(name); return this.defineClass(name, data, 0, data.length); } //读取.class文件 private byte[] loaderClassData(String name){ InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { is = new FileInputStream(new File(path + name + fileType)); int c = 0; while(-1 != (c = is.read())){ baos.write(c); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally{ try { if(is != null) is.close(); if(baos != null) baos.close(); } catch (IOException e) { e.printStackTrace(); } } return data; } } 我们如何利用我们定义的类加载器加载指定的字节码文件(.class)呢?如通过MyClassLoader加载C:\\Users\\Administrator\\下的Test.class字节码文件,代码如下所示: public class Client { public static void main(String[] args) { // TODO Auto-generated method stub //MyClassLoader的父类加载器为系统默认的加载器AppClassLoader MyClassLoader myCLoader = new MyClassLoader("MyClassLoader"); //指定MyClassLoader的父类加载器为ExtClassLoader //MyClassLoader myCLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent(),"MyClassLoader"); myCLoader.setPath("C:\\Users\\Administrator\\"); Class<?> clazz; try { clazz = myCLoader.loadClass("Test"); Field[] filed = clazz.getFields(); //获取加载类的属性字段 Method[] methods = clazz.getMethods(); //获取加载类的方法字段 System.out.println("该类的类加载器为:" + clazz.getClassLoader()); System.out.println("该类的类加载器的父类为:" + clazz.getClassLoader().getParent()); System.out.println("该类的名称为:" + clazz.getName()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
字节码的加载第一步,其后分别是认证、准备、解析、初始化,那么这些步骤又具体做了哪些工作,以及他们会对运行时数据区缠身什么影响呢?如下图所示: 如下我们将介绍运行时数据区,主要分为方法区、Java堆、虚拟机栈、本地方法栈、程序计数器。其中方法区和Java堆一样,是各个线程共享的内存区域,而虚拟机栈、本地方法栈、程序计数器是线程私有的内存区。
写了这么多,感觉还是少一个例子。通过最简单的一段代码解释一下,程序在运行时数据区个部分的变化情况。 public class Test{ public static void main(String[] args){ String name = "best.lei"; sayHello(name); } public static void sayHello(String name){ System.out.println("Hello " + name); } } 通过编译器将Test.java文件编译为Test.class,利用javap -verbose Test.class对编译后的字节码进行分析,如下图所示: 我们在看看运行时数据区的变化: |
|