【001 Java语言和JVM虚拟机】
Java语言是一种先编译后解释的语言。JVM是在操作系统之上的虚拟处理器,工作流程主要包括加载class文件、管理并分配内存、执行垃圾收集。
【002 JVM类加载器的执行流程】
装载流程包括加载、链接和初始化,其中类加载器只负责加载流程。
加载:取得class文件的二进制流,并在堆空间创建class对象的引用。
链接:将二进制流合并到JVM虚拟机中,通过文件验证、准备(例如为static和final域设置初始化信息)、解析(例如将符号引用直接替换为地址引用),实现与平台相关。
初始化:执行类构造器的过程,如果没有通过new可以不执行。
【003 JVM类加载器的分类】
从上到下可以分为四类:BootStrap ClassLoader、External ClassLoad、App ClassLoad和Custom ClassLoad。BootStrap ClassLoader是类加载器的顶级,主要负责对rt.jar进行加载,加载的都是JVM运行的核心类库文件,如IO、Lang、JDBC;External ClassLoader主要是对jdk的其他扩展类库进行加载,如Resource、Swing、Security;App ClassLoader主要是对开发者提供的类进行加载,是最常见的加载器;Custom ClassLoader是要自定义的加载器,重新实现类的加载方法,如Tomcat就定义了加载器以改变加载双亲规则。
【004 JVM类加载模式】
JVM默认的加载模式是双亲加载,加载class文件时会从底层加载器开始检查,是否已用该类型加载器加载了class文件,如果已经加载,则返回已加载的类引用,如果没有加载,则会把加载权限交给上一级的加载器。如果在BootStrap ClassLoader中也没有找到加载的类,则尝试从顶层加载器开始加载该类,如加载不了,则交给下一级加载器进行加载。
双亲模式加载的好处是,避免了底层加载器修改上层加载器的加载规则,比较安全。
双亲模式加载的缺点是,子类加载器可以通过成员parent看到父类,而父类加载器看不到子类加载器的信息,如果存在例如JDBC这样的接口定义在jt.jar父类加载器,而具体实现类在App ClassLoader的情况,就会加载异常。需要通过在上下文中传入子类加载器来解决。
【005 Tomcat类加载模式】
Tomcat打破了双亲加载这一模式,自定义的WebAppClassLoader可以直接加载应用类,而不会先交给父类加载器。这样做的原因是因为一个JVM虚拟机里可以存在多个Web应用,双亲加载的模式会导致父类加载的类库会被多个应用共享,做不到应用隔离。同时,Tomcat的类加载器还提供了热部署的功能,不用重启就可以重新加载类库。
【006 自定义类加载器】
首先,要自定义一个继承了ClassLoader抽象类的子类,并为其指定父类加载器;其次,需要重写findClassLoader方法指定在何处获取已加载的类库和loadClass方法指定如何加载类库;最后,还需要通过应用程序显示制定类加载器(从这点上看,用new方法无法使用自定义的类加载器)。
【007 JVM的内存结构】
JVM内存可以分为方法区、栈区、堆区和本地方法区,此外还有寄存器和直接内存区域。其中,栈区和寄存器是归属于线程的,每个线程都有一份独立的空间;而其他空间是归属于进程的,所有线程共享。本地方法区可以直接调用内存。
【008 栈的内存分配】
栈区可以分为操作栈、局部变量栈等,用于保存线程运行的局部数据。另外,JVM还存在一种栈上分配的技术,如果启用DoEscapeAnalysis,那么被线程独享的小对象,如byte数组,可以被分配到栈空间,避免垃圾回收的消耗。
【008扩展如何使一个程序的函数调用尽可能深?】
一是扩大栈的内容空间,通过-Xss设置;二是减少一次调用使用的栈空间,方法包括减少局部变量的空间,少用long、double,减少参数个数,此外还可以关闭JVM栈上分配。
【009 堆的内存分配】
堆空间可以分为新生代、老年代和永久区。新生代通常占整个堆空间的3/8,可以分为edon、from和to三个子区域,比例分别为8:1:1,新创建的对象通常情况下会被分配到edon区域,在第一次垃圾回收后会被转移到其他区域。老年代存放的是存活时间较长的对象,或是对象的空间较大,在初始化创建或第一次垃圾回收时,就被直接放入。永久区严格意义上说,是方法区的实现,在JDK1.7后被移入堆内存,在JDK1.8后被移入元区。
【010 JVM内存异常】
内存异常,常见的两个错误是OutofMemoryException和StackOverFlowException。前一个异常在内存的所有区域都可能抛出,原因是空间内存不足或者空间泄漏。后一个异常出现在栈区,由于调用的深度超出限制,在实际的操作中,由于栈空间的大小和递归深度正相关,通常栈区更容易出现StackOverFlowException。
【011 JVM内存参数】
① 堆的分配参数:-Xmx和-Xms,最大堆空间和最小堆空间,一般设置为一样大小,否则在运行过程中JVM需要动态调整堆空间大小;-Xmn,新生代大小;-XX:NewRatio:设置新生代的比例;-XX:SurvivoRatio:设置edon:from的比例;-XX:OnOutOfMemoryError:内存溢出时可以指定脚本报错或重启JVM。
② 栈的内存分配:-Xss,通常根据内存的大小分配在几百K到1M之间。
③ 永久区分配:-XX:PermSize,-XX:MaxPErmSize,永久区的最小和最大空间。
【012 JVM内存模型】
JVM的共享内存,在被线程使用时,通常会拷贝一个副本放入线程工作区域,做到数据的互相隔离,当然这也会造成多个线程共享的数据不同步问题。对于Volatile变量,规定线程每次读取时必须从主内存中加载,而修改后必须同步到主内存中,保证数据的同步,但是加载(read,load)和更新(store,write)操作都不是原子性的操作,会造成线程不安全。对于Synchronized变量,是通过锁机制,保证数据在同一时间内只能被获得锁的线程访问,从而保证线程安全,但只有释放锁后才会把数据写回到主内存。对于final变量,虽然和普通变量一样使用,但由于其是不可修改的,因此也不存在同步问题。
【013 垃圾回收器算法】
GC是JVM的后台线程,只有在系统空闲并且内存区域不足时才会执行。
① 引用计数法,循环引用问题,Python
② 标记-清理算法,标记从root可达的所有对象,清除不可达对象,会造成零碎空间问题
③ 标记-压缩算法,标记从root可达的所有对象,移动到连续内存,然后清除其他所有空间
④ 复制算法,两个一样的内存块空间,把其中一块的可达对象移动到另一块连续存储,然后清除前一块的空间
【014扩展什么是root可达?】
从栈空间/方法区的Static成员/常量池/Native方法栈存在对象引用链,可以访问到对象。
【015 堆区采用的GC算法】
① 新生代的edon区域采用标记-清理算法,80%的新生代对象在第一次GC时就会被清理,标记-清理算法是最快的;
② 新生代的from和to区域采用复制算法,超过年龄限制的对象会被清理到老年代;
③ 老年代的GC并不是太频繁,采用标记-压缩算法,尽量避免零碎区域FULL GC。
【016 GC收集器实例】
① 串行回收器,暂停所有用户线程,新生代复制,老年代标记-压缩,适用于单CPU的应用;
② 并行回收器ParNew,暂停所有用户线程,新生代回收器,并行复制,适用于多核应用;
③ 并行回收器Parallel,暂停所有用户线程,新生代并行复制,老年代并行标记-压缩,可分开使用,更注重吞吐量;
④ 并发回收器CMS,是目前JVM虚拟机默认的GC算法,目标是最短时间回收,允许用户线程与回收线程并发执行,采用标记-清理算法,常用于老年代;CMS的回收分为初始标记、并发标记、重新标记、并发清理和并发重置,其中初始标记和重新标记是不允许并发的,并发重置是对产生的零碎空间进行压缩,但不是每次都要执行;
⑤ G1回收器,是JDK1.7之后提出的回收算法,把堆划分成若干个大小一致的区域,打破了新生代、老年代的物理隔离;标记算法同CMS,但是回收时会优先选择要回收区域最大的空间进行回收,保证可用的连续空间最大化。
【016扩展1 finalize和System.GC】
① Finalize是Object类的方法,会在第一次调用GC时被触发,对类进行清理,但也可以重写该方法复活对象,创建新的引用指向自身this;另外不推荐在该方法中进行资源的释放,因为该方法执行的时间不可控。
② System.GC是系统方法,可以通过显示调用通知JVM虚拟机需要进行垃圾回收,但并不能保证虚拟机的立即执行。
【016扩展2 STOP-THE-WORLD】
STOP-THE-WORLD是指从执行效果看,JVM虚拟机出现全局暂停的问题,GC是导致该现象的一个原因。这一现象对系统运行存在危害,例如主从备份的机器,主机发现系统暂停,会主动切换到备机执行,此时主机恢复运行,会出现不同步问题。
【016扩展3 JDK1.8的分代变化】
最大的变化是将永久区移到了元空间,其垃圾回收算法采用标记-压缩。该变化的原因是为了更好的控制类加载器和class对象的内存空间,因为永久区很少会执行GC,导致不用的类加载器和class对象不能被及时删除。
【017 JVM指令重排】
指令重排是指编译器或运行时环境,会根据指令的执行情况,优化各指令的执行顺序。指令重排对于一个线程的执行顺序没有影响,但是多个不同线程的执行中,会造成执行顺序变化,得到不同的执行结果。因此在指令执行过程中,需要通过各种锁机制保证执行顺序。
【018 JVM锁机制】
JVM锁与Java语言中的Lock和Synchronized对应的层次不同,是虚拟机执行的真正机制。JVM的锁机制更适用于竞争不频繁的场景,避免真正加锁,如果竞争激励,建议关闭JVM锁优化。
① 偏向锁,首次使用的线程会占有该锁,同一线程再次访问不受限制,如果其他线程需要访问,则偏向结束,需要进行Lock竞争;
② 轻量级锁,如果对象没有被锁定,则可以使用,如果存在竞争,则需要进行Lock竞争;
③ 自旋锁,如果需要的对象被锁定,可以执行一段空语句,再看是否还被占用;
④ 无锁CAS,check and swap,每次执行都检查对象是否被修改,被修改则进行修正。
【019 Java语法糖】
① 泛型与类型擦除:List经过编译后的实际类型为List,因此如果要真正限制集合类型,可以使用List,泛型擦除后的结果为List;
② 自动装箱和拆箱:包装类型Integer和基本类型int之间的互相比较,会默认转为int类型;Integer与Integer类型的==比较,[-128,127]区间范围内编译器会认为是int类型比较,否则就是对象地址比较;
③ foreach的遍历循环,编译时需要转化为下标方式,因此执行效率较低。