关于类的加载:java
Java虚拟机与程序的生命周期:linux
在以下几种状况下,Java虚拟机将会结束生命周期:程序员
类的加载、链接与初始化:算法
加载:查找并加载类的二进制数据数据库
链接: api
注:
1.类的静态变量或类的静态方法,一般能够看作全局的,由类去直接调用。此时仍是个类的概念,不存在对象。
2.关于默认值问题:
class Test{
public static int a = 1;
}
中间过程: Test类加载到内存的过程当中,会给a分配一个内存。而后将a初始化为默认值0(整型变量)
初始化: 为类的静态变量赋予正确的初始值数组
class Test{ public static int a = 1; } 此时的a才真正成为1了
类的使用与卸载安全
使用: 类的方法变量使用等服务器
卸载: class字节码文件,加载到内存里面。造成了本身的数据结构,驻留在内存里面。能够销毁掉。卸载到了就不能进行new 对象了。网络
整体流程:
Java程序对类的使用方式分为两种:
全部的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才初始化他们。即初始化只会执行一次。
主动使用,七种(非精确划分,大致划分):
好比: class Parent{} class Child extends Parent{} 初始化Child时候,先去初始化Parent
Java虚拟机启动时候,被标明为启动的类,即为有main方法的类,也会主动使用
注:
1.java.lang.invoke.MethodHandle实例的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic句柄对应的类没有初始化,则初始化
2.1.7开始提供了对动态语言的支持。特别的JVM平台上经过脚本引擎调用JS代码(动态语言)。
注:助记符了解便可
除了以上七种状况,其余使用Java类的方式都被看作是对类的被动使用,都不会致使类的初始化。
类的加载:
类的加载指的是将类 .class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,而后在内存中建立一个java.lang.Class对象(规范并说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中,JVM没有规范这个)用来封装类在方法区内的数据结构。
引伸:一个类无论生成了多少实例,全部的实例对应只有一份Class对象。 Class对象是面镜子,能反映到方法区中的Class文件的内容、结构等各类信息。
加载.class文件的方式:
public class MyTest1 { public static void main(String[] args) { System.out.println(MyChild1.str1); // System.out.println(MyChild1.str2); } } class MyParent1{ //静态成员变量 public static String str1 = "str1"; // 静态代码块(程序加载初始化时候去执行) static { System.out.println("MyParent1 -----> static block running"); } } class MyChild1 extends MyParent1{ //静态成员变量 public static String str2 = "str2"; static { System.out.println("MyChild1 -----> static block running"); } }
str1 子类调用了继承到的父类的str1,子类的静态代码块没有执行。str1是父类中定义的。MyParent1的主动使用,可是没有主动使用MyChild1. 总结:看定义的!
str2 能够执行,同时初始化子类时候,父类会主动使用。全部的父类都会被初始化!
MyTest1是一个启动类,主动使用。先加载之。
总结:
引伸: -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来。能够看到类的加载状况。
打印: 虚拟机在当前启动状况下所加载的类的信息。
总结设置方式:
全部JVM参数都是: -XX: 开头
相似于Boolean类型的开关:
-XX:+<option> 表示开启option选项
-XX: - <option> 表示关闭option选项
赋值:
-XX:<option>=<value>, 表示将option选项的值设置为value
关于常量:
public class MyTest2 { public static void main(String[] args) { System.out.println(MyParent2.str); } } class MyParent2{ // final修饰成为常量 public static final String str = "hello world"; static { System.out.println("MyParent2 ----> run"); } }
在编译阶段这个常量被存入到 调用这个常量的方法所在的类的常量池中。
本例中:
“hello world”是一个常量,会放置到MyTest2类的常量池中。
这里指的时将常量存放到了MyTest2的常量池汇总,以后MyTest2与MyParent2就没有任何关系了
甚至,极端一些。咱们能够将MyParent3的class文件删除。(编译完毕后,把class字节码删除)
总结:
引伸反编译: javap -c 类的全路径名字
助记符引伸:
助记符是在rt.jar中相关类去实现的。
若是常量的值,在编译器不能肯定下来呢?
public class MyTest3 { public static void main(String[] args) { System.out.println(MyParent3.str); } } class MyParent3 { public static final String str = UUID.randomUUID().toString(); static { System.out.println("MyParent3 -- run"); } }
此时放在MyTest3类的常量池中没有意义的。
总结:
当一个常量值并不是编译期间能够肯定的,那么其值就不会被放到调用类的常量池中。这时在程序运行时,会致使主动使用这个常量所在的类,显然会致使这个类被初始化。
new对象实例状况:
public class MyTest4 { public static void main(String[] args) { MyParent4 myParent4 = new MyParent4(); } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
对这个类的主动使用。
若是屡次new,只会初始化一次。首次主动使用。
数组状况:
public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
不在七种状况范围内。不会初始化!
不是MyParent4的实例!
到底建立的什么实例?getClass!,数组的实例究竟是个啥玩意儿?
public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; //看看是啥 Class<? extends MyParent4[]> aClass = myParent4s.getClass(); System.out.println(aClass); } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
Java虚拟机在运行期,建立出来的类型。是个数组类型。有点相似动态代理
数组类型也是比较特殊的。[Lxxxx
二维数组也是同样的特殊
看下父类型:
public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; //看看是啥 System.out.println(myParent4s.getClass().getSuperclass()); } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
父类型实际上是Object
总结:
对于数组实例来讲,其类型是由JVM在运行期动态生成的
动态生成的类型,其父类就是Object
对于数组来讲,JavaDoc常常将构成数组的元素为Component,实际上就是将数组下降一个维度后的类型。
看下原生类型的数组:
public class MyTest4 { public static void main(String[] args) { int[] ints = new int[3]; System.out.println(ints.getClass()); System.out.println(ints.getClass().getSuperclass()); } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
助记符:
anewarray: 表示建立一个引用类型的(好比类、接口、数组)数组,并将其引用值压如栈顶。
newarray: 表示建立一个指定的原始类型(如:int,float,char等)的数组,并将其引用值压入栈顶。
以上所总结的是类与类之间的关系,包括继承的。下面接口的特色:
public class MyTest5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyParent5 { public static int a = 5; } interface MyChild5 extends MyParent5 { public static int b = 6; }
接口是没有静态代码块的。能够经过手动删除class文件来证实之。
public class MyTest5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyParent5 { public static int a = 5; } interface MyChild5 extends MyParent5 { // 只有在运行时候才会赋值,会放到MyTest5的常量池里面。若是Class删除了,运行时候就会报错! public static int b = new Random().nextInt(2); }
结论:
public class MyTest6 { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); System.out.println("counter"+ instance.counter1); System.out.println("counter"+ instance.counter2); } } class Singleton{ public static int counter1; public static int counter2 = 0; private static Singleton singleton = new Singleton(); private Singleton(){ counter1++; counter2++; } public static Singleton getInstance(){ return singleton; } }
分析: 先赋值: 默认的0 和 给定的0,而后构造方法进行++操做。
若是更改位置:
public class MyTest6 { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); System.out.println("counter1-->"+ instance.counter1); System.out.println("counter2-->"+ instance.counter2); } } class Singleton{ public static int counter1; private static Singleton singleton = new Singleton(); private Singleton(){ counter1++; counter2++; System.out.println(counter1); System.out.println(counter2); } public static int counter2 = 0; public static Singleton getInstance(){ return singleton; } }
按照从上到下的顺序进行初始化。
类主动使用时候,先准备,给类的静态变量赋初始值。
此时:
counter1 初始值 0
singleton 初始值 null
counter2 初始值 0
接着调用静态方法 getInstance时候,赋初始值。
sigleton 会指向一个实例,而后执行私有构造方法。
而后执行到 public static int counter2 = 0时候,显示赋值0了。
总结:
先准备
再初始化: 根据类里面代码的顺序去执行的.真正的赋值(准备为其提供初始值,要不谈不上作++操做)
画个图:
关于类的实例化:
为对象分配内存,即为new对象,在堆上面。
为实例变量赋默认值、为实例变量赋正确的初始值都跟静态变量似的了。赋予默认值以后,再去赋予开发者指定的值。
类的加载:
Class是反射的入口。像一面镜子同样。
有两种类型的类加载器:
1.Java虚拟机自带的加载器
2.用户自定义的类加载器
类的加载:
类加载器并不须要等到某个类被“首次主动使用”时候再加载它
注:
类的验证:
类被加载后,就进入链接阶段。链接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时的环境中去。
类的验证的内容:
在准备阶段:
初始化阶段:
类的初始化步骤:
只有当程序访问的静态变量或静态方法确实在当前类或当前接口定义时,才能够认为是对类或接口的主动使用。
调用ClassLoader类的loadClass方法加载一个类,并非对类的主动使用,不会致使类的初始化。
除了以上虚拟机自带的加载器外,用户还能够定制本身的类加载器。Java提供了抽象类java.lang.ClassLoader,全部用户自定义的类加载器都应该继承ClassLoader类
引伸看下这个例子:
public class MyTest { public static void main(String[] args) { System.out.println(MyChild.b); } } interface MyParent{ public static int a = 5; } interface MyChild extends MyParent{ public static final int b = 8; }
分析:
MyTest类有main函数。会主动使用,先去加载。
接口和类实际上是不一样的,以下:
加载层面:
若是是类的话,MyChild确定会被加载。若是是接口的话,不会被加载。
若是把b 修改成 Random(运行期才知道的值)。会将Parend 和 Child都加载. 很重要的一点是变量是编译器的仍是运行期才能肯定的
若是 parent和child都是final,test用到的常量会放入本身的常量池中,则不会对parent和child进行加载了。
若是把接口换作class,则存在加载,不加载的话必须是final的!
总结出了final关键字的区别小结:
final修饰后,哪一个类去主动调用就将这个常量放入到本身类的常量池里面。
Remember:
block 优先 构造函数执行,每次都执行。
证实初始化一个类时候,不会初始化他的接口:
public class MyTest5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyParent5 { public static Thread thread = new Thread(){ { System.out.println("MyParent5 Thread =========="); } }; } interface MyChild5 extends MyParent5 { public static int b = 6; } class C{ { System.out.println("hello c{block}"); } public C(){ System.out.println("hello c(construct)"); } }
若是将父子的interface 改为class 则会初始化父类
当一个类被初始化时候,他所实现的类是不会被初始化的。
继续看下面例子:
public class MyTest5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyGrandPa{ public static Thread thread = new Thread(){ { System.out.println("MyGrandPa Thread =========="); } }; } interface MyParent5 extends MyGrandPa{ public static Thread thread = new Thread(){ { System.out.println("MyParent5 Thread =========="); } }; } interface MyChild5 extends MyParent5 { public static int b = 6; } class C{ { System.out.println("hello c{block}"); } public C(){ System.out.println("hello c(construct)"); } }
总结:
类加载器的双亲委派机制:
在双亲委派机制中,各个加载器按照父子关系造成了树形结构,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器。
若是有一个类加载器可以成功加载Test类,那么这个类加载器被称为定义类加载器,全部可以成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始化类加载器。(了解便可)
public class MyTest7 { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz = Class.forName("java.lang.String"); System.out.println(clazz.getClassLoader()); Class<?> mClazz = Class.forName("com.jvm.t1.M"); System.out.println(mClazz.getClassLoader()); } } //位于工程的classPath目录地址下 class M{ }
以下例子:
package com.jvm.t1; public class MyTest9 { static { System.out.println("MyTest9 static block"); } public static void main(String[] args) { System.out.println(Child.b); } } class Parent{ static int a = 3; static { System.out.println("parent static block"); } } class Child extends Parent{ static int b = 4; static { System.out.println("chile static block"); } }
便于查看加载过程清晰:
输出结果:
看下面的例子:
public class MyTest10 { static { System.out.println("MyTest10 static block"); } public static void main(String[] args) { //声明类型的使用,并非主动使用 Parent2 parent2; System.out.println("-------"); parent2 = new Parent2(); System.out.println("---------"); System.out.println(parent2.a); System.out.println("---------"); System.out.println(Child2.b); } } class Parent2{ static int a = 3; static { System.out.println("Parent2 static block"); } } class Child2 extends Parent2{ static int b = 4; static { System.out.println("Child2 static block"); } }
使用child时候,parent已经被初始化了,只会初始化一次。
总结:
初始化一次就OK了。
看下面例子:
class Parent3{ static int a = 3; static { System.out.println("Parent3 static block"); } static void doSomeThing(){ System.out.println("do something"); } } class Child3 extends Parent3{ static { System.out.println("Child3 static block"); } } public class MyTest11 { public static void main(String[] args) { //访问父类的。调用父类的Parent的(主动使用) System.out.println(Child3.a); //访问的父类的。调用父类的Parent的(主动使用) Child3.doSomeThing(); } }
总结:
看下面例子:
class CL{ static { System.out.println("static block class CL"); } } public class MyTest12 { public static void main(String[] args) throws ClassNotFoundException { //系统类加载器(应用类加载器) ClassLoader classLoader = ClassLoader.getSystemClassLoader(); //指定加载的类 //这个不会致使类的初始 Class<?> clazz = classLoader.loadClass("com.jvm.t1.CL"); System.out.println(clazz); System.out.println("-------"); //类的初始化,反射致使类的初始化 clazz = Class.forName("com.jvm.t1.CL"); System.out.println(clazz); } }
总结:
关于双亲委派机制:
public class MyTest13 { public static void main(String[] args) { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); while (null != systemClassLoader){ systemClassLoader = systemClassLoader.getParent(); System.out.println(systemClassLoader); } } }
结论:
在HotSpot中,BootStrap ClassLoader使用null表示的.(启动类加载器)
看下面例子:
public class MyTest14 { public static void main(String[] args) { //获取上下文的类加载器。线程建立者提供的。(有默认值的) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); System.out.println(contextClassLoader); } }
类型是APPClassLoader,加载应用的类加载器(系统类加载器)。
看下面的例子:
public class MyTest14 { public static void main(String[] args) throws IOException { //获取上下文的类加载器。线程建立者提供的。(有默认值的) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); //存在磁盘上的字节码(磁盘上的目录) String resourceName = "com/jvm/t1/MyTest13.class"; //给定名字的全部资源(图片、音频等) Enumeration<URL> resources = contextClassLoader.getResources(resourceName); while (resources.hasMoreElements()){ URL url = resources.nextElement(); System.out.println(url); } } }
获取ClassLoader的途径:
咱们本身定义的类,APPClassLoader:
public class MyTest14 { public static void main(String[] args) throws IOException { Class<MyTest14> myTest14Class = MyTest14.class; System.out.println(myTest14Class.getClassLoader()); } }
public class MyTest14 { public static void main(String[] args) throws IOException { Class<String> stringClass = String.class; System.out.println(stringClass.getClassLoader()); } }
String 这个类位于rt.jar
用户自定义的类加载器都直接或间接的从ClassLoader类继承下来。
数组类的Class对象并非由类加载器建立的,运行时因为Java虚拟机自动建立的。只有数组如此
public class MyTest15 { public static void main(String[] args) { String[] strings = new String[2]; System.out.println(strings.getClass().getClassLoader()); System.out.println("--------------"); MyTest15[] myTest15s = new MyTest15[12]; System.out.println(myTest15s.getClass().getClassLoader()); System.out.println("--------------"); int[] ins = new int[2]; System.out.println(ins.getClass().getClassLoader()); } }
总结:
本身定义类加载器,看下面例子:
public class MyTest16 extends ClassLoader { private String classLoaderName = ""; private String fileExtension = ".class"; public MyTest16(String classLoaderName) { super(); // 将系统类加载器当作该类加载器的父类加载器 this.classLoaderName = classLoaderName; } public MyTest16(ClassLoader parent, String classLoaderName) { super(parent); //显示指定该类的加载器的父类加载器 this.classLoaderName = classLoaderName; } private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { //注意win和linux this.classLoaderName = this.classLoaderName.replace(".", "/"); is = new FileInputStream(new File(name + this.fileExtension)); baos = new ByteArrayOutputStream(); int ch ; while (-1 != (ch = is.read())) { baos.write(ch); } // 字节数组输出流转换成字节数组 data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e) { e.printStackTrace(); } } return data; } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); //返回Class对象 return this.defineClass(className, data, 0 , data.length); } public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException { //内部底层的api已经被咱们重写了 Class<?> clazz = classLoader.loadClass("com.jvm.t1.MyTest15"); Object object = clazz.newInstance(); System.out.println(object); } @Override public String toString() { return "[" + this.classLoaderName + "]"; } public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException { MyTest16 loader1 = new MyTest16("loader1"); test(loader1); } }
其实此时咱们定义的 findClass是没有被调用的!觉得双亲委派机制,让父类去加载了!
看下面例子:
public class MyTest16 extends ClassLoader { private String classLoaderName = ""; private String fileExtension = ".class"; private String path; public MyTest16(String classLoaderName) { super(); // 将系统类加载器当作该类加载器的父类加载器 this.classLoaderName = classLoaderName; } public void setPath(String path){ this.path = path; } public MyTest16(ClassLoader parent, String classLoaderName) { super(parent); //显示指定该类的加载器的父类加载器 this.classLoaderName = classLoaderName; } private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; className.replace(",","/"); try { //注意win和linux this.classLoaderName = this.classLoaderName.replace(".", "/"); //指定磁盘全路径 is = new FileInputStream(this.path + new File(className + this.fileExtension)); baos = new ByteArrayOutputStream(); int ch ; while (-1 != (ch = is.read())) { baos.write(ch); } // 字节数组输出流转换成字节数组 data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e) { e.printStackTrace(); } } return data; } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { System.out.println("findClass invoked:" + className); System.out.println("class loader name" + this.classLoaderName); byte[] data = this.loadClassData(className); //返回Class对象 return this.defineClass(className, data, 0 , data.length); } @Override public String toString() { return "[" + this.classLoaderName + "]"; } public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // 建立自定义类加载器 名字“loader1” 父类加载器是系统类加载器 MyTest16 loader1 = new MyTest16("loader1"); //此路径为classPath,故 findClass方法不会被调用执行! 若是换个路径,不是classPath就会去执行了! loader1.setPath("D:\\eclipse_pj\\dianshang\\jvmTest\\out\\production\\jvmTest\\"); Class<?> clazz = loader1.loadClass("com.jvm.t1.MyTest15"); System.out.println("class:"+ clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); } }
委托给父类,父类去classPath目录下面找,找到了加载之。
关于命名空间:
关于类的卸载:
加载 <----> 卸载
看下面的例子:
public class MySample { MySample(){ System.out.println("MySample is loaded by"+ this.getClass().getClassLoader()); MyCat myCat = new MyCat(); } }
public class MyCat { public MyCat() { System.out.println("MyCat is loaded by" + this.getClass().getClassLoader()); } }
public class MyTest17 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyTest16 loader1 = new MyTest16("loader1"); //要加载的类 Class<?> clazz = loader1.loadClass("com.jvm.t1.MySample"); System.out.println("clazz"+ clazz.hashCode()); //若是注释掉改行,那么并不会实例化MySample对象,即MySample构造方法不会被调用 // 所以不会实例化MyCat对象,即没有对MyCat进行主动使用,这里就不会加载MyCat class Object object = clazz.newInstance();// new instance 没有任何参数。调用无参构造方法 } }
关于命名空间的说明:
public class Test3 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { MyTest16 loader1 = new MyTest16("loader1`"); MyTest16 loader2 = new MyTest16("loader2`"); loader1.setPath("/User/test/"); loader1.setPath("/User/test/"); //加载相同的类。(都委托为appClassLoader了) Class<?> clazz1 = loader1.loadClass("com.jvm.test.Test"); //加载过了 Class<?> clazz2 = loader2.loadClass("com.jvm.test.Test"); // 都是app加载的,双亲委派 System.out.println(clazz1 == clazz2); Object o1 = clazz1.newInstance(); Object o2 = clazz2.newInstance(); Method setMyPerson = clazz1.getMethod("setMyPerson", Object.class); //执行o1的方法,参数是o2 setMyPerson.invoke(o1, o2); } }
状况1.若是 class字节码在classPath,返回 true。 执行成功。(读者自行考虑,提示双亲委派)
状况2.若是 class字节码只在:"/User/test/" 。返回false。执行报错。
缘由
双亲委派的好处:
知识总结:
简单看下:
public class test4 { public static void main(String[] args) { System.out.println(ClassLoader.class.getClassLoader()); //扩展类 System.out.println(Launcher.class.getClassLoader()); } }
能够本身作系统类加载器。略。须要控制台指令显示指定
经过改变属性,提示:
System.getProperty("java.system.class.loader")
引伸:
getSystemClassLoader()
OpenJDK是JDK开源版本。
解析Class.forName:
其实:Class.forName("Foo") 等价于 Class.forName("Foo",true, this.getClass().getClassLoader() )
关于线程上下文的类加载器: Thread.currentThread().setContextClassLoader(sys)
做用就是改变双亲委派模型在某些场景下不适用的状况。
看下面例子:
public class MyTest24 { public static void main(String[] args) { System.out.println(Thread.currentThread().getContextClassLoader()); System.out.println(Thread.class.getClassLoader()); // 路径位置致使的 } }
当前类加载器(Current ClassLoader)
每一个类都会使用本身的类加载器(即加载自身的类加载器)去加载其它类(指的是所依赖的类):
若是ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY还没有被加载)
线程上下文类加载器:
线程上下文类加载器的重要性:
应用场景:
SPI(Service Provider Interface)
父ClassLoader可使用当前线程Thread.currentThread().getContexClassLoader() 所指定的ClassLoader加载的类,这就改变了父ClassLoader不能使用子ClassLoader或是其余没有直接父子关系的ClassLoader加载的类的状况。
线程上下文类加载器就是当前线程的Current ClassLoader
在双亲委派模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。可是对于SPI来讲,有些接口是Java类核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不一样的jar包(厂商提供。
Java的启动类加载器是不会加 载其余来源你的Jar包 ,这样的传统的双亲委派模型就没法知足SPI的要求。而经过给当前线程设置上下文类加载器,就能够由设置的上下文类加载器来实现对于接口实现类的加载。
总结:接口是启动类加载器加载的, 实现类应用类加载器加载,经过给当前的线程设置上下文类加载器,实现对于接口实现类的加载,打破了双亲委派模型如今。(框架开发,底层开发会用到)
(JDK中没有对于JDBC的任何实现,除了传统的接口以外,具体实现都是由厂商趋势线的,好比MySQL。)
看下面代码:
public class MyTest25 implements Runnable { private Thread thread; public MyTest25(){ thread = new Thread(this); thread.start(); } @Override public void run() { // 获取到上下文类加载器 ClassLoader classLoader = this.thread.getContextClassLoader(); this.thread.setContextClassLoader(classLoader); System.out.println("Class:"+classLoader.getClass()); System.out.println("Class:"+classLoader.getParent().getClass()); } public static void main(String[] args) { MyTest25 myTest25 = new MyTest25(); } }
没有设置,因此线程将继承父线程的上下文类加载器。
线程上下文类加载器的通常使用模式(获取 - 使用 - 还原)
注意:若是一个类由A加载器加载,那么这个类的依赖也是由相同的类加载器加载的(若是该依赖以前没有被加载过的话)
ContextClassLoader的做用就是为了破坏Java的类加载委托机制
当高层提供了统一的接口让底层去实现,同时又要在高层加载(或者实例化)低层的类时候,就必需要经过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类
看下面例子:
public class MyTest26 { public static void main(String[] args) { //设置下 // Thread.currentThread().setContextClassLoader(MyTest26.class.getClassLoader()); ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); Iterator<Driver> iterator = loader.iterator(); while (iterator.hasNext()){ Driver driver = iterator.next(); System.out.println("driver" + driver.getClass() + ", loader" + driver.getClass().getClassLoader() ); } System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader()); } }
对于能编译成class字节码的代码,class的规范,合法性保证好了就OK了。
对于Idea编译器,是很是熟悉class字节码了,能够为所欲为的反编译。
对于java代码:
public class MyTest1 { private int a = 1; public int getA() { return a; } public void setA(int a) { this.a = a; } }
idea看字节码:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.jvm.t1.t2; public class MyTest1 { private int a = 1; public MyTest1() { } public int getA() { return this.a; } public void setA(int a) { this.a = a; } }
经过反编译指令:
看到三个方法:其中一个是默认的构造方法。
详细查看字节码信息:输入
javap -c com.jvm.t1.t2.MyTest1
Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1 {
//构造方法 public com.jvm.t1.t2.MyTest1();
//下面都是助记符 Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return public int getA(); Code: 0: aload_0 1: getfield #2 // Field a:I 4: ireturn public void setA(int); Code: 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return }
看下面指令:
javap -verbose com.jvm.t1.t2.MyTest1
Classfile /D:/eclipse_pj/dianshang/jvmTest/out/production/jvmTest/com/jvm/t1/t2/MyTest1.class Last modified 2019-10-20; size 473 bytes MD5 checksum c5b1387c6f6c79b14c1b6a5438da3b29 Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
// 常量池: 占据至关大的比重 Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/jvm/t1/t2/MyTest1.a:I #3 = Class #22 // com/jvm/t1/t2/MyTest1 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/jvm/t1/t2/MyTest1; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest1.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/jvm/t1/t2/MyTest1 #23 = Utf8 java/lang/Object
//方法的描述 { public com.jvm.t1.t2.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 5: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/jvm/t1/t2/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jvm/t1/t2/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 12: 0 line 13: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/jvm/t1/t2/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java"
使用如上的这个命令分析字节码时候,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。
备注:
魔数: 全部的.class字节码文件的前4个字节都是魔数,魔数值为固定值: 0xCAFEBABE。
魔数以后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。
常量池(constant pool): 紧接着主板号以后就是常量池入口。一个Java类中定义的不少信息都是由常量池来维护和描述的。常量池在整个字节码文件中占的比重最大,里面的信息会被不少地方引用到。至关于把常量集中在一个地方,其余地方用到时候去引用之。经过Index找到常量池中特定的常量。能够将常量池看作是class文件的资源仓库。好比:Java类总定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用量。
注意:常量池!里面存放的不必定都是常量。也有变量信息。
常量池的整体结构: Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同组成。常量池数量紧跟在主版本后面,占据2个字节;常量池数组则紧跟在常量池数量以后。常量池数组和通常的数组不一样的是,常量池数组中不一样的元素的类型,结构都是不一样的,长度固然也就不一样;可是每一种元素的数都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时候,会根据这个u1类型来获取元素的具体类型。值得注意的是:常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不使用)。目的是知足某些常量池索引值的数据在特定状况下须要表达 【不引用任何一个常量池】的含义。根本缘由在于,索引为0也是一个常量(保留常量)。只不过它不位于常量表中,这个常量就对应null值。因此常量池的索引从1而非从0开始。
以下,从1开始:
常量池中数据类型:
在JVM规范中,每一个变量/字段都有描述信息,描述信息主要的做用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和表明无返回值的void类型都
用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积。对于基本数据类型,JVM都只使用一个大写字母来表示,以下所示:
B ---> byte C --> char D ---> doube F ---> float I --> int J --long S --> short Z --> boolean V --> void
L --->对象类型 ,如: L java/lang/String
对于数组类型来讲,每个维度使用一个前置的 [ 来表示,如 int[ ] 被记录为 [I , String[][] 被记录为[[ Ljava/lang/String
用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组以内,如方法:
get getName (int id, String name)描述为:
常量池里面存储的各类 index 和 信息
Java字节码总体结构:
完整Java字节码接口例子:
Access_Flag访问标志
访问标志信息包括该Class文件是类仍是接口,是否被定义成public,是不是abstract,若是是类,是否被声明称final。
字段表集合:
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量(静态变量)以及实例变量(非静态变量),可是不包括方法内部声明的局部变量。
一个field_info包含的信息:
方法表:
methods_count: u2
前三个字段和field_info同样
方法中每一个属性都是一个attribute_info结构
JVM预约义了部分attribute,可是编译器本身也能够实现本身的attribute写入class文件里,供运行使用
不一样的attribute经过attribute_name_index来区分
Code结构
Code attribute的做用是保存该方法的结构,如所对应的字节码
code attribute的做用是保存该方法的结构,如所对应的字节码
推荐你们使用: jclasslib 阅读字节码信息
Java中,每个方法都是能够访问this(表示对当前对象的引用),
字节码角度,若是方法自己是个非静态(实例)的,this能够做为方法的第一个方法,能够隐式的传递进来。会使得每一个实例方法均可以访问this。至少会有个局部变量,这个局部变量就是this。
对于某各种Test,中的静态方法 使用了synchronized 关键字,至关于给这个Test对应的Class对象加锁了。
关于this关键字:
Java编译器在编译时候,把对this方法的访问,转变成了对普通参数的访问。在Java中,每个非静态实例的方法的局部变量中,至少会存在一个指向当前对象的局部变量。即:
对于Java类中的每个实例方法(非static方法),其中在编译后所生成的字节码当中,方法参数的数量总会比源代码汇总方法的参数多一个(this),它位于方法的第一个参数位置处;这样咱们就能够在Java实例方法中使用this访问当前对象的属性以及其余方法。这个操做是在编译期间完成的,由javac编译器,在编译时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。因此,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。
关于异常处理:
Code结构:
attribute_length表示attribute锁包含的字节数,不包含attribute_name_index和attribute_length字段
max_stack表示这个方法运行的任什么时候刻所能达到的操做数栈的最大深度
max_locals表示方法执行期间所建立的局部变量的数目,包含用来表示传入的参数的局部变量
code_lenght表示该方法所含的字节码的字节数以及具体的指令码
具体字节码便是该方法被调用时,虚拟机所执行的字节码
exception_table, 这里存放的是处理异常的消息
每一个exception_tabel 表项由start_pc, end_pc , handler_pc ,catch_type 组成
start_pc 和 end_pc 表示在code 数组中的从start_pc都end_pc处(包含start_pc, 不包含end_pc)的指令抛出的异常会由这个表项来处理
handler_pc表示处理异常的代码的开始处。catch_type 表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理全部的异常。
Java字节码对于异常的处理方式:
1. 统一采用异常表的方式来对异常进行处理
2. 老版本中,并非使用遗产表的方式来对异常进行处理的,而是采用特定的指令方式(了解)
3. 当异常处理存在finally语句块时,现代化的JVM采起的方式将finally语句块的字节码拼接到每个catch块后面,换句话说,程序存在多少个catch块,就会在每个catch块后面重复多少个finally语句块的字节码。
栈帧,是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。
栈帧, 自己是一种数据结构,封装了风阀的局部变量表,动态连接信息,方法的返回地址操做数栈等信息。
Java中,对于不一样的类之间的关系,编译期间,地址关系其实是不知道的。何时知道?
1. 类加载时候
2. 真正调用时候,才知道目标方法地址。
基于以上两点,引伸出了符号引用和直接引用。
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫作静态解析;另一些符号引用则是在每次运行期转为直接引用,这种转换叫作动态连接,这体现为Java的多态性
好比父类因用户指向子类实现。
Aninaml a = new Cat(); a.run(); a = new Fish(); a.run
编译时候,a都是Animal. 字节码角度,都是Animal
运行时候,每次运行期,都会进行一次直接引用的转换。
JVM 方法调用的字节码指令:
1. invokeinterface:调用接口中的方法,其实是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法(一个接口,n个实现类)。
2. invokestatic: 调用静态方法
3.invokespecial: 调用本身的私有方法,构造方法(<init>) 以及父类的方法
4. invokevirtual: 调用虚方法,运行期动态查找的过程。
5. invokedynamic: 动态调用方法。
静态解析的四种状况:
1. 静态方法
2.父类方法
3. 构造方法
4. 私有方法(公有方法能够被重写或者复写,多态的可能。私有方法在加载时候就可以被肯定了)
以上四种称之为: 非虚方法。他们是在类加载阶段就能够将符号引用转换为直接引用的。
public class MyTest5 { public void test(GrandPa grandPa){ System.out.println("grandPa"); } public void test(Father father){ System.out.println("father"); } public void test(Son son){ System.out.println("son"); } public static void main(String[] args) { //都是GrandPal类型的 GrandPa father = new Father(); GrandPa son = new Son(); MyTest5 myTest5 = new MyTest5(); myTest5.test(father); myTest5.test(son); } } class GrandPa{ } class Father extends GrandPa{ } class Son extends Father{
以上代码 , father的静态类型是Grandpa,而father的实际类型(真正指向的类型)是Father
变量自己的静态类型是不会被改变的, GrandPa father
结论:
变量的静态类型是不会发生变化的,而变量的实际类型是能够发生变化的(多态的一种体现)。实际类型是在运行期方可肯定。
以上,方法的重载,参数类型不同。方法重载是一种纯粹的静态行为。
因此,当使用myTest5调用方法的时候, 是根据类型进行匹配。寻找类型是 GrandPa的。编译器就能够彻底肯定的。
public class MyTest6 { public static void main(String[] args) { Fruit apple = new Apple(); Fruit orange = new Orange(); apple.test(); orange.test(); apple = new Orange(); apple.test(); } } class Fruit{ public void test(){ System.out.println("fruit"); } } class Apple extends Fruit{ @Override public void test() { System.out.println("apple"); } } class Orange extends Fruit{ @Override public void test() { System.out.println("orange"); } }
引伸:
Java中,new起到了三个做用:
1. 在堆上开辟空间
2. 执行构造方法
3. 将构造方法执行后返回的堆上的此引用值返回
方法的动态分派:
方法的动态分派涉及到一个重要概念:方法接收者
invokevirtual字节码指令的多态查找流程
方法重载和方法重写,咱们能够获得这个方法重载是静态的,是编译器行为,方法重写是动态的,是运行期行为。
public class MyTest7 { public static void main(String[] args) { Animal animal = new Animal(); Dog dog = new Dog(); animal.test("hello"); dog.test(new Date( )); } } class Animal{ public void test(String str){ System.out.println("animal str"); } public void test(Date date){ System.out.println("animal date"); } } class Dog extends Animal{ @Override public void test(String str) { System.out.println("dog str"); } @Override public void test(Date date) { System.out.println("dog date"); } }
针对于方法调用动态分派的过程,虚拟机会在类的方法区创建一个虚方法表的数据结构(virtual method table,简称 vtable)
现代JVM在执行Java代码的时候,一般会将解释执行与编译执行两者结合起来执行。
所谓解释执行:经过解释器读取字节码,遇到相应的指令就去执行该指令
所谓编译执行:经过及时编译器(Just In Time, JIT)将字节码转为本地机器码来执行,现代JVM会根据代码热点来生成相应的本地机器码。
基于栈的指令集合基于寄存器的指令集之间的关系:
1. JVM执行指令时所采起的的方式是基于栈的指令集
2. 基于栈的指令集的主要操做: 入栈、出栈
3. 基于栈的指令集的优点在于他能够在不一样平台间一直,而基于寄存器的指令集是与硬件架构密切关联的,没法作到可移植。
4. 基于栈的指令集的缺点: 完成相同的操做,执行数量一般要比基于寄存器的指令集数量多 。基于栈的指令集是在内存中操做的,而基于寄存器的指令集是直接由CPU执行的,它是在高速缓冲区中进行的,速度要快不少。虽然虚拟机能够采用一些优化手段,但整体 来讲,基于栈的指令集的执行速度要慢一些。
注意:
栈 配合 局部变量表使用,局部变量表的0位置是this
对应动态代理,主要有一个类(proxy)和一个接口(InvocationHandler)去搞定。
接口:
public interface Subject { void request(); }
实现类:
public class RealSubject implements Subject { @Override public void request() { System.out.println("reslsubjct"); } }
代理类:
/** * 动态代理文件 */ public class DynamicSubject implements InvocationHandler { private Object sub; public DynamicSubject(Object obj){ this.sub = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before calling"+ method); method.invoke(this.sub, args); System.out.println("after calling"+ method); return null; } }
测试:
public class Client { public static void main(String[] args) { RealSubject realSubject = new RealSubject(); DynamicSubject dynamicSubject = new DynamicSubject(realSubject); Class<?> clazz = realSubject.getClass(); //获取 Class对象是为了,动态代理须要类加载器。 Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), dynamicSubject); subject.request(); System.out.println(subject.getClass()); } }
程序运行期动态生成的:
首先建立代理类,而后建立代理类的实例对象。
对象分为两部份内容:
1, 对象自己拥有的那些数据(位于堆)
2, 对象所属的类型(元数据信息,MetaData) 全部实例对应一个Class对象。位于方法区(存储的一部分对象的类型数据信息)
方案一:
对象引用的是一个指向对象实例的指针,另一个指针指向方法区中的类型数据
方案二:(HotSpot的方案)
对象引用的是对象自己,和一个指向方法区汇总的类型数据指针 (对象实例数据、方法区)
两种方案的差异L
堆发生垃圾回收频率很高,对于垃圾回收算法来讲,有几种会涉及到对象移动(压缩):为了保证区域连续的地方增大,移动之
方案一:对象一旦移动了,指针值会发生变化!随着每次垃圾回收会变化。
方案二:指针不会随之变化。
JVM内存划分:
虚拟机栈
程序计数器
本地方法栈:主要用于处理本地方法
堆: JVM管理的最大一块内存空间
线程共享的区域,主要存储元信息。从JDK1.8开始,完全废弃永久代。使用元空间(meta space)
运行时常量池(方法区的一部分): 方法区的一部份内容。编译后的字节码的符号引用等等。加载完后,放入到方法区的运行时常量池。
直接内存: Direct Memory。 与Java NIO密切相关,JVM经过堆上的DirectByteBuffer来直接操做内存。
现代几乎全部的垃圾收集器都是采用的分代收集算法,因此堆空间也基于这一点进行了相应的划分。
Java对象的建立:
new
反射
克隆
反序列化
new关键字建立对象的3个步骤:
1, 在堆内存中建立出对象的实例
2, 为对象成员变量赋初始值(指的是,实例变量,区别静态变量)
3, 将对象的引用返回。
虚拟机干的活儿: 检查指令的参数new指令建立一个对象,指令参数是否是能在常量池中定位成一个类的符号引用。查看这个类是否是已经加载、连接、初始化了。
指针碰撞: 前提是堆中的空间经过一个指针进行分割,一侧是已经被占用的空间,另外一侧是未被占用的空间。
空闲列表:(前提是堆内存空间中已被使用与未被使用的空间交织在一块儿的。这时,虚拟机就须要经过一个列表来记录那些空间是能够用的,哪些空间是已被使用的,接下来找出能够容纳下新建立对象的且未被使用的空间,在此空间存放该对象,同时还要修改列表的记录)
一个对象包含三部分布局:
1.对象的头,
2.实例数据(class中定义的成员变量)
3.对齐填充
永久代属于与堆链接的一个空间,对于永久代处理是比较麻烦的。
元空间,使用的操做系统的本地内存。能够不连续的。元空间里还有元空间虚拟机,管理元空间的内存的分配和回收状况。 初始大小21M,随着对于内存占用,会进行垃圾回收,甚至内存扩展,能够扩展到内存大小的最大值。
存放一个类的元数据信息,在框架中,用到运行期动态生成类的手段。动态建立出来的类,元信息放在元空间。
元空间参数: -XX:MaxMetaspaceSize=200M
在Java虚拟机(如下简称JVM)中,类包含其对应的元数据,好比类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已肯定的符号引用和虚方法表。
在过去(当自定义类加载器使用不广泛的时候,几乎不动态搭理),类几乎是“静态的”而且不多被卸载和回收,所以类也能够被当作“永久的”。另外因为类做为JVM实现的一部分,它们不禁程序来建立,由于它们也被认为是“非堆”的内存。
在JDK8以前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫作永久代的区域。永久代一段连续的内存空间,咱们在JVM启动以前能够经过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。可是有一个明显的问题,因为咱们能够经过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。
备注:在JDK7以前的HotSpot虚拟机中,归入字符串常量池的字符串被存储在永久代中,所以致使了一系列的性能问题和内存溢出错误。想要了解这些永久代移除这些字符串的信息,请访问这里查看。
随着Java8的到来,咱们再也见不到永久代了。可是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域就是咱们要提到的元空间。
这项改动是颇有必要的,由于对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。而且为永久代设置空间大小也是很难肯定的,由于这其中有不少影响因素,好比类的总数,常量池的大小和方法数量等。
同时,HotSpot虚拟机的每种类型的垃圾回收器都须要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不只实现了对元空间的无缝管理,还能够简化Full GC以及对之后的并发隔离类元数据等方面进行优化。
因为类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。所以,咱们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户能够为元空间设置一个可用空间最大值,若是不进行设置,JVM 会自动根据类的元数据大小动态增长元空间的容量。
注意:永久代的移除并不表明自定义的类加载器泄露问题就解决了。所以,你还必须监控你的内存消耗状况,由于一旦发生泄漏,会占用你的大量本地内存,而且还可能致使交换区交换更加糟糕。
元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据咱们须要不一样的垃圾回收器进行处理,如今只须要执行元空间虚拟机的 C++ 代码便可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,于是不会被回收掉。
咱们从行文到如今提到的元空间稍微有点不严谨。准确的来讲,每个类加载器的存储区域都称做一个元空间,全部的元空间合在一块儿就是咱们一直说的元空间。当一个类加载器被垃圾回收器标记为再也不存活,其对应的元空间会被回收。在元空间的回收过程当中没有重定位和压缩等操做。可是元空间内的元数据会进行扫描来肯定 Java 引用。
元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个全局的空闲组块列表。当一个类加载器须要组块时,它就会从这个全局的组块列表中获取并维持一个本身的组块列表。当一个类加载器再也不存活,那么其持有的组块将会被释放,并返回给全局组块列表。类加载器持有的组块又会被分红多个块,每个块存储一个单元的元信息。组块中的块是线性分配(指针碰撞分配形式)。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式链接,一旦某个虚拟内存映射区域清空,这部份内存就会返回给操做系统。
上图展现的是虚拟内存映射区域如何进行元组块的分配。类加载器 1 和 3 代表使用了反射或者为匿名类加载器,他们使用了特定大小组块。 而类加载器 2 和 4 根据其内部条目的数量使用小型或者中型的组块。
参考:https://www.infoq.cn/article/Java-PERMGEN-Removed
命令:jstat -gc 进程号 打印元空间信息
jmap -clstats PID 打印类加载器数据
jcmd PID GC.class_stats 诊断命令
jcmd 是从jdk1.7开始增长的命令
1. jcmd pid VM.flag:查看JVM启动参数
2. jcmd pid help: 列出当前运行的Java进行能够执行的操做
3. jcmd pid help JFR.dump: 查看具体命令的选项
4. jcmd pid PerfCounter.print: 查看JVM性能相关参数
5. jcmd pid VM.uptime:查看JVM的启动时长
6. jcmd pid GC.class_histogram 查看系统中类的统计信息
7. jcmd pid Thread.print: 查看线程堆栈信息
8. jcmd pid GC.heap_dump filename: 导出heap dump文件,导出的文件能够经过jvisualvm查看
9. jcmd pid VM.system_properties: 查看JVM的属性信息
10. jcmd pid VM.version: 查看目标JVM进程的版本信息
11. jcmd pid VM.command_line:查看JVM启动的命令行参数信息
jstack: 能够查看或是导出Java应用程序中栈线程的堆栈信息
jmc: java Mission Control
补充:
针对于犯法调用动态分派的过程,虚拟机会在类的方法区创建一个虚方法表的数据结构(virtual method table, vtable)
针对于invokeinterface指令来讲,迅疾会创建一个叫接口方法表的数据结构(interface method table, itable)
JVM运行时数据区:
程序计数器
本地方法栈
Java虚拟机栈(JVM Stack)
堆
方法区:
看下面例子:
public void method(){ Object obj = new Object(); }
生成了两部份内存区域:
1.obj这个引用变量,由于是方法内的变量,放到JVM Stack里面
2. 真正Object class的实例对象,放到Heap里面
上述的new语句一共消耗12个byte。JVM规定引用占4个byte(JVM Stack),而空对象是8个byte(在Heap)
方法结束后,对应Stack中的变量立刻回收,可是Heap中的对象要等GC来回收