怎样解析JVM虚拟机
本篇文章给大家分享的是有关怎样解析JVM虚拟机,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:主机域名、网站空间、营销软件、网站建设、曲江网站维护、网站推广。
什么是JVM虚拟机
首先我们需要了解什么是虚拟机,为什么虚拟机可以实现夸平台,虚拟机在计算机中扮演一个什么样的角色。
(从下向上看)
看上图的操作系统与虚拟机层,可以看到,JVM是在操作系统之上的。他帮我们解决了操作系统差异性操作问题,所以可以帮我们实现夸操作系统。
JVM是如果实现夸操作系统的呢?
接着向上看,来到虚拟机可解析执行文件这里,虚拟机就是根据这个.class的规范来实现夸平台的。
在向上到语言层,不同的语言可以有自己的语法、实现方式,但最终都要编译为一个满足.class规范的文件,来让虚拟机执行。
所以理论上,任何语言想使用JVM虚拟机实现夸平台的操作,都可以根据规范生成.class文件,就可以使用JVM,并实现“一次编译,多次运行”。
虚拟机具体帮我们都做了哪些工作?
字节码规范(.class)
内存管理
第一点已经在上边说过,不在重复。
第二点内存管理也是我们接下来主要讲的内容。在没有JVM的时代,在C/C++时期,写代码中除了写正常的业务代码之外,有很大一部分代码是内存分配与销毁相关的代码。稍有不慎就会造成内存泄露。而使用虚拟机之后关于内存的分配、销毁操作就都由虚拟机来管理了。
相对的肯定会造成虚拟机占用更多内存,在性能上与C/C++对比会较差,但随着虚拟机的慢慢成熟性能差距正在缩小。
JVM架构
Jvm虚拟机主要分为五大模块:类装载子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。
ClassLoader(类加载)
类的加载过程包含以下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子类。
App ClassLoader(系统<应用>类加载器)
Extension ClassLoader(扩展类加载器)
BootStrap ClassLoader(根加载器)
用户自定义的类加载器
其一定是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.String: 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对象来表示该类,即执行类构造器
1.()方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。 2. 虚拟机会确保先执行父类的 ()方法。 3. 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成 ()方法。 4. 虚拟机会保证 ()方法的执行过程是线程安全的。
使用
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 对象没有在任何地方被引用。 如:不能在任何地方通过反射访问该类的方法。
运行时数据区(虚拟机的内存模型)
运行时数据区主要分两大块: 线程共享:方法区(常量池、类信息、静态常量等)、堆(存储实例对象) 线程独占:程序计数器、虚拟机栈、本地方法栈
程序计数器(PC寄存器)
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
特点: 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(); } }
本地方法栈
方法区(Method Area)
方法区,主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
常亮池中的值是在类加载阶段时,通过静态方法块加载到内存中
Heap(堆)
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。也是我们在开发过程中主要使用的地方。
Heap的数据是二叉树实现,每个分配的地址会存储内存地址、与对象长度。
在jdk 1.8之前的版本heap分新生代、老年带、永久代,但在1.8之后永久代修改为元空间,本质与永久代类似,都是对JVM规范中方法区的实现。元空间不在虚拟机中,而是在本地内存中。
为什么内存要分代?
我们使用下面一个生活中的例子来说明:
首先我们把整个内存处理过程比作一个仓库管理,用户会有不同的东西要在我们仓库做存取。
仓库中的货物比作我们内存中的实例,用户会不确定时间来我们这做存取操作,现在让我们来管理这个仓库,我们如何做到效率最大化。
用户会有不同大小的货物要寄存,我们不做特殊处理,就是谁先来了按照固定的顺序存放。如下图
但过了一段时间之后,用户会不定期拿走自己的货物
这时在我们仓库中就会产生大小不同的空位,如果这时还有用户来存入货物时,就会发现我们需要拿着货物在仓库中找到合适的空位放进去(像俄罗斯方块),但用户的货物不一定会正好放到对应的空位中,就会产生不同大小的空位,而且不好找。
如果在有货物取走之后我们就整理一次的话,又会非常累也耗时。
这时我们就会发现,如果我们不对仓库做有效的划分管理的话,我们的使用效率非常低。
我们将仓库逻辑的划分为:
最常用:用户所有的货物都先进入到这里,如果用户只是临时存放,可以快速从这里取走。除非货物大小超过仓库剩余空间(或我们认定的大货物)。
临时缓冲1、2:临时缓冲存放,存放小于一定天数的货物暂时放到这里,当超出天数还未取走再放到后台仓库中。
后台仓库:存放大货物与长期无人取的货物
上图划分了俩大区域,左边比较小的是常用区域,用户在存入货物时最先放到这里,对于临时存取的货物可以非常快的处理。 右边比较大的区域做为后台仓库,存放长时间无人取的或者常用区无法放下的大货物。
通过这样的划分我们就可以把存取快的小货物在一个较小的区域中处理,而不需要到大仓库中去找,可以极大的提升仓库效率。
垃圾回收算法
JVM的垃圾回收算法是对内存空间管理的一种实现算法,是在逐渐演进中的内存管理算法。
标记-清除
标记-清除算法,就像他的名字一样,分为“标记”和“清除”两个阶段。首先遍历所有内存,将存活对象进行标记。清除阶段遍历堆中所有没被标记的对象进行全部清除。在整个过程中会造成整个程序的stop the world。
缺点:
造成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收集器的缺点:
对CPU资源非常敏感。
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
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查看到指令集
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
当然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)需要在使用javac编译成class文件时,指定参数才能输出,比如,你直接javac xx.java,就不会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。
javap的用法格式: javap
用法与参数: -help --help -? 输出此用法消息 -version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。 -v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息) -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示静态最终常量 -classpath指定查找用户类文件的位置 -bootclasspath 覆盖引导类文件的位置
一般常用的是-v -l -c三个选项。
下面通过一个简单例子说明一下汇编指令,具体说明会以注释形式说明。
具体指令作用与意思可参考该地址:
https://my.oschina.net/u/1019754/blog/3116798
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."":()V #2 = String #46 // 111222 #3 = Class #47 // java/lang/StringBuilder #4 = String #48 // 111 #5 = Methodref #3.#49 // java/lang/StringBuilder." ":(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." ":()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 #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 &nbs
分享题目:怎样解析JVM虚拟机
分享URL:http://hbruida.cn/article/jdejhj.html