【烈日炎炎战后端】JAVA虚拟机(3.6万字)

JVM(36098字)

        • 1. 说一下 JVM 的主要组成部分/li>
        • 2. 谈谈对运行时数据区的理解/li>
        • 3. 谈谈对内存泄漏的理解/li>
        • 4. JMM是什么/li>
        • 5. 为什么要学习Jvm/li>
        • 6. 什么是栈帧/li>
        • 7. Stop-The-World
        • 8. 元空间(Metaspace)
        • 【】
          • [1] 什么是类加载器/li>
          • [2] 类加载器的分类有/li>
          • [3] 类加载器的职责有/li>
          • [4] 什么是双亲委派机制/li>
        • 【JVM垃圾回收专题】
          • [1] 垃圾回收的场所及原因/li>
          • [2] 为什么学习GC和如何学习GC/li>
          • [3] JVM如何判定一个对象是否应该被回收/li>
          • [4] 在java中为什么不推荐使用finalize
          • [4] 详细说下四种引用/li>
    • 强调
          • [5] 常用的垃圾收集算法有哪些/li>
          • [4] 分代收集的理论支撑/li>
          • [6] 常用的垃圾收集器(内部使用垃圾收集算法)有哪些/li>
          • [7] 垃圾收集器如何互相配合使用
          • [8] CMS说一下/li>
          • [9] G1 说一下/li>
          • [10] G1和CMS/li>
          • [10] 说下你对垃圾回收策略的理解/垃圾回收时机/li>
          • [11] 分区大小比值及其理论依据
          • [12] 内存分配策略
          • [13] Minor GC 和 Full GC触发条件
        • 【JVM调优】
          • [1] 说下你用过的 JVM 监控工具/li>
          • [2] 如何利用监控工具调优/li>
          • [3] JVM 的一些参数/li>
          • [4] JVM在内存调优方面
          • [5] 怎么做JDK8的内存调优/li>
          • [6] 调优实战

1

VM中的内存主要划分为5个区域,即方法区堆内存虚拟机栈本地方法栈以及程序计数器

  1. 方法区:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等(存储的数据一般持久性比较强)数据。
  2. 堆内存:在虚拟机启动时创建,存放对象实例。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC”堆(Garbage Collected Heap)。从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代。
  3. 虚拟机栈:为虚拟机执行Java方法(也就是字节码)服务。
  4. 本地方法栈:为虚拟机使用到的 Native(调用非java代码的接口)方法服务。
  5. 程序计数器:是当前线程所执行的字节码的行号指示器
  1. 哪些属于线程独占,哪些属于线程共享/strong>

线程共享:方法区,堆内存
线程独占:程序计数器,虚拟机栈以及本地方法栈

  1. 解释下运行时常量池/strong>

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一些信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  1. 堆和栈的区别是什么/strong>

从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据

3. 谈谈对内存泄漏的理解/h4>

在 Java 中,内存泄漏就是存在一些不会再被使用确没有被回收的对象,这些对象有下面两个特点:

  1. 这些对象是可达的,即在有向图中,存在通路可以与其相连;
  2. 这些对象是无用的,即程序以后不会再使用这些对象。

如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。

哪些操作会造成内存泄漏/strong>

内存泄漏,就是不再需要的对象仍然存在内存中,内存泄漏不断堆积的后果就是内存溢出,即内存不够用。

垃圾回收机制会定期扫描对象,如果一个对象没有被其他对象引用,或两个对象互相引用但没有被第三个对象引用,则它们的内存会被回收。

1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄露

2. 全局变量

3. 闭包

4. dom清空或删除时,事件未清除导致的内存泄漏

5. 控制台日志

6. 循环

4. JMM是什么/h4>

:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别()。

其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的来作为内存与处理器之间的缓冲。

将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:。

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rXqeM8Hr-1596595328720)(X:UsersxuAppDataRoamingTyporatypora-user-imagesimage-20200518151220348.png)]

5. 为什么要学习Jvm/h4>
  1. 为了更好的理解java语言
  2. 为了再内存泄漏和溢出时候进行补救

6. 什么是栈帧/h4>

每个方法再虚拟机栈被执行的时候,都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

7. Stop-The-World

一.概述:

java对象内存申请过程:

