[Java复习] JVM

Part1:Java类加载机制:类加载器,类加载机制,双亲委派模型

1. Java 类加载过程?

类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

三阶段:加载 - 链接 (验证 - 准备 - 解析) - 初始化

加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。

加载阶段:(三件事)

    1. 通过类名获取二进制字节流。

    2. 将字节流的静态存储结构转化为方法区的运行时数据结构。

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

链接阶段:

   1. 验证:确保加载的类的正确性(文件格式验证,元数据验证,字节码验证,符号引用验证)

   2.准备:为类的静态变量分配内存,并将其初始化为默认值

  • 假设一个类变量的定义为: public static int value=1;

    那么变量value在准备阶段过后的初始值为0,而不是1,因为这时候尚未开始执行任何Java方法。

    而把value赋值为1的public static指令是在程序编译后,存放于类构造器 <clinit>()方法之中的,所以把value赋值为1的动作将在初始化阶段才会执行。

  • 假设一个类变量的定义为: public static final int value=1;

          编译时Javac会将value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为1。

          static final常量在编译期就将其结果放入了调用它的类的常量池中。

     3. 解析: 把类中的符号引用转换为直接引用

初始化阶段:为类的静态变量赋正确的初始值,对类进行初始化(主要是类变量初始化)。

    JVM初始化步骤:

       1.假如这个类还没有被加载和连接,则程序先加载并连接该类

       2.假如该类的直接父类还没有被初始化,则先初始化其直接父类

       3.假如类中有初始化语句,则系统依次执行这些初始化语句

2. 描述一下 JVM 加载 Class 文件的原理机制?类加载机制?

   类加载三种方式:
  1.隐式装载,通过new 等方式生成对象时,隐式调用类装载器加载对应的类
  2. 通过Class.forName()等方法,显式加载需要的类
       3.通过ClassLoader.loadClass()方法动态加载

     Class.forName()和ClassLoader.loadClass()区别?

          1.Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

             Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。

          2.ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

     new关键字和newInstance()方法区别?

          1.类加载方式不同:new创建对象时,类可以没有被加载。newInstance()之前,必须保证这个类已经加载,链接了(Class.forName()方法)。

          2. 构造方法不同:new能调用任何构造方法,newInstance()只能调用无参构造方法。

     

        Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,

        至于其他类,则在需要的时候才加载。这是为了节省内存开销。

    加载机制:

     全盘负责:指当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个ClassLoader载入。

     双亲委派:指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

3. 什么是类加载器,类加载器有哪些?

    类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。

    1. 启动类加载器(Bootstrap ClassLoader, C++实现):负责加载存放在 JDKjrelib下,能被JVM识别的类库,如rt.jar。

        所有的java.开头的类均被 Bootstrap ClassLoader加载。

    2. 扩展类加载器(Extension ClassLoader, Java实现):负责加载 JDKjrelibext目录中,如javax.开头的类。

    3. 应用程序类加载器(Application ClassLoader,Java实现):负责加载用户类路径(ClassPath)所指定的类。

4. 类加载器双亲委派模型机制?

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。

因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,

只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

Part2: JVM内存结构

1. Java 内存分配

 JVM内存结构主要有三大块:堆、方法区和栈。

  堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,

   默认情况下年轻代按照8:1:1的比例来分配。

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

   线程共享:堆、方法区

Part3: GC算法 GC收集器

1. GC是什么? 为什么要有GC?

   垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。

2. 简述 Java 垃圾回收机制.

     1. 怎么定义垃圾 2. 垃圾收集算法

3. 如何判断一个对象是否存活?(或者 GC 对象的判定方法, 怎么定义垃圾)

  判断对象存活 2种方法:

    1. 引用计数法 :每当有一个地方引用它时, 计数器就+1;当引用失效时, 计数器就-1, 计数为0回收。缺点:无法解决对象相互引用问题。

    2. 可达性分析法:GC Roots向下搜索,搜索走过路径为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达对象。

  什么样的节点可以作为根节点(GC Roots)?

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

  各种引用:

     强引用Strong Reference:(=) 垃圾收集器不会搜集被引用的对象。

     软引用Soft Reference:只有在内存不足的时候JVM才会回收该对象。这个特性很适合用来实现缓存,比如网页缓存、图片缓存。

     弱引用Weak Reference:当JVM进行来讲收集时,无论内存是否充足,都会被回收。

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

典型引用场景:WeakHashMap, ThreadLocal。

ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。

     虚引用Phantom Reference:在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。能在对象被GC时受到系统通知。

