JVM

2019-02-23  本文已影响0人  rabbittttt

1 JVM 运行时内存区域划分
ans: (1)程序计数器:这部分空间用于保存当前线程所执行的字节码的行号,字节码解释器利用这个计数器的值来选去下一条主要执行的字节码指令。每条线程都有一个独立的程序计数器,各线程之间不互相影响。但如果执行的是Native方法,这个计数器的值为空。
(2)Java虚拟机栈:线程执行每个方法时创建一个栈帧,用于保存局部变量表(存放方法参数和方法内部定义的局部变量)、操作数栈(用于字节码执行时操作数据)、动态链接、方法返回地址等信息。


栈帧的结构

(3)本地方法栈:与虚拟机栈功能相同,但本地方法栈是虚拟机运行Native方法时使用,Java虚拟机规范并没有强制规定它的实现方式,有些虚拟机实现直接把本地方法栈和虚拟机栈合二为一了(Sun HotSpot)。
(4)Java堆:Java堆是被所有线程共享的一块内存区域,用于存放对象实例,几乎所有对象实例和数组都在堆上分配(栈上分配、标量替换 例外)。从内存回收的角度看,Java堆可细分为新生代、老年代。从内存分配角度来看,Java堆中可以划分出线程私有的分配缓冲区。
(5)方法区:也是一个线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在这个区域虚拟机可以选择不实现垃圾回收。
方法区中有一个重要的部分是运行时常量池,用于存放编译期生成的各种字面量和符号引用。在运行期间,开发人员也可以将新的常量放入常量池,用的较多的就是String类的intern()方法。
参考:《深入理解JVM》2.2

2 常见的 GC 回收算法,CMS&G1
ans:(1)标记-清除算法,它是现代GC算法的思想基础,基本思路是将GC分为两个阶段:标记和清理,先把活动对象标记出来(从根节点遍历),然后将没有被标记的对象统一清理掉。这里主要存在两个问题:一是标记和清理的效率都不高;而是清理之后会产生大量不连续的内存碎片,碎片太多可能导致大对象无法分配到足够的空间,从而再次触发GC。
(2)复制算法,是在标记-清除算法的基础上,试图解决产生碎片的问题。这个算法会将内存分为大小相等的两块,每次只使用其中一块,GC的时候就将仍然存活的对象复制到另一块上,然后把已使用的内存一次清理干净,这样就不用考虑碎片问题。实现简单,运行高效,但会浪费一半内存。
现代虚拟机常用此算法来回收新生代,同时也可以调整Eden和survivor的比例,HotSpot默认是8:1。
(3)标记-整理算法,复制算法在对象存活率较高的情况下效率不好,因为需要很多复制的操作,并且存在空间浪费。标记-整理算法是在清理阶段让存活对象都向一端移动,然后清理掉边界外的空间,这样就能获得完整的空闲空间。
(4)分代收集,现代虚拟机都采用“分代收集”算法,它的思想是更加对象存活的周期,将内存化为新生代和老年代。在新生代中,大部分对象“朝生夕死”,比较适合采用复制算法,而老年代的对象存货率较高,一般采用 标记-清除 或 标记-整理算法。
CMS原理:Concurrent Mark Sweep,基于标记-清除算法的收集器,以获取最短回收停顿时间为目标。收集主要包括四个阶段:
(1)初始标记,仅标记root能直接关联的对象,需要停机,但速度很快。
(2)并发标记,标记存活的对象,可以与用户线程一起工作。
(3)重新标记,修正并发标记期间因用户程序继续运作导致标记变动的记录,需要停机。
(4)并发清除,清理对象,可以与用户线程一起工作。
优点是能进行并发收集、低停顿。但有三个缺点:
(1)对CPU资源敏感
(2)无法收集浮动垃圾,即在并发清理阶段产生的垃圾,可能出现“Concurrent Mode Failure”失败而导致一次Full GC。
(3)基于标记-清除算法,会产生大量空间碎片,为了解决这个问题,CMS收集器提供了一个参数,用于在CMS收集器不得不尽兴Full GC时开启内存碎片整理。
G1原理:G1搜集器将Java堆化为多个大小相等的独立区域Region,新生代与老生代不再是物理隔离的,他们都是一部分Region的集合。G1收集器也不再是每次对整个内存空间进行垃圾收集,而是追踪各个Region的垃圾堆积的价值,每次优先收集价值最大的。G1收集器的运作可以分为4个步骤:
(1)初始标记,仅标记root能直接关联的对象,需要停机,但速度很快。
(2)并发标记,标记存活的对象,可以与用户线程一起工作。
(3)最终标记,修正并发标记期间因用户程序继续运作导致标记变动的记录,需要停机。因为仅需要回收一部分Region,时间可控。
(4)筛选回收,将Region的回收价值和成本进行排序,根据用户期望的GC停顿时间指定回收计划,需要停机。
表面上看与CMS非常相似,但G1有3个很大的优点:
(1)“化整为零”,G1每次仅回收部分Region,可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。
(2)Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用使用Remembered Set 记录,扫描存活对象时可以使用它,这样扫描的效率有所提升。
(3)G1是基于标记-整理算法实现的,这就意味着清理过程中不会产生内存空间碎片,分配大对象就不会因为找不到可用空间触发Full GC,有利于程序长时间运行。
参考:《深入理解JVM》第3章
http://www.cnblogs.com/hunrry/p/9210022.html

