首先咱们须要了解什么是虚拟机,为何虚拟机能够实现夸平台,虚拟机在计算机中扮演一个什么样的角色。java
(从下向上看)web
看上图的操做系统与虚拟机层,能够看到,JVM是在操做系统之上的。他帮咱们解决了操做系统差别性操做问题,因此能够帮咱们实现夸操做系统。算法
接着向上看,来到虚拟机可解析执行文件这里,虚拟机就是根据这个.class的规范来实现夸平台的。数组
在向上到语言层,不一样的语言能够有本身的语法、实现方式,但最终都要编译为一个知足.class规范的文件,来让虚拟机执行。安全
因此理论上,任何语言想使用JVM虚拟机实现夸平台的操做,均可以根据规范生成.class文件,就可使用JVM,并实现“一次编译,屡次运行”。网络
第一点已经在上边说过,不在重复。数据结构
第二点内存管理也是咱们接下来主要讲的内容。在没有JVM的时代,在C/C++时期,写代码中除了写正常的业务代码以外,有很大一部分代码是内存分配与销毁相关的代码。稍有不慎就会形成内存泄露。而使用虚拟机以后关于内存的分配、销毁操做就都由虚拟机来管理了。多线程
相对的确定会形成虚拟机占用更多内存,在性能上与C/C++对比会较差,但随着虚拟机的慢慢成熟性能差距正在缩小。架构
Jvm虚拟机主要分为五大模块:类装载子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。并发
类的加载过程包含如下7步:
加载 -->校验-->准备-->解析-->初始化-->使用-->卸载
其中链接校验、准备-解析能够统称为链接。
1. 经过Class的全限定名获取Class的二进制字节流 2. 将Class的二进制内容加载到虚拟机的方法区 3. 在内存中生成一个java.lang.Class对象表示这个Class
获取Class的二进制字节流这个步骤有多种方式:
1. 从zip中读取,如:从jar、war、ear等格式的文件中读取Class文件内容 2. 从网络中获取,如:Applet 3. 动态生成,如:动态代理、ASM框架等都是基于此方式 4. 由其余文件生成,典型的是从jsp文件生成相应的Class
有两种类型的类加载器
虚拟机自带的类加载器
该类加载器没有父加载器,他负责加载虚拟机的核心类库。 如:java.lang.*等。 根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。 根类加载器的实现依赖于底层操做系统,属于虚拟机的实现的一部分,他并无继承java.lang.ClassLoader类。 如:java.lang.Object就是由根类加载器加载的。
它的父类加载器为根类加载器。 他从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库 若是把用户建立的JAR文件放在这个目录下,也会自动有扩展类加载器加载。 扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。
也称为应用加载器,他的父类加载器为扩展类加载器。 他从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。 他是用户自定义的类加载器的默认父加载器。 系统类加载器是纯java类,是java.lang.ClassLoader子类。
用户自定义的类加载器
注意: 《类加载器的子父关系》非《子父类继承关系》,而是一种数据结构,能够比作一个链表形式或树型结构。
代码:
public class SystemClassLoader { public static void main(String[] args) { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); System.out.println(classLoader); while (classLoader != null){ classLoader = classLoader.getParent(); System.out.println(classLoader); } } } 输出: sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@7a7b0070 null
得到类加载器的方法
方式 | 说明 |
---|---|
clazz.getClassLoader(); | 得到当前类的ClassLoader,clazz为类的类对象,而不是普通对象 |
Thread.currentThread().getContextClassLoader(); | 得到当先线程上下文的ClassLoader |
ClassLoader.getSystemClassLoader(); | 得到系统的ClassLoader |
DriverManager.getCallerClssLoader(); | 得到调用者的ClassLoader |
/** * 获取字符串的类加载器 * 返回为null表示使用的BootStrap ClassLoader */ public static void getStringClassLoader(){ Class clazz; try { clazz = Class.forName("java.lang.String"); System.out.println("java.lang.String: " + clazz.getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } 输出: java.lang.List: null 表示使用BootStrap ClassLoader加载
除了根加载器,每一个加载器被委托加载任务时,都是第一时间选择让其父加载器来执行加载操做,最终老是让根类加载器来尝试加载,若是加载失败,则再依次返回加载,只要这个过程有一个加载器加载成功,那么就会执行完成(这是Oracle公司Hotpot虚拟机默认执行的类加载机制,而且大部分虚拟机都是如此执行的),整个过程以下图所示:
自定义类加载器:
public class FreeClassLoader extends ClassLoader { private File classPathFile; public FreeClassLoader(){ String classPath = FreeClassLoader.class.getResource("").getPath(); this.classPathFile = new File(classPath); } @Override protected Class<?> findClass(String name){ if(classPathFile == null) { return null; } File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class"); if(!classFile.exists()){ return null; } String className = FreeClassLoader.class.getPackage().getName() + "." + name; Class<?> clazz = null; try(FileInputStream in = new FileInputStream(classFile); ByteArrayOutputStream out = new ByteArrayOutputStream()){ byte [] buff = new byte[1024]; int len; while ((len = in.read(buff)) != -1){ out.write(buff,0,len); } clazz = defineClass(className,out.toByteArray(),0,out.size()); }catch (Exception e){ e.printStackTrace(); } return clazz; } /** * 测试加载 * @param args */ public static void main(String[] args) { FreeClassLoader classLoader = new FreeClassLoader(); Class<?> clazz = classLoader.findClass("SystemClassLoader"); try { Constructor constructor = clazz.getConstructor(); Object obj = constructor.newInstance(); System.out.println("当前:" + obj.getClass().getClassLoader()); ClassLoader classLoader1 = obj.getClass().getClassLoader(); while (classLoader1 != null){ classLoader1 = classLoader1.getParent(); System.out.println("父:" + classLoader1); } SystemClassLoader.getClassLoader("com.freecloud.javabasics.classload.SystemClassLoader"); } catch (Exception e) { e.printStackTrace(); } } } 输出: 当前:com.freecloud.javabasics.classload.FreeClassLoader@e6ea0c6 父:sun.misc.Launcher$AppClassLoader@18b4aac2 父:sun.misc.Launcher$ExtClassLoader@1c6b6478 父:null com.freecloud.javabasics.classload.SystemClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
验证一个Class的二进制内容是否合法
1. 文件格式验证,确保文件格式符合Class文件格式的规范。 如:验证魔数、版本号等。 2. 元数据验证,确保Class的语义描述符合Java的Class规范。 如:该Class是否有父类、是否错误继承了final类、是否一个合法的抽象类等。 3. 字节码验证,经过分析数据流和控制流,确保程序语义符合逻辑。 如:验证类型转换是合法的。 4. 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。 如:验证引用的类、成员变量、方法的是否能够被访问(IllegalAccessError),当前类是否存在相应的方法、成员等(NoSuchMethodError、NoSuchFieldError)。
使用记事本或文本工具打开任意.class文件就会看到以下字节码内容:
左边方框内容表示魔数: cafe babe(做用是肯定这个文件是否为一个能被虚拟机接收的Class文件) 右边方框表示版本号 :0000 0034 (16进制转为10进制为52表示JDK1.8)
在准备阶段,虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值。
注意这里仅仅会为static变量分配内存(static变量在方法区中),而且初始化static变量的值为其所属类型的默认值。 如:int类型初始化为0,引用类型初始化为null。 即便声明了这样一个static变量: public static int a = 123; 在准备阶段后,a在内存中的值仍然是0, 赋值123这个操做会在中初始化阶段执行,所以在初始化阶段产生了对应的Class对象以后a的值才是123 。
public class Test{ private static int a =1; public static long b; public static String str; static{ b = 2; str = "hello world" } } 为int类型的静态变量 a 分配4个字节(32位)的内存空间,并赋值为默认值0; 为long类的静态变量 b 分配8个字节(64位)的内存空间,并默认赋值为0; 为String类型的静态变量 str 默认赋值为null。
解析阶段,虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类、接口、方法、成员变量等符号引用。在转换成直接引用后,会触发校验阶段的符号引用验证,验证转换以后的直接引用是否能找到对应的类、方法、成员变量等。这里也可见类加载的各个阶段在实际过程当中,多是交错执行。
public class DynamicLink { static class Super{ public void test(){ System.out.println("super"); } } static class Sub1 extends Super{ @Override public void test(){ System.out.println("Sub1"); } } static class Sub2 extends Super { @Override public void test() { System.out.println("Sub2"); } } public static void main(String[] args) { Super super1 = new Sub1(); Super super2 = new Sub2(); super1.test(); super2.test(); } }
在解析阶段,虚拟机会把类的二进制数据中的符号引用替换为直接引用。
初始化阶段即开始在内存中构造一个Class对象来表示该类,即执行类构造器<clinit>()的过程。须要注意下,<clinit>()不等同于建立类实例的构造方法<init>()
1. <clinit>()方法中执行的是对static变量进行赋值的操做,以及static语句块中的操做。 2. 虚拟机会确保先执行父类的<clinit>()方法。 3. 若是一个类中没有static的语句块,也没有对static变量的赋值操做,那么虚拟机不会为这个类生成<clinit>()方法。 4. 虚拟机会保证<clinit>()方法的执行过程是线程安全的。
Java程序对类的使用方式能够分为两种
主动使用类的七中方式,即类的初始化时机:
1. 建立类的实例; 2. 访问某个类或接口的静态变量(无重写的变量继承,变量其属于父类,而不属于子类),或者对该静态变量赋值(静态的read/write操做); 3. 调用类的静态方法; 4. 反射(如:Class.forName("com.test.Test")); 5. 初始化一个类的子类(Chlidren 继承了Parent类,若是仅仅初始化一个Children类,那么Parent类也是被主动使用了); 6. Java虚拟机启动时被标明为启动类的类(换句话说就是包含main方法的那个类,并且自己main方法就是static的); 7. JDK1.7开始提供的动态语言的支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_public,REF_invokeStatic句柄对应的类没有初始化,则初始化;
除了上述所讲七种状况,其余使用Java类的方式都被看做是对类的被动使用,都不会致使类的初始化,好比:调用ClassLoader类的loadClass()方法加载一个类,并非对类的主动使用,不会致使类的初始化。
注意: 初始化单单是上述类加载、链接、初始化过程当中的第三步,被动使用并不会规定前面两个步骤被使用与否 也就是说即便被动使用只是不会引发类的初始化,可是彻底能够进行类的加载以及链接。 例如:调用ClassLoader类的loadClass方法加载一个类,这并非对类的主动使用,不会致使类的初始化。 须要铭记于心的一点: 只有当程序访问的静态变量或静态变量确实在当前类或当前接口中定义时,才能够认为是对类或接口的主动使用,经过子类调用继承过来的静态变量算做父类的主动使用。
JVM中的Class只有知足如下三个条件,才能被被卸载(unload)
1. 该类全部的实例都已经被GC,也就是JVM中不存在该Class的任何实例。 2. 加载该类的ClassLoader已经被GC。 3. 该类的java.lang.Class 对象没有在任何地方被引用。 如:不能在任何地方经过反射访问该类的方法。
运行时数据区主要分两大块: 线程共享:方法区(常量池、类信息、静态常量等)、堆(存储实例对象) 线程独占:程序计数器、虚拟机栈、本地方法栈
程序计数器是一块较小的内存空间,它的做用能够看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。
特色: 1. 若是线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址 2. 若是正在执行的是Native 方法,则这个技术器值为空(Undefined) 3. 此内存区域是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域
public class ProgramCounterJavap { public static void main(String[] args) { int a = 1; int b = 10; int c = 100; System.out.println( a + b * c); } }
使用javap反汇编工具可看到以下图:
图中红框位置就是字节码指令的偏移地址,当执行到main(java.lang.String[])时在当前线程中会建立相应的程序计数器,在计数器中存放执行地址(红框中内容)。
这也说明程序在运行过程当中计数器改变的只是值,而不是随着程序的运行须要更大的空间,也就不会发生溢出状况。
一个方法表示一个栈,遵循先进后出的方式。每一个栈中又分局部变量表、操做数栈、动态链表、返回地址等等。
虚拟机栈是线程隔离的,即每一个线程都有本身独立的虚拟机栈。
局部变量:存储方法参数和方法内部定义的局部变量名 操做数栈:栈针指令集(表达式栈) 动态连接:保存指向运行时常量池中该指针所属方法的引用 。做用是运行期将符号引用转化为直接引用 返回地址:保留退出方法时,上层方法执行状态信息
虚拟机栈的StackOverflowError
单个线程请求的栈深度大于虚拟机容许的深度,则会抛出StackOverflowError(栈溢出错误)
JVM会为每一个线程的虚拟机栈分配必定的内存大小(-Xss参数),所以虚拟机栈可以容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会致使当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用,代码见下:
/** * 虚拟机栈的StackOverflowError * JVM参数:-Xss160k * @Author: maomao * @Date: 2019-11-12 09:48 */ public class JVMStackSOF { private int count = 0; /** * 经过递归调用形成StackOverFlowError */ public void stackLeak() { count++; stackLeak(); } public static void main(String[] args) { JVMStackSOF oom = new JVMStackSOF(); try { oom.stackLeak(); }catch (Throwable e){ System.out.println("stack count : " + oom.count); e.printStackTrace(); } } }
设置单个线程的虚拟机栈内存大小为160K,执行main方法后,抛出了StackOverflow异常
stack count : 771 java.lang.StackOverflowError at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:18) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)
虚拟机栈的OutOfMemoryError
不一样于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,而且没法再申请到新的内存时抛出的异常。
JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大体上等于“JVM进程能占用的最大内存(依赖于具体操做系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(能够忽略不计) - JVM进程自己消耗内存”。当虚拟机栈可以使用的最大内存被耗尽后,便会抛出OutOfMemoryError,能够经过不断开启新的线程来模拟这种异常,代码以下:
/** * java栈溢出OutOfMemoryError * JVM参数:-Xms20M -Xmx20M -Xmn10M -Xss2m -verbose:gc -XX:+PrintGCDetails * @Author: maomao * @Date: 2019-11-12 10:10 */ public class JVMStackOOM { private void dontStop() { try { Thread.sleep(24 * 60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 经过不断的建立新的线程使Stack内存耗尽 */ public void stackLeakByThread(){ while (true){ Thread thread = new Thread(() -> dontStop()); thread.start(); } } public static void main(String[] args) { JVMStackOOM oom = new JVMStackOOM(); oom.stackLeakByThread(); } }
方法区,主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
常亮池中的值是在类加载阶段时,经过静态方法块加载到内存中
对于绝大多数应用来讲,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。能够位于物理上不连续的空间,可是逻辑上要连续。也是咱们在开发过程当中主要使用的地方。
在jdk 1.8以前的版本heap分新生代、老年带、永久代,但在1.8以后永久代修改成元空间,本质与永久代相似,都是对JVM规范中方法区的实现。元空间不在虚拟机中,而是在本地内存中。
咱们使用下面一个生活中的例子来讲明:
首先咱们把整个内存处理过程比做一个仓库管理,用户会有不一样的东西要在咱们仓库作存取。
仓库中的货物比做咱们内存中的实例,用户会不肯定时间来咱们这作存取操做,如今让咱们来管理这个仓库,咱们如何作到效率最大化。
用户会有不一样大小的货物要寄存,咱们不作特殊处理,就是谁先来了按照固定的顺序存放。以下图
但过了一段时间以后,用户会不按期拿走本身的货物
这时在咱们仓库中就会产生大小不一样的空位,若是这时还有用户来存入货物时,就会发现咱们须要拿着货物在仓库中找到合适的空位放进去(像俄罗斯方块),但用户的货物不必定会正好放到对应的空位中,就会产生不一样大小的空位,并且很差找。
若是在有货物取走以后咱们就整理一次的话,又会很是累也耗时。
这时咱们就会发现,若是咱们不对仓库作有效的划分管理的话,咱们的使用效率很是低。
咱们将仓库逻辑的划分为:
上图划分了俩大区域,左边比较小的是经常使用区域,用户在存入货物时最早放到这里,对于临时存取的货物能够很是快的处理。 右边比较大的区域作为后台仓库,存放长时间无人取的或者经常使用区没法放下的大货物。
经过这样的划分咱们就能够把存取快的小货物在一个较小的区域中处理,而不须要到大仓库中去找,能够极大的提高仓库效率。
JVM的垃圾回收算法是对内存空间管理的一种实现算法,是在逐渐演进中的内存管理算法。
标记-清除算法,就像他的名字同样,分为“标记”和“清除”两个阶段。首先遍历全部内存,将存活对象进行标记。清除阶段遍历堆中全部没被标记的对象进行所有清除。在整个过程当中会形成整个程序的stop the world。
缺点:
为何要stop the world?
举个简单的例子,假设咱们的程序与GC线程是一块儿运行的,试想这样一个场景。
假设咱们刚标记完的A对象(非存活对象),此时在程序当中又new了一个新的对象B,且A对象能够到达B对象。 但因为此时A对象在标记阶段已被标记为非存活对象,B对象错过了标记阶段。所以到清除阶段时,新对象会将B对象清除掉。如此一来GC线程会致使程序没法正常工做。 咱们刚new了一个对象,通过一次GC,变为了null,会严重影响程序运行。
产生内存碎片
内存被清理完以后就会产生像下图3中(像俄罗斯方框游戏同样),空闲的位置不连续,若是须要为新的对象分配内存空间时,没法建立连续较大的空间,甚至在建立时还须要搜索整个内存空间哪有空余空间能够分配。
效率低
也就是上边两个缺点的集合,会形成程序stop the world影响程序执行,产生内存碎片势必在分配时会须要更多的时间去找合适的位置来分配。
为解决标记清除算法的缺点,提高效率,“复制”收集算法出现了。它将可用的内存空间按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一快上,而后把已使用过的内存空间一次清理掉。
这样使每次都是对其中一块进行内存回收,内存分配也不用考虑内存碎片等复杂状况,只要移动指针按顺序分配内存就能够了,实现简单运行高效。
缺点:
针对以上两种算法的问题,又出现了“标记-整理”算法,看名字与“标记-清除”算法类似,不一样的地方就是在“整理”阶段。
在《深刻理解Java虚拟机》中对“整理”阶段的说明是:"让全部存活对象都向一端移动,而后直接清理掉端边界之外的内存"
没有找到具体某一个使用的方案,我分别画了3张图来表示个人理解:
标记-移动-清除
相似冒泡排序,把存活对象像最左侧移动
疑问:
若是肯定边界?记录最后一个存活对象移动的位置,后边的所有清除?
为何不是遇到可回收对象先回收再移动,这样能够减小移动可回收对象的操做(除非回收须要的性能比移动还高)
标记-移动-清除 2
划分移动区域,将存活对象暂时放到该区域,而后一次清理使用过的内存,最后再将存活对象一次移动
疑问:
如何分配逻辑足够存活对象的连续内存空间?
若是空间不足怎么办?
标记-清除-整理
以上我对标记-整理算法理解,若有不对的地方还请指正。
参考资料:
https://liujiacai.net/blog/2018/07/08/mark-sweep/
https://www.azul.com/files/Understanding_Java_Garbage_Collection_v41.pdf
分代收集不是一种新的算法,是针对对象的存活周期的不一样将内存划分为几块。当前商业虚拟机的垃圾收集都采用“分代收集”。
GC分代的基本假设:绝大部分对象的生命周期都很是短暂,存活时间短。
把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。
新生代 每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
老年代 由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
垃圾收集器,就是针对垃圾回收算法的具体实现。
下图是对收集器的推荐组合关系图,有连线的说明能够搭配使用。没有最好的收集器,也没有万能的收集器,只有最合适的收集器。
Serial
特色:
- 单线程、简单高效(与其余收集器的单线程相比),对于限定单个CPU的环境来讲,Serial收集器因为没有线程交互的开销,专心作垃圾收集天然能够得到最高的单线程收集效率。 - 收集器进行垃圾回收时,必须暂停其余全部的工做线程,直到它结束(Stop The World)。
应用场景:
适用于Client模式下的虚拟机
ParNew
ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多线程外其他行为均和Serial收集器如出一辙(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)
特色:
- 多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU很是多的环境中,可使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 - 与Serial收集器同样存在Stop The World问题
应用场景:
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,由于它是除了Serial收集器外,惟一一个能与CMS收集器配合工做的。
Parallel Scavenge
与吞吐量关系密切,故也称为吞吐量优先收集器。 除了使用多线程外其他行为均和Serial收集器如出一辙(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)
特色:
属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器相似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。 当开关打开时不须要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等。 虚拟机会根据系统的运行情况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。 Parallel Scavenge收集器使用两个参数控制吞吐量: XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间 XX:GCRatio 直接设置吞吐量的大小。
Serial Old
Serial Old是Serial收集器的老年代版本。
特色:一样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途
1.在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用。 2.做为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
CMS
一种以获取最短回收停顿时间为目标的收集器。
特色:基于标记-清除算法实现。并发收集、低停顿。
应用场景:
适用于注重服务的响应速度,但愿系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快可是仍存在Stop The World问题。 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。 从新标记:为了修正并发标记期间因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录。仍然存在Stop The World问题。 并发清除:对标记的对象进行清除回收。
CMS收集器的内存回收过程是与用户线程一块儿并发执行的。
CMS收集器的缺点:
G1
一款面向服务端应用的垃圾收集器。再也不是将整个内存区域按代总体划分,他根据,将每个内存单元独立为Region区,每一个Region仍是按代划分。 以下图:
特色:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优点,使用多个CPU来缩短Stop-The-World停顿时间。 部分收集器本来须要停顿Java线程来执行GC动做,G1收集器仍然能够经过并发的方式让Java程序继续运行。 - 分代收集:G1可以独自管理整个Java堆,而且采用不一样的方式去处理新建立的对象和已经存活了一段时间、熬过屡次GC的旧对象以获取更好的收集效果。 - 空间整合:G1运做期间不会产生空间碎片,收集后能提供规整的可用内存。 - 可预测的停顿:G1除了追求低停顿外,还能创建可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1为何能创建可预测的停顿时间模型?
由于它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内能够获取尽量高的收集效率。
G1与其余收集器的区别:
其余收集器的工做范围是整个新生代或者老年代、G1收集器的工做范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代再也不是相互隔离的,他们都是一部分Region(不须要连续)的集合。
G1收集器存在的问题:
Region不多是孤立的,分配在Region中的对象能够与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其余收集器也存在这种问题(G1更加突出而已)。会致使Minor GC效率降低。
G1收集器是如何解决上述问题的?
采用Remembered Set来避免整堆扫描。G1中每一个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),若是是,便经过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set便可保证不对全堆进行扫描也不会有遗漏。
若是不计算维护 Remembered Set 的操做,G1收集器大体可分为以下步骤:
- 初始标记:仅标记GC Roots能直接到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新对象。(须要线程停顿,但耗时很短。) - 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行) - 最终标记:为了修正在并发标记期间因用户程序执行而致使标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(须要线程停顿,但可并行执行。) - 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。(可并发执行)
上边详细说了垃圾收集相关的内容,那有很重要的一点没有说,就是如何肯定某个对象是垃圾对象,可被回收呢? 有下边两种方式,虚拟机中使用的是可达性分析算法。
引用计数法
给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器的数值就+1,当引用失效时,计数器就-1。
任什么时候候计数器的数值都为0的对象时不可能再被使用的。
可达性分析算法 (java使用)
以GC Roots的对象做为起始点,从这些起始点开始向下搜索,搜索所搜过的路径称为引用链Reference Chain,当一个对象到GC Roots没有任何引用链相链接时,则证实此对象时不可用的。
什么是GC Roots?
在虚拟机中可做为GC Roots的对象有如下几种:
汇编指令是指可被虚拟机识别指令,咱们平时看到的.class字节码文件中就存放着咱们某个类的汇编指令,经过了解汇编指令,能够帮助咱们更深刻了解虚拟机的工做机制与内存分配方式。
javap是jdk自带的反解析工具。它的做用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
固然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)须要在使用javac编译成class文件时,指定参数才能输出,好比,你直接javac xx.java,就不会在生成对应的局部变量表等信息,若是你使用javac -g xx.java就能够生成全部相关信息了。
javap的用法格式: javap <options> <classes>
用法与参数: -help --help -? 输出此用法消息 -version 版本信息,实际上是当前javap所在jdk的版本信息,不是class在哪一个jdk下生成的。 -v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息) -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示全部类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示静态最终常量 -classpath <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置
通常经常使用的是-v -l -c三个选项。
下面经过一个简单例子说明一下汇编指令,具体说明会以注释形式说明。
具体指令做用与意思可参考该地址:
http://www.javashuo.com/article/p-kcgbkvmp-mr.html
package com.freecloud.javabasics.javap; /** * @Author: maomao * @Date: 2019-11-01 09:57 */ public class StringJavap { /** * String与StringBuilder */ public void StringAndStringBuilder(){ String s1 = "111" + "222"; StringBuilder s2 = new StringBuilder("111").append("222"); System.out.println(s1); System.out.println(s2); } public void StringStatic(){ String s1 = "333"; String s2 = "444"; String s3 = s1 + s2; String s4 = s1 + "555"; } private static final String STATIC_STRING = "staticString"; public void StringStatic2(){ String s1 = "111"; String s2 = STATIC_STRING + 111; } }
汇编指令
//文件地址 Classfile /Users/workspace/free-cloud-test/free-javaBasics/javap/target/classes/com/freecloud/javabasics/javap/StringJavap.class //最后修改日期与文件大小 Last modified 2019-11-5; size 1432 bytes MD5 checksum 1c6892dd51b214a205eae9612124535d Compiled from "StringJavap.java" //类信息 public class com.freecloud.javabasics.javap.StringJavap minor version: 0 //编译版本号(jdk1.8) major version: 52 flags: ACC_PUBLIC, ACC_SUPER //常量池 Constant pool: #1 = Methodref #18.#45 // java/lang/Object."<init>":()V #2 = String #46 // 111222 #3 = Class #47 // java/lang/StringBuilder #4 = String #48 // 111 #5 = Methodref #3.#49 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V #6 = String #50 // 222 #7 = Methodref #3.#51 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Fieldref #52.#53 // java/lang/System.out:Ljava/io/PrintStream; #9 = Methodref #54.#55 // java/io/PrintStream.println:(Ljava/lang/String;)V #10 = Methodref #54.#56 // java/io/PrintStream.println:(Ljava/lang/Object;)V #11 = String #57 // 333 #12 = String #58 // 444 #13 = Methodref #3.#45 // java/lang/StringBuilder."<init>":()V #14 = Methodref #3.#59 // java/lang/StringBuilder.toString:()Ljava/lang/String; #15 = String #60 // 555 #16 = Class #61 // com/freecloud/javabasics/javap/StringJavap #17 = String #62 // staticString111 #18 = Class #63 // java/lang/Object #19 = Utf8 STATIC_STRING #20 = Utf8 Ljava/lang/String; #21 = Utf8 ConstantValue #22 = String #64 // staticString #23 = Utf8 <init> #24 = Utf8 ()V #25 = Utf8 Code #26 = Utf8 LineNumberTable #27 = Utf8 LocalVariableTable #28 = Utf8 this #29 = Utf8 Lcom/freecloud/javabasics/javap/StringJavap; #30 = Utf8 main #31 = Utf8 ([Ljava/lang/String;)V #32 = Utf8 args #33 = Utf8 [Ljava/lang/String; #34 = Utf8 MethodParameters #35 = Utf8 StringAndStringBuilder #36 = Utf8 s1 #37 = Utf8 s2 #38 = Utf8 Ljava/lang/StringBuilder; #39 = Utf8 StringStatic #40 = Utf8 s3 #41 = Utf8 s4 #42 = Utf8 StringStatic2 #43 = Utf8 SourceFile #44 = Utf8 StringJavap.java #45 = NameAndType #23:#24 // "<init>":()V #46 = Utf8 111222 #47 = Utf8 java/lang/StringBuilder #48 = Utf8 111 #49 = NameAndType #23:#65 // "<init>":(Ljava/lang/String;)V #50 = Utf8 222 #51 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #52 = Class #68 // java/lang/System #53 = NameAndType #69:#70 // out:Ljava/io/PrintStream; #54 = Class #71 // java/io/PrintStream #55 = NameAndType #72:#65 // println:(Ljava/lang/String;)V #56 = NameAndType #72:#73 // println:(Ljava/lang/Object;)V #57 = Utf8 333 #58 = Utf8 444 #59 = NameAndType #74:#75 // toString:()Ljava/lang/String; #60 = Utf8 555 #61 = Utf8 com/freecloud/javabasics/javap/StringJavap #62 = Utf8 staticString111 #63 = Utf8 java/lang/Object #64 = Utf8 staticString #65 = Utf8 (Ljava/lang/String;)V #66 = Utf8 append #67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #68 = Utf8 java/lang/System #69 = Utf8 out #70 = Utf8 Ljava/io/PrintStream; #71 = Utf8 java/io/PrintStream #72 = Utf8 println #73 = Utf8 (Ljava/lang/Object;)V #74 = Utf8 toString #75 = Utf8 ()Ljava/lang/String; { //默认构造方法 public com.freecloud.javabasics.javap.StringJavap(); //输入参数(该处表示无参) descriptor: ()V flags: ACC_PUBLIC //指令代码《也是执行代码,重点关注》 Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return //指令与代码中的行号关系 LineNumberTable: line 7: 0 //本地变量表 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/freecloud/javabasics/javap/StringJavap; // 对应StringAndStringBuilder方法 public void StringAndStringBuilder(); descriptor: ()V //描述方法关键字 flags: ACC_PUBLIC Code: //stack() locals(本地变量数/方法内使用的变量数) args_size(入参数,全部方法都有一个this因此参数至少为1) stack=3, locals=3, args_size=1 //经过#2可在常量池中找到111222字符串,表示在编译时就把本来的"111" + "222"合并为一个常量 0: ldc #2 // String 111222 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: ldc #4 // String 111 9: invokespecial #5 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 12: ldc #6 // String 222 14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: astore_2 18: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 21: aload_1 22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload_2 29: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V //返回指针,不管方法是否有返回值,都会有该指令,做用是出栈 32: return LineNumberTable: line 19: 0 line 20: 3 line 22: 18 line 23: 25 line 24: 32 LocalVariableTable: Start Length Slot Name Signature 0 33 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 30 1 s1 Ljava/lang/String; 18 15 2 s2 Ljava/lang/StringBuilder; public void StringStatic(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=5, args_size=1 0: ldc #11 // String 333 2: astore_1 3: ldc #12 // String 444 5: astore_2 6: new #3 // class java/lang/StringBuilder 9: dup 10: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: new #3 // class java/lang/StringBuilder 28: dup 29: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V 32: aload_1 33: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: ldc #15 // String 555 38: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 41: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 44: astore 4 46: return LineNumberTable: line 27: 0 line 28: 3 line 29: 6 line 30: 25 line 31: 46 LocalVariableTable: Start Length Slot Name Signature 0 47 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 44 1 s1 Ljava/lang/String; 6 41 2 s2 Ljava/lang/String; 25 22 3 s3 Ljava/lang/String; 46 1 4 s4 Ljava/lang/String; public void StringStatic2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=1 0: ldc #4 // String 111 2: astore_1 3: ldc #17 // String staticString111 5: astore_2 6: return LineNumberTable: line 35: 0 line 36: 3 line 37: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 4 1 s1 Ljava/lang/String; 6 1 2 s2 Ljava/lang/String; } SourceFile: "StringJavap.java"
能够在指令集中明确看到咱们上边讲解的内存运行时数据区的一些影子。
好比常量池、本地变量表、虚拟机栈(每一个方法能够理解为一个栈,具体方法内就是Code区)、返回地址(return)