JVM特性
- 跨平台
- 垃圾自动回收
类加载机制
JVM的类加载是通过ClassLoader及其子类来完成的
- 自子类向父类检查是否已经参加载
- 自父类到子类依次尝试加载
类加载器
1、Bootstrap ClassLoader 根加载器
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2、Extension ClassLoader 扩展加载器
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包
3、App ClassLoader 应用加载器
负责记载classpath中指定的jar包及目录中class
4、Custom ClassLoader 自定义加载器
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
双亲委派模型
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
为什么需要自定义类加载器
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
JVM内存模型
JVM栈由堆、栈、本地方法栈、方法区等部分组成
堆
所有通过new创建的对象的内存都在堆中分配,堆的大小可以通过-Xmx和-Xms来控制。
堆分成新生代和老年代。新生代又分为 eden Survivor组成,Survivor由 from space 和to space组成
- 新生代。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例
- 老年代。用于存放新生代中经过多次垃圾回收仍然存活的对象
- 持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等。可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区
常用jvm配置
- -Xmx:最大堆内存,如:-Xmx512m
- -Xms:初始时堆内存,如:-Xms256m
- -XX:MaxNewSize:最大年轻区内存
- -XX:NewSize:初始时年轻区内存.通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
- -XX:MaxPermSize:最大持久带内存
- -XX:PermSize:初始时持久带内存
- -XX:+PrintGCDetails。打印 GC 信息
- -XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
- -XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
。
GC日志分析
JVM的GC日志的主要参数包括如下几个:
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
-XX:+HeapDumpOnOutOfMemoryError dump出内存溢出快照
-XX:HeapDumpPath=D:/heapdump.hprof dump出内存快照的位置
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:D:/gc.log -Xms10m -Xmx10m
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/heapdump.hprof
栈
每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
-xss:设置每个线程的堆栈大小. JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
本地方法栈
用于支持native方法的执行,存储了每个native方法调用的状态
方法区
存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值
垃圾回收器策略
垃圾回收期算法
引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
垃圾回收器
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
Serial/Serial Old
Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
ParNew
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。相对于Parallel Scavenge收集器用户相应速度较快。
Parallel Scavenge
Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
Parallel Old(年老代)
年老代收集器,只能和Parallel Scavenge组合使用(Parallel Scavenge收集器的年老代版本)
采用 ”标记-整理“算法,会对垃圾回收导致的内存碎片进行整理
关注吞吐量的系统可以将Parallel Scavenge+Parallel Old组合使用
CMS(老年代)
CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。(只适合用到老年代)
CMS垃圾护手器分为以下几个阶段
- 初始标记 (串行,会发生stop the word)
- 并发标记 (为了缩短stop the world 的时间)
- 并发预清理
- 重新标记 (串行,会发生stop the word,这个阶段是多线程的)
- 并发清理
- 重置
- 初始标记:初始标记需要stop the world该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。(PS:间接关联在下一个阶段)
- 并发标记:该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。
- 并发预清理:并发预处理阶段做的工作还是标记,与重标记功能相似。既然相似为什么要有这一步?CMS是以获取最短停顿时间为目的的GC。重标记需要STW(Stop The World),因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
- 重标记(STW) 暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。
有了前面的基础,这个阶段的工作量被大大减轻,停顿时间因此也会减少。
注意这个阶段是多线程的。
- 并发清理。用户线程被重新激活,同时清理那些无效的对象。
- 重置。 CMS清除内部状态,为下次回收做准备。