Java基础 - JVM

1.介绍Java内存区域(运行时数据区)

      Java代码执行过程如下,

 运行时数据区,即JVM内存结构图如下,

        Java虚拟机(JVM)在执行Java程序的过程中,会把它管理的内存划分成若干个不同的数据区域。JDK1.8和之前的版本略有不同,下图展示了JDK1.8之前和JDK1.8数据区域,

Java基础 - JVM_第1张图片Java基础 - JVM_第2张图片

   线程共享的:堆、方法区、直接内存(非运行时数据区的一部分)。

   线程私有的:程序计数器、本地方法栈、虚拟机栈。

   程序计数器是唯一一个不会出现OOM的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

   Java虚拟机栈,即栈内存,里面存放了编译器可知的各种数据类型(int、byte等)、对象引用(reference类型,不同于对象本身)。Java虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

   本地方法栈,与虚拟机栈发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,本地方法栈为虚拟机使用到的 Native 方法服务。

   堆,即堆内存,此内存的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

   方法区,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

   运行时常量池,方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译器生成的各种字面量和符号引用)。

Java基础 - JVM_第3张图片

2.Java对象的创建过程Java基础 - JVM_第4张图片

 (1)类加载检查

        虚拟机遇到一条new指令时,首先要去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析、初始化过,若是没有,则必须先执行相应的类加载过程。

(2)分配内存

        在类加载检查通过后,接下来虚拟机会为新生对象分配内存。对象所需的内存在类加载完成后便可以确定,为对象分配内存的任务就等同于把一块大小确定的内存从Java堆中划分出来。

     分配方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式是由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。     

 内存分配的两种方式:选择两种方式中的哪一种取决于Java堆内存是否规整。而Java堆内存是否规整又取决于GC收集器的算法是“标记-清除”还是“标记-整理(标记-压缩)”。

Java基础 - JVM_第5张图片

  内存分配并发问题:在实际的开发中,创建对象是很频繁的,作为虚拟机来说,必须要保证线程是安全的。通常,虚拟机采用两种方式来保证线程安全,

      a. CAS+失败重试:CAS是乐观锁的一种实现方式。乐观锁,就是每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS + 失败重试的方式保证更新操作的原子性。

      b. TLAB:每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),分配内存的时候在TLAB上分配,互不干扰。

(3)初始化零值

        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这样保证了对象即使没有赋值,也可以直接使用。

(4)设置对象头

        初始化零值完成之后,虚拟机要对对象进行必要的设置。如,这个对象所属的类、类的元数据信息、对象的哈希码等,这些信息都存放在对象头中。

(5)执行init方法

        完成以上操作后,从虚拟机的角度看,一个新的对象已经产生了,但是从Java程序的角度看,对象创建才刚开始,init方法还没有执行,所有的字段都还为零。所以new指令之后接着执行init方法,这样一个对象才算产生出来。

3.对象的访问定位有哪两种方式(已经创建的对象,如何找到这个对象)?

      创建对象是为了使用对象,Java程序通过栈上的reference数据(对象引用)来操作堆上的具体对象(对象实例)。对象的访问方式由虚拟机的实现而定,目前主流的访问方式有使用句柄和直接指针两种

(1)句柄

        如果使用句柄的话,那么Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据的具体地址信息。

Java基础 - JVM_第6张图片

 (2)直接指针

        如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象的类型、实现的接口、方法、父类等),而reference中存放的就是对象的地址

Java基础 - JVM_第7张图片

 (3)总结

        两种方式各有优势。使用句柄访问时,当对象被移动(GC中移动对象很常见)时,只会改变句柄中的实例数据指针,reference本身不需要修改。使用直接指针,可以直接获取到对象的内容,速度快,节省了一次指针定位的时间开销(句柄需要两步才能获取到对象内容,时间上有损失)。

4.说一说堆内存中对象的分配的基本策略

        堆空间的基本结构,包括Eden区、s0区、s1区以及tentired区。其中,Eden区、s0区、s1区属于新生代,tentired属于老年代。

