JVM(Java虚拟机),和其它虚拟机一样,都有自己独立的CPU、内存等。JVM是运行在内存中,JVM主要是它的内存模型与GC垃圾回收器。
JVM如何加载class文件
JVM架构图
JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。图中已经用颜色区分,绿色表示“通行”,橘黄色表示停一停(需等待)。
Class Loader:依据特定格式,加载class文件到内存
Execution Engine:对命令进行解析
Native Interface:融合不同开发语言的原生库为Java所用->Class.ForName()->native()
Runtime Data Area:JVM内存空间结构模型
什么是反射
1、Class rc = Class.forName(“类路径”);
2、Robot r = (Robot) rc.newInstance();
3、反射提供的方法:
getName();获取类名
getDeclaredMethod(方法名,类型class);获取声明的方法,不管public、private
setAccessible(true)
getDeclaredField(属性名)
4、方法名.invoke(实例,方法参数);
谈谈ClassLoader
种类:
Boot Strap Class Loader:加载核心库 java.*
Ext Class Loader:加载扩展库 javax.*
App Class Loader:加载程序所在目录(即环境变量CLASS_PATH)
自定义Class Loader:定制化类加载
自定义Class Loader
1、findClass(类名)
2、defineClass(byte[] byte,int off,int len)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package com.interview.javabasic.reflect; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class MyClassLoader extends ClassLoader { private String path; private String classLoaderName; public MyClassLoader(String path, String classLoaderName) { this.path = path; this.classLoaderName = classLoaderName; } //用于寻找类文件 @Override public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } //用于加载类文件 private byte[] loadClassData(String name) { name = path + name + ".class"; InputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(new File(name)); out = new ByteArrayOutputStream(); int i = 0; while ((i = in.read()) != -1) { out.write(i); } } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); in.close(); } catch (Exception e) { e.printStackTrace(); } } return out.toByteArray(); } } |
ClassLoader的双亲委派机制
打开“java.lang”包下的ClassLoader类。然后将代码翻到loadClass方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } // -----??----- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,检查是否已经被类加载器加载过 Class<?> c = findLoadedClass(name); if (c == null) { try { // 存在父加载器,递归的交由父加载器 if (parent != null) { c = parent.loadClass(name, false); } else { // 直到最上面的Bootstrap类加载器 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. c = findClass(name); } } return c; } |
其实这段代码已经很好的解释了双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?
为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
loadClass和forName的区别
类加载方式
(1)隐式加载
创建类对象(new)
使用类的静态域
创建子类对象
使用子类的静态域
在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
(2)显式加载
ClassLoader.loadClass(className),只加载和连接、不会进行初始化
Class.forName(String name, boolean initialize,ClassLoader loader); 使用loader进行加载和连接,根据参数initialize决定是否初始化。
类装载过程
1、加载;通过Class Loader 加载class文件字节码,生成class对象。
2、连接:
1)验证:确保被加载的类的正确性
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。
2)准备:为类的静态变量分配内存,并将其赋默认值
为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
3)解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。
假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。
3、初始化
为类的静态变量赋初值
赋初值两种方式:
定义静态变量时指定初始值。如 private static String x=”123″;
在静态代码块里为静态变量赋值。如 static{ x=”123″; }
Class.forName()得到的class是已经初始化完成的(完成了1、2、3)
ClassLoader.loaderClass() 得到的class是还没有连接的(只完成第1步,因此可用于Lazy Load)
了解 Java内存模型
程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一关系即“线程私有”
- 对Java方法计数,如果是Native方法,则计数器值为Underfined
- 不会发生内存泄漏
Java虚拟机栈(Stack)
- Java方法执行的内存模型
- 包含多个栈帧
局部变量表和操作栈
局部变量表:包含方法执行过程中的所有变量
操作栈:入栈、出栈、复制、交换、产生消费变量
递归为什么会引发java.lang.StackOverflowError异常
递归过深,栈帧数超过虚拟栈深度
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
本地方法栈
与虚拟机栈相似,主要作用于标注了native的方法
元空间(MetaSpace)与永久代(PermGen)的区别
元空间使用本地内存,永久代使用JVM的内存
MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出。
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难。
- 永久代会为GC带来不必要的复杂性。
- 方便HotSpot与其它JVM如Jrockit的集成。
Java堆(Heap)
线程共享与私有
线程共享:MetaSpace、堆(Heap)
线程私有:程序计数器、虚拟机栈、本地方法栈
JVM 三大性能调优参数
java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar
-Xss:规定了每个线程虚拟机栈(堆栈)的大小
-Xms:堆的初始值
-Xmx:堆能达到的最大值
Java内存模型中堆和栈的区别
内存分配策略:
- 静态存储:编译时确定每个数据目标在运行时存储空间需求。
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定。
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配。
联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址。
管理方式:栈自动释放,堆需要GC。
空间大小:栈比堆小。
碎片相关:栈产生的碎片远小于堆。
分配方式:栈支持静态和动态分配,而堆仅支持动态分配。
效率:栈的效率比堆高。
元空间、堆、线程独占部分间的联系
不同JDK版本之间的intern()方法的区别–JDK6 VS JDK6+
s.intern();
JDK6:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串的引用。
JDK6+:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
注:在JDK1.6的时候,字符串常量池是存放在Perm Space(永久代)中的(永久代存在于方法区中,所以Perm Space和堆是相隔而开的),在1.6+的时候,字符串常量池移到了堆内存中
下面分别测试intern方法在jdk6和jdk6+的区别:
jdk6及以前的版本:返回false false
解答:
String s = new String(“a”)的时候, “a”对象会被首先创建,放入字符串常量池中(图中圈1),然后new出的对象放在堆中(图中圈2),在s调用intern()的时候(s是圈2对象的引用),由于发现常量池中本来就有一个和a一样值的字符串,所以s.intern()直接返回圈1对象的引用(这里我们在代码中并没有接收这个返回值),在String s2 = “a”的时候,会先在常量池中寻找有没有对应的字符串,如果有,就直接返回它的引用。所以,由于s2返回的是圈1对象的引用,而s是圈2对象的引用,故两者不相等。
在String s3 = new String(“a”) + new String(“a”),字符串常量池是不会创建“aa”这个字符串的,因为“”中只有单个a,但是会在堆内存中new出一个“aa”对象(圈3),所以在s3调用intern()的时候,首先,由于常量池中没有aa这个对象,所以就会尝试将“aa”也就是堆中的那个字符串对象放入常量池中,并返回字符串常量池中“aa”的引用(这里我们在代码中并没有接收这个返回值),但是由于常量池中放的相当于是一个对象副本,其本质也是一个对象,所以,比较s3==s4时,s3是堆内存中的圈3对象的引用,而s4是方发区中圈4对象的引用,故二者肯定不相等。
JDK7及更高的版本:返回false true
解答:
String s = new String(“a”)的时候, “a”会被首先创建,放入字符串常量池中(圈1),然后new出的对象放在堆中(圈2),在s调用intern()的时候,会尝试将字符串对象放入字符串常量池中,但是发现字符串常量池中已经有了,就不能放了,故s.intern()直接返回圈1对象的引用(这里我们在代码中并没有接收这个返回值),在String s2 = “a”的时候,会先在常量池中寻找有没有对应的字符串,如果有,就直接返回它的引用,所以,由于s2返回的是圈1对象的引用,而s是圈2对象的引用,故两者不相等。
在String s3 = new String(“a”) + new String(“a”),字符串常量池是不会创建“aa”这个字符串的,因为“”中只有单个a,但是会在堆内存中new出一个“aa”对象(圈3),所以在s3调用intern()的时候,首先,由于常量池中没有aa这个对象,所以接下来,按照jdk7中intern定义来,会继续去堆内存中查看是否有“aa”对象,而此时s3就是“aa”对象,故把s3的引用放到常量池中(圈4),然后返回常量池中的引用(这里我们在代码中并没有接收这个返回值),在执行String s4 = “aa”;由于常量池中已经有了“aa”的引用,故直接返回这个引用,所以,由于s4和s3都是堆内存中圈3对象的引用,故二者相等,结果为true。
再来看第二段代码:
1 2 3 4 5 6 7 8 9 10 11 |
public static void main(String[] args) { String s = new String("1"); String s2 = "1"; s.intern(); System.out.println(s == s2); String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4); } |
jdk6 下false false
jdk7 下false false
第一段代码和第二段代码的改变就是 s3.intern(); 的顺序是放在String s4 = “11”;后了。这样先执行String s4 = “11”;声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();时,常量池中“11”对象已经存在了,不需要新建任何对象或引用; 因此 s3 和 s4 的引用是不同的。(一个指向常量池,一个指向Java堆)。
第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String(“1”);的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:
将String常量池从Perm区(永久区)移动到了Java Heap(堆内存)区
String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
补充:字符串常量池
字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就会把对象的实例放到常量池中,然后返回这个对象的引用。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。
其中:
常量池在 jdk1.6(含)之前也是方法区(永久代)的一部分,并且其中只能够存放字符串的实例;
常量池在 jdk1.7(含)之后是在堆内存之中,可以存储的是字符串对象的实例和堆内存中字符串对象的引用;
jdk1.8 已移除永久代,字符串常量池还是在堆内存当中。