Java虚拟机学习笔记
GC(Garbage Collection)
Java和JVM的历史
- 1996年 SUN JDK 1.0 Classic VM,纯解释运行,使用外挂进行JIT
- 1997年 JDK1.1 发布,AWT、内部类、JDBC、RMI、反射
- 1998年 JDK1.2 Solaris Exact VM
JIT 解释器混合 ,Accurate Memory Management 精确内存管理,数据类型敏感,提升的GC性能
JDK1.2开始 称为Java 2,J2SE J2EE J2ME 的出现,加入Swing Collections
- 2000年 JDK 1.3 Hotspot 作为默认虚拟机发布,加入JavaSound
- 2002年 JDK 1.4 Classic VM退出历史舞台,Assert 正则表达式 NIO IPV6 日志API 加密类库
- 2004年发布 JDK1.5 即 JDK5 、J2SE 5 、Java 5
泛型、注解、装箱、枚举、可变长的参数、Foreach循环
- JDK1.6 JDK6
脚本语言支持、JDBC 4.0、Java编译器 API
- 2011年 JDK7发布
延误项目推出到JDK8、G1、动态语言增强、64位系统中的压缩指针、NIO 2.0
- 2014年 JDK8发布
Lambda表达式、语法增强 Java类型注解,HashMap添加了红黑树,ConcurrentHashMap加锁方式改变
- 2016年JDK9
模块化
运行时数据区域
Java虚拟机会在执行Java程序过程中将其所管理的内存划分为若干不同的数据区域。
方法区、堆、虚拟机栈、本地方法栈、程序计数器。
其中方法区和堆为线程共享区域,其他为线程隔离区域。
程序计数器
是一块较小的内存空间,可以看作是当前线程执行的字节码的行号指示器,通过改变该计数器实现分支、循环、跳转、异常等基础功能的流程控制。
每条线程都有一个独立的程序计数器,以便于各自执行各自的任务。
若执行的是Java方法,计数器记录的则是虚拟机字节码指令地址;如果执行的是Native方法,计数器值则为空。
该区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
线程私有,生命周期与线程相同。
每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。(方法需要在帧中分配多大的局部变量空间是完全确定的,在运行期间不会改变局部变量表的大小)。
该区域两种异常,StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度时抛出。
OutOfMemoryError:如果虚拟机栈可以动态扩展,若无法申请到足够的内存时抛出。
某函数中创建对象,如StringBuffer str = new StringBuffer(“Hello World”);,那么局部变量str将被分配在栈内,StringBuffer实例被分配在Java堆上。
本地方法栈
和Java虚拟机栈类似,只不过Java虚拟机栈执行Java方法,本地方法栈执行Native方法。
该区域同样会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
一般来说,堆是Java虚拟机管理的最大的一块内存也是GC管理的主要区域,被所有线程所共享。此区域的唯一目的就是存放对象实例。由于现在收集器基本采用分代收集算法,所有Java堆还可以细分为新生代和老年代,在细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。
Java堆要求逻辑上连续而物理上可以不连续。
该区域会抛出OutOfMemoryError异常,在堆中没有内存完成实例分配并且堆也无法再扩展时。
方法区
线程共享的区域,用于存储已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。在HotSpot中,有人更喜欢把方法区成为“永久代”。JDK 1.7中的HotSpot中,已经把原本放在永久代的字符串常量池移出到堆中。
同样会抛出OutOfMemoryError异常。
Java7将字符串常量池位置调整到Java堆内,创建s4时,常量池中的”11”指向s3的对象,因为已经创建存在了。
String s = new String("1");
s.intern();//确保字符串在内存中只有一份拷贝
String s2="1";
System.out.print(s==s2);//false
String s3=new String("1")+new String("1");
s3.intern();
String s4 = "11";
System.out.print(s3==s4);//true,jdk6中是false
运行时常量池
方法区的一部分,Class文件中有一项常量池信息,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
该区域若无法再申请到内存时会抛出OutOfMemoryError异常。
直接内存
该区域并不是虚拟机运行时数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。
JDK 1.4中加入NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。若忽略直接内存,使得各个区域总和大于物理总和,从而导致动态扩展时抛出OutOfMemoryError异常。
垃圾收集器
判断对象是否存活
引用计数法
给对象添加引用计数器,每当有一个地方引用它时,计数器+1;当引用失效,计数器-1;当为0时,表示对象不可再被引用。
该方法实现简单,判定高效,但是主流的Java虚拟机里没有选用该方法来管理内存,主要是因为它很难解决对象之间相互循环引用的问题。
可达性分析算法
主流商用程序语言主流实现中都是称通过可达性分析来判定对象是否存活。
基本思想:通过一系列“GC Roots”的对象作为起始点,搜索走过的路径成为引用链,当一个对象到GC Roots没有任何引用链时,证明该对象不可用。
可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
引用的类别
JDK 1.2之前只有引用(现在的强引用)和未引用之说。
JDK 1.2之后,有强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种强度依次减弱。
- 强引用:代码中普遍存在的Object obj = new Object(),只要强引用还在,永远不会被回收
- 软引用:描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在Java中用java.lang.ref.SoftReference类来表示,适用于实现缓存等。
- 弱引用:只能生存到下一次垃圾收集发生之前。WeakReference类来实现。
- 虚引用:幽灵引用或幻影引用。唯一目的就是能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类来实现虚引用。
finalize()
真正宣告一个对象死亡,至少经历两次标记过程:
第一次,没有与GC Roots相连接的引用链的对象,是否有必要执行finalize()方法。如果没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,将不会执行finalize()方法。
如果有必要执行,这个对象将会放置在一个F-Queue的队列中,并在稍后由虚拟机自动建立的优先级低的Finalizer线程去执行。finalize()方法是对象逃脱死亡的最后一次机会。
第二次,对F-Queue中对象第二次小规模标记,如果这时还没“逃脱”,就真的回收了。
finalize()方法只会被系统调用一次,尽量避免使用该方法。
垃圾收集算法
标记-清除算法
先标记出需要回收的对象,标记完成后,统一回收。
效率不高,同时会产生不连续的内存碎片。
复制算法
将内存分为大小相等的两块,每次使用其中一块,当该块用完了,将存活的复制到另外一块,然后再把之前那一块全部清理掉。
简单,高效,但是内存使用率为原来的一半。
目前这种方法用来回收新生代,可以将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时将Eden和用过的Survivor上存活的对象复制到另一块Survivor上,清理掉Eden和之前的Survivor。(HotSpot默认Eden和一块Survivor比例为8:1)
当新的Survivor空间不够复制时,需要依赖老年代进行分配担保(直接进入老年代)。
标记整理算法
和标记-清除一样,先标记,然后将存活的对象向一端移动(整理),然后直接清理掉端边界以外的内存。
分代收集
分为新生代和老年代。新生代中“朝生夕死”只有少量存活,选用复制算法。老年代中对象存活率高,没有担保内存,必须使用标记-清理或标记-整理算法。
垃圾收集器
两个收集器之间存在连线,说明它们可以搭配使用。
mirror GC 和 major GC(也叫做Full GC),
mirror GC针对young generation,比较频繁,回收速度也快。当jvm无法为新的对象分配空间的时候就会发生minor gc,所以分配对象的频率越高,也就越容易发生minor gc。
major GC针对 old generation,速度为Minor GC的10倍以上。发生Full GC有两种情况,①当老年代无法分配内存的时候,会导致Full GC,②当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清除自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,这样就会有一个问题,当发生一次Minor GC以后,存活的对象剧增(假设小对象),此时老年代并没有满,但是此时平均值增加了,会造成发生Full GC
Serial收集器
单线程收集器,垃圾收集时必须停掉其他所有工作线程。
Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
Serial GC,串行方式,最小化使用内存和并行开销
ParNew收集器
ParNew其实就是Serial的多线程版本。
Server模式下虚拟机中首选的新生代收集器,还有一个原因是因为目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
新生代收集器,使用复制算法的收集器,同时也是多线程收集器。
目标是达到一个可控制的吞吐量。
Parallel GC,并行方式,最大化应用程序吞吐量
Serial Old收集器
Serial Old是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。
给Client模式下的虚拟机使用。如果在Server模式下,有两种用途,一种是在JDK1.5之前与Parallel Scavenge搭配使用,另一种是作为CMS收集器的后备预案。
Parallel Old收集器
Parallel Scavenge的老年代版本,使用多线程和标记-整理算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在B/S系统的服务端上,注重响应速度。采用标记清除算法。
过程:初始标记(STW)——并发标记——重新标记(STW)——并发清除
STW(Stop The World),过程中还是有停顿的地方,但是时间比较低
缺点:
1、CMS对CPU资源非常敏感,因为要占用CPU资源进行并发收集
2、CMS无法处理浮动垃圾,在并发清理阶段产生的垃圾(浮动垃圾)需要等到下次GC,如果出现内存不足,会调用后备预案——临时启动Serial Old收集器。
3、CMS基于标记清除,产生不连续内存碎片。
G1收集器
G1是目前最新的成果之一。G1是面向服务端应用的垃圾收集器。特点:并行与并发、分代收集、空间整合、可预测的停顿。
G1将Java堆划分为多个大小相等的独立区域(Region),虽然保留新生代和老年代的概念,但这两者已不再是物理隔离。G1跟踪各个Region垃圾堆积的价值大小,后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这也是G1(Garbage-First)名称的由来。采用标记整理和复制(Region之间使用复制)GC的区间划分基数一般采用兆级别。
过程:初始标记(STW)——并发标记——最终标记(STW)——筛选回收(STW)
内存分配策略
1、对象优先在Eden分配。当Eden空间不足时,发起一次Minor GC。
2、大对象直接进入老年代。要避免写“朝生夕死”的大对象。
3、长期存活的对象进入老年代。对象在Survivor中每经历一次Minor GC,年龄就增加1岁(默认15进入老年代)。
4、动态对象年龄判断。如果Survivor空间中相同年龄的所有对象大小总和大于Survivor空间一半,年龄大于等于该年龄的对象将直接进入老年代,而不用等到MaxTenuringThreshold中要求的年龄。
5、空间分配担保。Minor GC发生之前,需要检查老年代最大可用的连续空间是否大于新生代所有对象总空间,成立则Minor GC是安全的。否则,检查老年代剩余连续空间是否大于历次晋升的平均大小(将进行一次Minor GC)。如果小于或者设置不允许担保失败将进行一次Full GC。
System.gc()会显示直接触发Full GC。
除了System.gc外,触发Full GC执行的情况有四种:老年代空间不足,永久代空间满,CMS GC时出现Promotion Failed和Concurrent Mode Failure,统计的到Minor GC晋升到老年代的平均大小大于老年代剩余空间。
虚拟机类加载机制
Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略会稍微增加一些性能开销,但提供了高度的灵活性。
类加载的五个过程:加载、验证、准备、解析、初始化。
加载:加载完成三件事,通过类的全限定名来获取定义此类的二进制字节流;将此二进制流转为方法区的运行时数据结构;在内存中生成一个该类的Class对象作为方法区这个类的访问入口。加载有两种情况,①当遇到new关键字,或者static关键字的时候就会发生(他们对应着对应的指令)如果在常量池中找不到对应符号引用时,就会发生加载 。②动态加载,当用反射方法(如class.forName(“类名”)),如果发现没有初始化,则要进行初始化。(注:加载的时候发现父类没有被加载,则要先加载父类)
验证:这一阶段的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全(虽然编译器会严格的检查java代码并生成class文件,但是class文件不一定都是通过编译器编译,然后加载进来的,因为虚拟机获取class文件字节流的方式有可能是从网络上来的,者难免不会存在有人恶意修改而造成系统崩溃的问题,class文件其实也可以手写16进制,因此这是必要的)。包括文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:该阶段就是为对象分派内存空间,然后初始化类中的属性变量,但是该初始化只是按照系统的意愿进行初始化,也就是初始化时都为0或者为null。因此该阶段的初始化和我们常说初始化阶段的初始化时不一样的
解析:解析就是虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用其实就是class文件常量池中的各种引用,他们按照一定规律指向了对应的类名,或者字段,但是并没有在内存中分配空间,因此符号因此就理解为一个标示,而在直接引用直接指向内存中的地址。
初始化:简单讲就是执行对象的构造函数,给类的静态字段按照程序的意愿进行初始化,注意初始化的顺序。(此处的初始化由两个函数完成,一个是<clinit>,初始化所有的类变量(静态变量),该函数不会初始化父类变量,还有一个是实例初始化函数<init>,对类中实例对象进行初始化,此时要如果有需要,是要初始化父类的)
类加载器
参考https://blog.youkuaiyun.com/qq407388356/article/details/79128608。
双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
类加载器的工作过程:如果一个类加载器收到类类加载的请求,他首先不会自己去加载这个类,而是把类委派个父类加载器去完成,因此所有的请求最终都会传达到顶 层的启动类加载器中,只有父类反馈无法加载该类的请求(在自己的搜索范围类没有找到要加载的类)时候,子类才会试图去加载该类。
- 对象创建方法,对象的内存分配,对象的访问定位。
对象的创建包括三步骤:①当遇到new命令的时候,会在常量池中检查该对象的符号引用是否存在,不存在则进行类的加载,否则执行下一步②分配内存,将将要分配的内存都清零。③虚拟机进行必要的设置,如设置hashcode,gc的分代年龄等,此时会执行<init>命令在执行之前所有的字段都为0,执行<init>指令以后,按照程序的意愿进行初始化字段。
对象的内存分配:包括对象头,实例数据,对齐填充
①对象头:包括对象的hascode,gc分代年龄,锁状态标等。
②实例数据:也就是初始化以后的对象的字段的内容,包括父类中的字段等
③对齐填充:对象的地址是8字节,虚拟机要求对象的大小是对象的整数倍(1倍或者两倍)。因此就会有空白区。
对象的访问:hotspot中 是采用对象直接指向对象地址的方式(这样的方式访问比较快)(还有一种方式就是句柄,也就是建一张表维护各个指向各个地址的指针,然后给指针设置一个句柄 (别名),然后引用直接指向这个别名,就可以获得该对象,这种的优势就是,实例对象地址改变了,只要修改句柄池中的指针就可以了,而引用本身不会发生改变)。
方法调用
并非方法执行,而是确定调用哪一个方法。
解析:“编译期可知,运行期不可变”,即在程序真正运行前就有一个可确定的调用版本,主要包括静态方法、私有方法、实例构造器、父类方法。解析调用是一个静态的过程,不会延迟到运行期再去完成。
分派:分派调用可能是静态的也可能是动态的。静态分派(重载)。动态分派(重写),运行期根据实际类型确定方法执行版本。
早期(编译期)优化
前端编译器,将.java文件编程.class文件的过程。虚拟机设计团队将性能优化集中到后端即时编译器中。前端编译器更多的是优化编码过程,提高编码效率,充分利用Java“语法糖”。“语法糖”包括泛型、变长参数、自动拆装箱、foreach等。
javac本身就是用java写的。
晚期(运行期)优化
在运行时,虚拟机将热点代码(被多次调用的方法、被多次执行的循环体)编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。JIT并不是JVM必须的部分。
解释器和编译器各有优势,当程序需要迅速启动时,解释器可以首先发挥作用,随着程序的运行,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,以获得更高的效率。解释器可以节约内存,编译器可以提高效率。
配置JVM
IDEA配置方法
Run->Edit Configurations->VM options
查看配置信息:
public static String toMemoyInfo() {
Runtime runtime = Runtime.getRuntime();
int freeMemory = (int) (runtime.freeMemory() / 1024 / 1024);
int totalMemory = (int) (runtime.totalMemory() / 1024 / 1024);
return freeMemory + "M/" + totalMemory + "M";
}
配置参数
常见配置汇总
//堆设置
-Xms:最小堆大小
-Xmx:最大堆大小
-Xss:设置栈大小
-Xoss:设置本地方法栈大小(对Hotspot无效)
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
//收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
//垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
//并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集//线程数.
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比.公式为1/(1+n)
//并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式.适用于单CPU情况.
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数.并行收集线程数.