Java基础 - JVM_第8张图片

        大部分情况,对象会首先在eden区分配,在一次新生代垃圾回收后,如果对象还存在,则会进入s0或s1,并且对象的年龄会加1。当年龄增加到一定程度(默认是15),就会晋升到老年代。另外,大对象和长期存活的对象会直接进入老年代。

Java基础 - JVM_第9张图片

5.Minor GC和Full GC(Major GC)有什么不同呢?

      大多数情况下,对象在eden区分配,当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,

      Minor GC:发生在新生代的垃圾收集动作,Minor GC动作非常频繁,回收速度一般比较快;

      Major GC/Full GC:发生在老年代的GC,出现了Major GC一般会伴随至少一次的Minor GC(并非绝对),Major GC速度一般比Minor GC慢10倍以上。

Full GC会”Stop The World“。

5.1 Stop -  the - World

(1)含义

        GC 在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即 GC 停顿,会带给用户不良的体验;

(2)原因

        可达性分析的时候为了确保快照的一致性,需要对整个系统进行冻结,不可以出现分析过程中对象引用关系还在不断变化的情况,也就是Stop-The-World。

        举例:你在做家务,正在计算家里有多少垃圾的时候,是不能允许别人在这时候清理或者增加垃圾的,否则你的计算将毫无意义。所以在这个时候,你需要把家里的人都关在门外,等你计算好垃圾的数量之后才能让他们进来。

6. 如何判断对象是否死亡?

        堆中存放着几乎所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡,

(1)引用计数法

        给对象添加一个引用计数器,每当有一个地方使用它,计数器就加1;当引用失效,计数器就减1。任何时候,计数器为0的对象是不可能再被使用的。

(2)可达性分析算法

        该算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链的话,则证明该对象是不可用的。

Java基础 - JVM_第10张图片

7. 介绍一下强引用、软引用、弱引用、虚引用

      无论是采用“引用计数法”判断对象引用数量,或是采用“可达性分析算法”判断对象的引用链是否可达,判断对象的存活都与“引用”有关。

(1)强引用(StrongReference) :必不可少

        使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。当内存空间不足时,虚拟机宁愿抛出OOM错误使程序终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

(2)软引用(SoftReference):可有可无

        软引用可以用来实现内存敏感的高速缓存。当一个对象只具有软引用,当内存空间足够时,垃圾回收器不会回收它;当内存空间不足时,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

(3)弱引用(WeakReference):可有可无

        弱引用与软引用的区别在于,只具有弱引用的对象拥有更短的生命周期。垃圾回收器在扫描它所管辖的内存区域的时候,一旦发现只具有弱引用的对象,不管当前内存足够与否,都会回收它的内存。