3 类加载器、双亲委派模型,类加载的过程
ans: Java的类加载器用于实现类的加载动作,但加载并不是由一个加载器完成的,Java有一套加载器的双亲委派模型。
类加载器可以为分3类:
(1)启动类加载器,它是虚拟机自身的一部分,负责装载Java核心API的class文件,<JAVA_HONE>\lib 路径下
(2)扩展类加载器,负责加载<JAVA_HONE>\lib\ext 路径下的class文件
(3)应用程序加载器,一般也称作系统类加载器,负责加载用户路径上指定的class文件。
开发人员可以直接使用扩展类加载器和应用程序加载器。如果有需要,还可以加入自定义的加载器。
所有这些类加载器被连接在一个双亲-孩子的关系链中,这个链条的顶点是启动类加载器,末端是应用程序加载器(或自定义类加载器)。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先将这个请求委派给父类加载器,每个层次的类加载器都是这样工作,因此这个请求最终会被传递给启动类加载器。只有当父加载器反馈自己无法完成请求时(在它的搜索范围内没有找到所需的类),自加载器才会自己尝试加载这个类。
使用双亲委派模型的一个好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Integer 这个Java核心API类一定会被启动类加载器加载,这样就可以防止可信任的类不会被不可靠代码被有意或无意的替换。
参考:《深入理解JVM》7章
《深入JVM-第2版》3.3

4 强引用、软引用、弱引用、虚引用
ans: (1)强引用:是指在程序中普遍存在的,显示的引用,Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。
(2)软引用:用来描述一些还有用但非必需的对象,内存溢出之前进行回收。主要用于实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。Java提供了SoftReference类实现软引用。
(3)弱引用:被若引用关联的对象只能生存到下一次垃圾回收之前,比软引用更弱一些。可以用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,或已经被回收。Java提供了WeakReference实现弱引用。
(4)虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也不能通过虚引用取得对象实例,使用它的唯一目的是检测对象是否已经从内存中删除。Java提供了PhantomReference实现虚引用。
参考:《深入理解JVM》3.2.3
https://www.cnblogs.com/yw-ah/p/5830458.html

5 Java 内存模型 JMM
ans: JMM,Java Memory Model,是Java设计用于屏蔽硬件和操作系统差异,使Java程序在各种平台下都能达到一致的内存访问效果的内存模型。
(1)内存空间定义。Java内存模型将内存空间分为主内存和工作内存,主内存主要用于存储各种对象,工作内存是每个线程工作时使用,存储所有线程工作时用到的变量的主内存的副本。线程使用这些变量时,必须使用指令将它们从主内存加载到工作内存,线程修改了工作内存中的变量后也要将他们写回主内存。
(2)主内存与工作内存的交互。JMM定义了8种指令用于实现主内存与工作内存之间的交互:lock&unlock, read->load>use, assign->store->write。例如,如果要将一个变量从主内存复制到工作内存,就要顺序执行store->write,但并不需要连续执行,也就是说中间还可以插入其他指令。
(3)volatile 的实现。https://www.jianshu.com/p/436d332c1bf5 (4 volatile 实现原理)
(4)JMM 还提供方法来保证 原子性、可见行、有序性。
原子性:基本数据类型的访问读写是具备原子性的,如果需要一个更大范围的原子性保证,可以使用synchronized关键字,反映到字节码就是 monitorenter & monitorexit 指令。
可见行:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,volatitle、synchronized(对一个变量执行unlock之前,必须将其写回主内存)、final (一旦初始化则不能修改)都能够实现这一点。
有序性:Java程序中天然的有序性表现为 “从线程本地来看,所有操作都是有序的;从一个线程观察另一个线程观察,所有操作都是无序的” 后半句指的是“指令重排序”(多条指令不按程序顺序分送给多个电路处理单元,但保证结果与顺序执行相同)和“工作内存与主内存同步延迟”(由于更新延迟,导致不能及时看到最新的数值)。volatitle、synchronized能够保证线程之间操作的有序性。
(5)先行发生原则。除了volatitle、synchronized 这两种同步手段,JMM还有一些“天然的”先行发生原则,无需任何同步就可以直接使用。
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C, 那么操作A先行发生于操作C
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行完成)先行发生于它的finalize()方法的开始
线程中断规则(Thread Interruption Rule):对线程中断方法interrupt()的调用,先行发生于被中断线程的代码检测到中断事件的发生。
线程终止规则(thread Termination Rule):线程的所有操作都先行发生于对该线程的终止检测
线程启动规则(Thread Start Rule):Thread 的 start() 方法先行发生于此线程的每一个动作
volatile变量规则(Volatile Variable Rule):对一个volatile的写操作,先于后面对它的所有读操作
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作
程序次序规则(Program Order Rule):在一个线程中,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(Java编译器可能会进行重排指令优化,但保证输出结果不变)
参考:《深入理解JVM》12.3

6 降低GC的方法
ans:
code 时的技巧:

JVM 配置:观察GC日志

上一篇下一篇

猜你喜欢

热点阅读