JVM知识点总结(一)
by Kay 2017.8.26 总结
参考资料:《深入理解Java虚拟机:JVM高级特性与最佳实践》周志明 http://www.ityouknow.com/java/2017/03/01/jvm-overview.html 《Java虚拟机精讲》高翔龙
类加载
什么叫类加载?
虚拟机把.class文件(.class文件里面放的是描述类的数据)加载到内存中,对数据进行检查、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
类的生命周期
类生命周期是怎么样的?每一步做了什么工作?
类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用、卸载这几个阶段。 需要注意的是,在类加载的过程中(加载、连接(验证、准备、解析)、初始化),每个步骤的开始顺序是确定的,但是并不是一定要等待上一过程的完成才会开始下一步骤。其中解析这一步并不是必须的,而且也可以在初始化之后执行,下面会提到。
加载(Loading)
注意此“加载”与“类加载”的意义是不同的,不要混淆了概念。 在加载阶段,完成的工作如下:
通过类的全限定名来找到类对应的二进制字节流
将字节流转换成描述类的数据结构放到方法区
在堆中生成一个java.lang.Class,作为方法区描述类数据结构的访问入口 总的来说就是在堆区生成了一个Class象,可以通过这个Class对象来访问相关类的数据。
连接(Linking)
- 验证 Verification 确保被加载类的正确性(文件格式、元数据、字节码、符号引用验证)
- 准备 Preparation 该阶段主要为类变量分配内存,也就是static修饰的类的静态成员,这些内存分配在方法区。 类变量分配内存的时候每个类变量会初始化,注意这里的初始化是指设置默认值,也就是每个类型的零值。比如 public static int value = 5
在准备阶段,value被赋予默认值,也就是 value = 0 把value的值赋值为 5 的操作是发生在初始化阶段,并不是准备阶段。 另外一个要注意的地方是,如果类变量中存在常量,也就是被**final **修饰,那么在准备阶段就会被赋值为常量的值,而不再是默认零值。 - 解析 Resolution 解析阶段将常量池中的符号引用替换为直接引用。 符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄符号引用就是一组符号来描述目标,可以是任何字面量。 如果只是符号引用,并不能确定被引用的目标已经加载到了内存中。如果是直接引用,则表示被引用的目标一定在内存中存在了
初始化(Initialization)
类初始化是类加载过程的最后一步,在这个过程中主要就是为类的静态成员赋予正确的初始化,就也是在代码层面指定的值,一方面是声明语句指定值,还有就是静态代码块执行的赋值操作。
那么问题来了,什么时候会执行类的初始化动作呢?
虚拟机规范严格规定了有且只有四种情况会触发对类的初始化:
- 使用new关键字创建对象、使用类的静态变量或静态方法
- 使用反射对类进行调用
- 初始化一个类的时候发现其父类还没有被初始化,则先初始化父类
- 虚拟机指定的启动类,比如main方法包含的那个类
需要注意的地方:
- 对于静态字段,调用时只有定义这个字段的类才会被初始化,比如子类调用父类的静态字段,只会触发父类被初始化,而子类不会
- 数组对象的创建,比如 MyClass[] arr=new MyClass[10];
并不会触发MyClass类的初始化(因为只会初始化这个数组类型) - 一个类调用了另一个类的静态常量。比如A类中有一个public static final int VALUE =5;
B类调用了A.VALUE ,这个时候不会触发A类的初始化,原因是这个静态的常量在编译的时候会将VALUE放在类B的常量池里面,对常量A.VAlUE的引用实际上都转换成了类B对自身常量池的引用,也就是说这个时候类B与类A其实已经没有什么关系了。
接口的加载比较特殊,接口在初始化的时候并不要求其父接口都初始化了,只有真正用到父接口的时候才会初始化父接口。别忘了接口中不能使用static块
类加载器
类加载器有哪些?什么关系?分别加载了什么?类加载的方式又哪几种?什么是双亲委派机制(为什么要用它,有什么好处)?怎么自定义加载类,什么方法?
启动类加载器:Bootstrap ClassLoader,负责加载存放在JAVA_HOME\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
除了以上三种类加载器之外,我们还可以通过继承java.lang.ClassLoader这个抽象类并重写findClass()方法来实现自定义的类加载器,比如要对字节码文件进行加密解密的时候.. ClassLoader中有几个重要的方法要注意一下:findClass()和defineClass()通常配合使用,resolveClass(),可以通过查看jdk文档查看具体这些方法的用法。
双亲委派模型
所谓双亲委派模型,是指如果一个类加载器比如AppClassLoader要加载一个类,它首先不会自己加载这个类,而是委托给父类去加载这个类,这里就是ExtClassLoader,而ExtClassLoader也不会尝试去加载这个类,它也是委托给父类去加载,每一层都是如此,一直到顶层的类加载器,当父类加载器反馈自己无法加载这个类的时候,子加载器就会尝试自己去加载这个类 使用双亲委派模型加载一个类的好处就是可以保证这个类在全局加载的唯一性。 可以通过查看ClassLoader类的源码的loadClass()方法,可以看到双亲委派机制的语法实现。
JVM内存结构
JVM内存结构是怎么样的
每个区域有什么作用
JVM内存结构
根据受访问权限的不同JVM内存区分为线程共享内存区和线程私有内存区,线程共享内存区就包括:Java堆区、方法区、运行时常量池;线程私有内存区包括:Java栈、本地方法栈、PC寄存器
堆 Heap
Java堆区是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。堆区是GC的重点区域
方法区 Method Area
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。在Java虚拟机规范里面把方法区描述为堆的一个逻辑部分,在HotSpot虚拟机上也有很多人将之称为“永久代”,或者说用永久代来实现方法区。这个区域在GC时主要针对常量池的回收和对类型的卸载
运行时常量池 Runtime Constant Pool
运行时常量池是方法区的一部分,Class文件中有一项信息是常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用,在类加载后这些信息就放在了方法区的常量池中了。 是否还记得String类的intern()方法,可以在运行期间将新的常量放到常量池中
JVM栈 JVM Stacks
JVM栈为线程私有,生命周期与线程相同。JVM栈里面存放的是每个方法执行时都会产生一个栈帧,表示这个方法的局部变量、操作栈、方法出口等信息,一个方法被调用和执行完就是一个栈帧在JVM栈中入栈和出栈的过程。
本地方法栈 Native Method Stacks
本地方法栈,顾名思义就是调用本地方法对应的栈空间,只不过本地方法是Native方法,我们看不到。
PC寄存器 Program Counter Register
程序计数器可以看作是记录当前线程所执行字节码的行号指示器
GC算法以及垃圾收集器
怎么判断对象是否存活
主要的垃圾收集算法有哪些,各有什么优势
有哪些垃圾收集器,适应场景
对象存活判断
判断对象是否存活一般有两种方式:
引用计数:
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但是无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
垃圾收集算法
标记 -清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 特点:标记和清除过程的效率都不高;标记清除之后会产生大量的内存碎片,当以后要分配大内存对象的时候可能会因为无法找到大量连续的内存而提前触发又一次垃圾收集动作复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 特点:需要额外的内存来存放复制的对象,不过当对象比较少时效率比较高。(可以想想为什么新生代会是Eden,S0,S1这样的划分?)标记-压缩算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 特点:需要移动对象,但是不会产生像“标记-清除”那样的内存碎片分代收集算法
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 特点:根据分代不同采用不同的回收算法,比如新生代采用复制算法(上面那个问题想通了吗), 老年代采用“标记-清除”或“标记-压缩”算法。
垃圾收集器
Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
后续再总结一些深入一点的JVM知识点。 –Kay