1.JVM会试图为相关Java对象在Eden中初始化一块内存区域;当Eden空间足够时,内存申请结束。否则到下一步;
2.JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
3.Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
4.当old区空间不够时,JVM会在old区进行major collection;
5.垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”Out of memory错误”;

Stop-The-World:

在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。

不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。

  • Java中一种全局暂停的现象,jvm挂起状态
  • 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
  • 多半由于jvm的GC引起,如:
    1.老年代空间不足。
    2.永生代(jkd7)或者元数据空间(jkd8)不足。
    3.System.gc()方法调用。
    4.CMS GC时出现promotion failed和concurrent mode failure
    5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间
    6.有连续的大对象需要分配
  • 除了GC还有以下原因:
    1.Dump线程–人为因素。
    2.死锁检查。
    3.堆Dump–人为因素。
    Full GC 是清理整个堆空间—包括年轻代和老年代。

GC时为什么会有全局停顿/strong>

类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。

危害

  • 长时间服务停止,没有响应
  • 遇到HA系统,可能引起主备切换,严重危害生产环境。
  • 新生代的gc时间比较短(),危害小。
  • 老年代的gc有时候时间短,但是有时候比较长几秒甚至100秒–几十分钟都有。
  • 堆越大花的时间越长。
    链接:https://www.jianshu.com/p/d686e108d15f

8. 元空间(Metaspace)

java8中移除了永久代,新增了元空间的概念。原来的方法区是逻辑划分中的一个区域,对应hotspot jdk6中的永久代,可以说永久代是方法区在hotspot的一个具体实现,但是从jdk7以后方法区就“四分五裂了”,不再是在单一的一个去区域内进行存储。

java8中继承了一些jdk7中的改变:符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这个影响了String的intern()方法的行为,这里不做intern的详述。

而在java8中移除了永久代,新增了元空间,其实在这两者之间存储的内容几乎没怎么变化,而是在内存限制、垃圾回收等机制上改变较大。元空间的出现就是为了解决突出的类和类加载器元数据过多导致的OOM问题,而从jdk7中开始永久代经过对方法区的分裂后已经几乎只存储类和类加载器的元数据信息了,到了jdk8,元空间中也是存储这些信息,而符号引用、字符串常量等存储位置与jdk7一致,还是“分裂”的方法区。

符号引用没有存在元空间中,而是存在native heap中,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。
链接:https://www.jianshu.com/p/474d98fc4776

【】

[1] 什么是类加载器/h5>

【烈日炎炎战后端】JAVA虚拟机(3.6万字)
[2] 类加载器的分类有/h5>

启动类加载器(Bootstrap ClassLoader):启动类加载器负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的类。

扩展类加载器(ExtClassLoader):扩展类加载器负责加载JDKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。

应用类加载器(AppClassLoader):应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。

[3] 类加载器的职责有/h5>

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

父类委托(防止重复):类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。父类委托机制是为了防止内存中出现多份同样的字节码,保证java程序安全稳定运行。

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

[4] 什么是双亲委派机制/h5>

双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式.那么这种模式有什么作用/p>

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap classLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。


【JVM垃圾回收专题】

  1. 我们首先需要判断垃圾,其中中心思想为判断其是否还有引用。可以使用引用计数法可达性分析
  2. 我们对垃圾进行回收时,需要一些垃圾回收算法进行理论支持。包括:标记-清除复制标记-整理分代收集算法
  3. 如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。见下图,其中CMS和G1重点了解。
  4. 最后我们划分了堆内存(年轻代和老年代)和非堆内存(永久代),进行垃圾分类。还有两种针对不同区域实施垃圾回收策略,追求高效率的回收。
    img
  5. 复制算法(Copying):为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价未免太高了一点。

    img
  6. 分代收集算法:当前的商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法

    为什么在新生代使用使用复制算法,在老年代使用标记整理算法/strong>

    在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

什么是内存碎片何解决/strong>

由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。

[4] 分代收集的理论支撑/h5>

弱分代假说:绝大数对象都是朝生夕灭的;

强分代假说:熬过多次垃圾收集过程的对象越难以消亡。

跨代引用假说:跨代引用相对于同代引用只占少数。

1. 弱分代假说和强分代假说证明了什么/strong>