4. 各种垃圾回收机制(算法)的原理。

  1. 标记清除算法(Mark-Sweep)

     首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

      缺点:1. 效率问题,标记和清除过程的效率都不高.

                 2. 空间问题,标记清除之后会产生大量不连续的内存碎片。

   2. 复制算法(Copying)

           将内存一分为二,每次只用其中一块。当一块用完时,将存活的对象复制到另一块,然后把这块清空。

           优点:简单高效。缺点:内存容量缩小为原来一半。对象存活率较高时会执行较多复制,效率低。

    3. 标记压缩算法(Mark-Compact)

         标记过程与“标记清除算法”一样,只是后续不是直接清理,而时让存活对象都向一端移动,再清理边界以外的空间。

         优点:利用率高,没有内存碎片。缺点:比标记清除效率低。

    4. 分代收集算法(Generational Collection)

       把Java堆分新生代和老年代。新生代:存活率低,复制算法。老年代:存活率高,标记清除or标记压缩算法。

5. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

   垃圾回收算法是方法论,回收器时具体实现。

  Serial收集器:

     一个线程回收,稳定,效率高。可能产生较长停顿,STW(Stop The World)。

    新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩。

   参数控制: -XX:+UseSerialGC

   ParNew收集器:

        Serial收集器多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩。

     参数控制:

      -XX:+UseParNewGC ParNew收集器

      -XX:ParallelGCThreads 限制线程数量

    Parallel收集器:

       类似ParNew, parallel更关注吞吐量。可动态调整参数提供最合适的停顿时间或最大吞吐量。新生代复制算法、老年代标记-压缩。

       参数控制: -XX:+UseParallelGC 使用Parallel收集器 + 老年代串行

     Parallel Old 收集器:

         Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

     参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

    CMS收集器(Concurrent Mark Sweep):

       以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的。运作分4个步骤:

     初始标记(CMS initial mark)

     并发标记(CMS concurrent mark)

     重新标记(CMS remark)

     并发清除(CMS concurrent sweep)

     由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

    优点: 并发收集、低停顿

    缺点: 产生大量空间碎片、并发阶段会降低吞吐量

  参数控制:

   -XX:+UseConcMarkSweepGC 使用CMS收集器

   -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

   -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理

   -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

    G1收集器:

    优势:

    1. 空间整合。G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

     2. 可预测停顿。这是G1的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

     G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

   参数控制:

   -XX:+UnlockExperimentalVMOptions

   -XX:+UseG1GC #开启;

   -XX:MaxGCPauseMillis=50 #暂停时间目标;

   -XX:GCPauseIntervalMillis=200 #暂停间隔目标;

   -XX:+G1YoungGenSize=512m #年轻代大小;

   -XX:SurvivorRatio=6 #幸存区比例

 

6. Java 中会存在内存泄漏吗,请简单描述

   程序在申请内存后,无法释放已申请的内存空间,可用内存变小。

7.System.gc() 和 Runtime.gc() 会做什么事情?

  System.gc()实际上就是调用Runtime.getRuntime().gc();

  这个命令只是建议JVM安排GC运行,GC本身是会周期性的自动运行的,由JVM决定运行的时机。

8. finalize() 方法什么时候被调用?

   finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性。

   finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

9. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

   并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。

10. 什么是分布式垃圾回收(DGC)?它是如何工作的?

    RMI 子系统实现基于引用计数的“分布式垃圾回收”(DGC),以便为远程服务器对象提供自动内存管理设施。当客户机创建(序列化)远程引用时,会在服务器端 DGC 上调用 dirty()。当客户机完成远程引用后,它会调用对应的 clean() 方法。

11.串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

   串行GC:单线程。吞吐量GC(Parallel Scavenge收集器):多线程。

12. 在 Java 中,对象什么时候可以被垃圾回收?

   没有任何引用指向该对象时。

13. Minor GC、Major GC和Full GC之间的区别

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

1.Partial GC:并不收集整个GC堆的模式:

  Young GC:只收集young gen的GC

  Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式

  Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

2. Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式

-----

Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;

Minor GC:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC.

当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。

质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。

其中的真相就是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。

有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC.

14. JVM 的永久代中会发生垃圾回收么?

“相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。”

--周志明. 深入理解Java虚拟机

Java8中已经移除了永久代,新加了一个叫做元数据区。

15. 永生代和方法区的关系?

永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。

永久代物理上是堆的一部分,和新生代,老年代地址是连续的。而元空间属于本地内存。

元空间存储类的元信息;静态变量和常量池等移到堆中 了。

为什么要替换?

永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到内存溢出的问题。

对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题。

参考文章:https://zhuanlan.zhihu.com/p/34426768

原文地址:https://www.cnblogs.com/fyql/p/11660774.html