(4)虚引用((PhantomReference):形同虚设

        与其他引用不同,虚引用并不会影响对象的生命周期。如果一个对象仅具有虚引用,那么就和没有任何引用一样,在任何时候都可能会被回收。虚引用主要用来跟踪对象被垃圾回收的活动

(5)总结

        软引用使用比较多,因为它可以加速JVM对垃圾内存的回收速度、维护系统的运行安全、防止内存溢出等问题的产生。

8.如何判断一个常量是废弃常量?

      运行时常量池主要回收的是废弃常量

      假设常量池中存在字符串“abc”,而当前没有任何String对象引用该字符串常量的话,就说明常量“abc”是废弃常量。如果这时发生垃圾回收的话,“abc”就会被系统清理出常量池。

9. 如何判断一个类是无用的类?

      方法区主要回收的是无用的类。类需要同时满足以下3个条件才算是无用的类:

(1)该类所有的实例都已被回收,即Java堆中不存在该类的任何实例;

(2)加载该类的类加载器(ClassLoader)已被回收;

(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射来访问该类的方法。

      虚拟机可以对满足以上3个条件的无用类进行回收,但仅仅是可以,而并不是和对象一样,不使用了就会必然被回收。  

10.垃圾收集有哪些算法?各自的特点?

(1)标记 - 清除算法

        该算法分为两个阶段:标记和清除阶段。首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:效率问题、空间问题(标记清除后会产生大量不连续的碎片)。是最基础的算法,后续的算法是对其不足改进得到的。

Java基础 - JVM_第11张图片

(2)复制算法

       为了解决效率问题。将内存分为大小相同的两块,每次使用其中一块。当其中一块内存使用完之后,就将还存活的对象复制到另一块中,再直接将使用的空间一次性清理掉。这样每次的回收都是对内存区间的一半进行回收。实现简单、效率高、内存空间连续没有碎片,但需要两倍的内存。

Java基础 - JVM_第12张图片

(3)标记 - 整理算法

        标记过程与”标记 - 清除算法“一致,标记完之后,将所有的存活对象压缩(整理)到内存一端,按序存放,最后直接清理掉空余空间。 没有复制算法的两倍内存的问题,但是效率没有复制算法高,并且若在移动对象的过程中,对象被其他对象引用,还需要调整引用地址。

Java基础 - JVM_第13张图片

(4)分代收集算法

        当前虚拟机都采用分代收集算法。该算法根据对象存活的周期不同,将内存分为几块。一般将Java堆分为新生代和老生代,可以根据各个年代的特点选择合适的垃圾收集算法。

        如,新生代中,若每次收集都有大量对象死去,则可以选择复制算法。老生代中对象存活的机率比较高,且没有额外的空间对它进行分配担保,所以必须选择”标记 - 清除“或”标记 - 整理“算法进行垃圾收集。

11.HotStop虚拟机为什么要分为新生代和老生代?

      主要是为了提高GC效率。上述分代收集算法很好的解释了。

12.1 查看垃圾回收器

      JCM 分为 client 模式和server 模式,如果启动 JVM 不指定模式,JDK 会根据当前操作系统的配置来启动不同模式的 JVM。默认64位操作系统下都是 server 模式。

12.常见的垃圾收集器有哪些?

    垃圾收集器:Serial、ParNew、Parallel Scavenge、CMS、G1。

        JVM是一个进程,垃圾收集器就是一个线程。垃圾收集线程是一个守护线程,优先级低,其在当前系统空闲或堆中老年代占用率较大时触发。

        没有万能的垃圾回收器,要做的是根据具体场景选择适合自己的垃圾收集器。

       串行:用户线程和垃圾回收线程交替执行,当执行垃圾回收线程时暂停用户线程,因此会出现Stop The World,并且垃圾回收线程是单线程的,并不是CPU是单核,在多核CPU下串行垃圾回收也是单线程。

       并行:和串行基本一致,唯一不同的就是垃圾回收是多线程并行进行,不再是单线程,需要在多核CPU环境下,用户线程会被暂停,同样出现Stop The World。

       并发:在多核CPU环境下,用户线程和垃圾回收线程同时执行,也就是在同一时刻,CPU0执行用户线程,CPU1执行垃圾回收线程。

 (1)Serial

        新生代收集器,使用复制算法。client 模式下的默认垃圾收集器。        串行。       

        是单线程收集器,但它的”单线程“的意义不仅仅是它只会使用一条垃圾收集线去完成垃圾收集任务,同时在它进行垃圾收集工作的时候,必须暂停其他所有的工作线程(Stop The World),直到它收集结束。简单高效,没有线程交互的开销,Client模式下默认的年轻代收集器。

    适用场景:对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。       

(2)ParNew收集器

        可以认为是 Serial 的多线程版,除了使用多线程进行垃圾收集外,其余行为(回收算法、回收策略等)与 Serial 一致。也是新生代收集器。        复制算法。        串行。         多线程。

       多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。

        运行在 server 模式下的虚拟机首选的新生代收集器。

(3)Parallel Scavenge收集器

        类似于 ParNew 收集器。该收集器以高吞吐量为目标(即减少垃圾收集的时间),GMS 等收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

        所谓吞吐量就是CPU中用于运行用户代码的时间与 CPU 总消耗时间的比值。

        新生代收集器。        复制算法。        并行。        多线程。

        虚拟机运行在Server模式下的默认垃圾收集器。适合交互少、运算多的场景,如批量处理、订单处理。

(4)GMS收集器

        是一种以获取最短回收停顿时间为目标的收集器,第一次实现了让垃圾收集线程和用户线程(基本上)同时工作,即并发。        标记 - 清除算法。        并发。        多线程。

        并发收集、低停顿,但对CPU资源敏感、无法处理浮动垃圾、使用的垃圾回收算法会产生大量空间碎片。

        注重服务响应速度的场合使用多。

(5)G1收集器

        基于标记 - 整理算法实现,不会产生空间碎片。是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

        取消了新生代和老年代的划分。可并行、可并发。空间整合,不产生碎片。

(6)Serial Old 收集器

        Serial Old是Serial的老年代版本,除了采用标记-整理算法,其他与Serial相同。

(7)Parallel Old

        Parallel Old是Parallel Scavenge的老年代版本,Parallel Old 老年代采用的是标记 - 整理算法,其他特点与Parallel Scavenge相同。

        在注重吞吐量以及CPU资源敏感的场合可以考虑。

13.类加载过程

        类加载过程:加载  -->  连接  --> 初始化。连接过程又可分为三步:验证  --> 准备  --> 解析。

Java基础 - JVM_第14张图片

 (1)加载:主要完成以下三件事

        a. 通过全类名获取定义此类的二进制字节流;

        b. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;

        c. 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。

      一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,可以使用系统提供的引导类加载器,还可以由用户自定义的类加载器(重写类加载器的loadClass方法)完成。数组类型的加载阶段不通过类加载器创建,它由Java虚拟机直接创建。

获取二进制字节流:对于Class文件,虚拟机没有指明从哪里获取、怎样获取。除了直接从编译好的.class文件中读取,还有以下几种方式,从zip包中读取,如war、jar等;从网络中获取,如Applet;通过动态代理技术生成代理类的二进制字节流。

     加载阶段和连接阶段的部分是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

(2)验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的要求

       a. 文件格式验证:验证类文件的魔术版本号是否符合当前虚拟机支持的范围。

       b. 元数据验证:验证类的语义信息是否符合Java语言规范的要求。

       c. 字节码验证验:证程序员语义是合法的。

       d. 符号引用验证。

(3)准备:给类变量(被static修饰的变量)在方法区分配内存。

Java基础 - JVM_第15张图片

(4)解析:虚拟机将常量池内的符号引用替换为直接引用的过程。

(5)初始化:对类变量和静态代码块执行初始化操作。

14. 类加载器

      JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader,其余类加载器均由Java实现且都继承自java.lang.ClassLoader,

(1)BootstrapClassLoader(启动类加载器)

        最顶层的加载器,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类,或被

-Xbootclasspath参数指定的路径中所有的类。

(2)ExtensionClassLoader(扩展类加载器)

        主要负责%JRE_HOME%/lib/ext目录下的jar包和类,或被java.ext.dirs系统变量所指定的路径下的jar包。

(3)AppClassLoader(应用程序类加载器)

        面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

15.双亲委派模型

        每个类都有一个对应它的类加载器,每一个类加载都有一个父类加载器。系统中的ClassLoader在协同工作的时候会默认使用双亲委派模型。

(1)概念

       当需要加载一个类的时候,子类加载器并不会马上去加载,而是依次去请求父类加载器加载;如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

Java基础 - JVM_第16张图片

 (2)双亲委派模型的好处

        可以防止内存中出现多份同样的字节码,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,而且如果不使用这种双亲委派模型将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。

        保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。

(3)如果不想使用双亲委派模型怎么办?

        为了避免双亲委托机制,可以自定义一个类加载器,然后重载loadClass()即可。

(4)如何自定义类加载器?

    除了BootstrapClassLoader,其他类加载器均由Java实现且全部继承自java.lang.ClassLoader。因此,如果要自定义类加载器,就需要继承ClassLoader。

你可能感兴趣的:(java,jvm,开发语言)