弱分代假说和强分代假说鉴定了垃圾收集器一致的原则,收集器应该讲java划分为不同区域,然后将回收对象依据年龄(年龄即对象熬过垃圾收集的过程)分配到不同的存储之中存储,使用不同垃圾回收策略。

2. 跨代引用假说解释了什么/strong>

由于对象之间不是孤立的,对象之间会存在跨代引用。所以可以用跨代引用假说证明,存在互相引用关系的两个对象,应该是倾向于同时生存和消亡的。举个例子,如果某新生代的对象存在于老年代难以消亡,那么引用会使新生代的对象存活很久,进而晋升到老年代中,这种跨代引用随即被消除了。

3. 记忆集/strong>

https://segmentfault.com/q/1010000023017473/

依据这条假说,我们不应该为了少量的跨代引用去扫描整个老年代。我们我们可以在新生代中建立一个全局数据结构–记忆集。这个结构把老年代划分为很多小块,并且标记出那一块存在跨代引用。当发生minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入GC roots进行扫描。

[6] 常用的垃圾收集器(内部使用垃圾收集算法)有哪些/h5>

垃圾回收算法是垃圾回收的方法论,垃圾收集器,是内存回收的实践论。link,记忆方法:单线程:serial与serial old,多线程:ParNew与CMS,高吞吐:Parallel Scavenge与Parallel Old,G1

  1. Serial 收集器:新生代与老年代的单线程收集器,标记和清理都是单线程,在垃圾回收时会暂停其他工作线程,直到它收集完,即stop the world。优点是简单粗暴适合单核及少核处理器。
  2. Serial Old 收集器(标记-整理算法)老年代单线程收集器,Serial 收集器的老年代版本。服务端使用时:可以与Parallel Scavenge 收集器配合使用,也可以成为CMS收集器失败后的备选预案。
  3. ParNew 收集器(标记-复制算法)新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。它是激活CMS后默认的新生代收集器。
  4. Parallel Scavenge 收集器(标记-复制算法)并行多线程收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 适合后台应用等对交互相应要求不高的场景。吞吐量= 用户线程时间 / (用户线程时间+GC线程时间),吞吐量适合保证高效率的执行有效工作。
  5. Parallel Old 收集器(标记-整理算法)Parallel Old 收集器的老年代版本,并行收集器,吞吐量优先。
  6. **CMS(Concurrent Mark Sweep)**收集器(标记-清除算法)高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
  7. G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First的由来。
[7] 垃圾收集器如何互相配合使用

image-20200715164625585

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoRDCOOC-1596595328729)(X:UsersxuAppDataRoamingTyporatypora-user-imagesimage-20200715164735100.png)]

[8] CMS说一下/h5>
  • CMS垃圾清除的步骤/li>

link:首先停止线程,标记root根遍历直接对象,然后根据其扫瞄,之后停止线程重新扫描,最后删除

  1. 初始标记(stop the world):使用可达性分析记录下直接与 root 相连的对象,暂停所有的其他线程,速度很快;
  2. 并发标记:同时开启 GC 和用户线程,用 CG root 直接关联对象一个去记录可达对象。但在这个阶段结束,这并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  3. 重新标记(stop the world):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录【这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短】;
  4. 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。
  • CMS 的优缺点/li>

优点:CMS(concurrent low pause collector)并发收集、低停顿;

缺点:对 CPU 资源敏感、无法处理浮动垃圾、它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

并发消耗CPU资源 其中的并发标记和并发清理是工作线程和垃圾回收线程并发工作,这样在需要STW的时间内不会让整个系统不可用。但是在并发标记阶段,需要根据GC Roots标记出大量的存活对象,而在并发清理阶段,则需要将垃圾对象从各种随机内存位置删掉,这两个阶段都非常消耗性能,所以垃圾回收线程会占用一部分的CPU资源,导致系统的执行效率降低。

CMS默认的回收线程数是 (CPU个数+3)/4,当在CPU核数较多的时候,对系统性能的影响并不是特别大。但是如果是CPU核数较少,例如双核的时候,就会占用一个CPU去处理垃圾回收,系统的CPU资源直接降低50%,这就严重影响了效率。

因为现在CPU的核数越来越多,所以这种场景基本不会对系统造成很大的影响,可以忽略不计。

Concurrent

来源:徐明曉

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年7月4日
下一篇 2020年7月4日

相关推荐