标题

读书笔记

前言

个人读书笔记,希望能给大家带来帮助,面试之前再看一遍。

1、jre和jdk的关系,jdk包含jre

在这里插入图片描述

  • Java SE:支持桌面级应用
  • Java EE:支持使用多层架构的企业应用
  • Java ME:移动端

2、Java运行时数据区域

在这里插入图片描述
https://blog.csdn.net/airjordon/article/details/72867397

程序计数器:

较小的内存空间,概念模型中,可以看作当前线程所执行的字节码行号指示器,实际会有高效方式,**字节码解释器通过程序计数器的值选择下一跳需要执行的字节码指令。**在多线程中,每个线程都需要一个独立的程序计数器,各线程中的程序计数器不影响,独立存储,这类内存区域被称为“线程私有”的内存。native方法https://www.jianshu.com/p/22517a150fe5

native方法就是用java调用其他语言写的方法,java方法就是指用java写的方法,并且这个区域没有OutOfMemoryError情况,因为在程序调用中,只是改变其中的指,并不需要更大的空间。

java虚拟机栈:

  • 线程私有,生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法执行的时候会创建一个栈帧,用来存储局部变量表、操作数帧、动态链接、方法出口等信息,每一个方法调用到执行完成的过程,对于虚拟机栈入栈出栈过程。

书上没有得

1.局部变量表:用于存放方法参数和内部定义的局部变量。局部变量表的容量是以变量槽(slot)为最小单位的,32为虚拟机中一个Slot可以存放32位以内的数据类型。

returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。

虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。

Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
https://www.cnblogs.com/Codenewbie/p/6184898.html

2.操作数栈:

Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。

操作数栈也常被称为操作栈。

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

    1. begin 
    2. iload_0  // push the int in local variable 0 onto the stack 
    3. iload_1  // push the int in local variable 1 onto the stack 
    4. iadd    // pop two ints, add them, push result 
    5. istore_2  // pop int, store into local variable 2 
    6. end

3.动态链接:Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

采用了运行时动态链接,相当于把操作系统中的把逻地址接转化物理地址。(就我理解)

4/方法出口,自然就是一个方法结束的,有正常完成出口和异常完成出口,一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

本地方法栈:

与虚拟机栈发挥作用相似,虚拟机栈位虚拟机执行java方法服务,本地方法栈位虚拟机用到的native方法服务,Sun Hot Spot虚拟机将本地方法栈和虚拟机栈合二为一,和虚拟机栈一样本地方法栈区也会抛出栈溢出和内存泄漏异常

java堆

java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,存放所有的对象实例,这里说的对象实例就是new出来的东西,引用就是Student a; 类似a这样的叫引用。本来对象实例都应该在这里分配内存,但是由于JIT编译器的发展和逃逸分析技术优化技术会导致一些微妙的变化,导致不那么绝对了。

java堆是辣鸡收集器管理的主要区域,很多时候被称为“GC堆”,细分可分为新生代和老生代,逻辑连续即可。内存无法完成实例分配,并且堆无法再扩展的时候,就会抛出内存泄漏异常即OutOfMenoryError

方法区:

和java堆一样是各个线程共享内存区域的地方,保存的是虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据,也叫非堆,在HotSpot虚拟机开发的人把这个区域称为永久代,有放弃永久代改为Native Memory来实现方法去的规划,目前1.7中,已经把放在永久代的字符串常量池移出了。(这个位置会考一个问题,要理解1.7中移除常量池又放到了哪里?)

虽然GC回收在这个区域比较少出现,到那时并不是永久存在的,这个区域的内存回收主要是针对常量池的回收和堆类型的卸载

当方法区无法满足内存分配需求的时候,将抛出OutOfMemoryError异常。

运行时常量池:

运行时常量池时方法区的一部分,并不是内存结构中的特定一部分,是被包含,让你介绍JVM内存结构有什么东西,不要说错了。Class文件中有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池。常量池用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中
运行时常量池相对于Class文件常量池的另一个特征就是具备动态性,运行期间也可以将新的常量放入池中,利用的最多的是String的intern()方法。

会抛出OutOfMemoryError异常

直接内存:

不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,频繁使用,可能导致OutOfMemoryError异常出现。
1.4加入了NIO,采用通道与缓冲区的I/O方式,他用Native函数库直接分配堆外内存,通过java堆中的DirectByteBuffer对象作为内存引用进行操作,避免了在java堆和Native堆中来回复制数据。

3、new指令过程(创建对象)

(普通java对象,不包括数组和Class对象)

  • 先去检查这个指令的参数是否能再常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载解析和初始化,没有就进行类加载过程。

  • 接下来就是为新生对象分配内容,假设java堆中内存规整就,就把用过内存和空闲内存分开放,中间放一个指针作为分界点的指示器,分配内存就是将指针移动与对象大小相等的距离,这种分配方式叫做指针碰撞

  • 如果已经使用的内存和空闲内存相互交错。虚拟机需要维护一个列表记录哪些内存可用,再分配的时候找一块足够大的空间划分给对象实例,这种分配方法叫做空闲表法

  • java堆是否规整的看GC是否带有压缩整理功能,Serial(see)、ParNew等有Compact(整理)过程的收集器,系统采用分配算法就是指针碰撞CMS这种就用的是空闲表法

  • 对象头的设置,后面再说

  • 到这一步,一个新的对象已经创建了,但是还没有调用init方法,所有方法字段都还为0,执行完new指令还会执行init方法,把对象按照程序员的意愿初始化,才算创建了一个可用的对象。

    至于并发虚拟机采用的CAS配和失败重试的方式保证更新操作的原子性,另一种是给每个线程都在堆中预先分配一小块内存,这个区域叫做TLAB。

4.对象的内存布局

对象在内存中布局分为三块区域:对象头,实例数据和对齐填充

4.1对象头

包含两部分信息,第一部分:

用于存储自身运行时数据,hashcode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据被称为Mark Wrod,例如32位虚拟机中,32bit中,25bit用以存储对象哈希码,4bit存储对象分代年龄、2bit用于存储标志位,1bit固定为0

第二部分是类型指针,即对象指向类元数据的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例。如果对象是一个java数组,那么在对象头重还必须有一块用于记录数组长度的数据。

自然是无法从数组元数据确定数组的大小,才会在头重记录数组长度。

元数据是什么?

任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元
数据指用来描述一个文件的特征的系统数据,诸如访问权限、文件拥有者以及文件数据
块的分布信息(inode…)等等。

4.2实例数据

对象真正存储的有效信息,相同宽度的字段会被分配到一起,父类定义的变量会出现在子类之前,如果CompactFields参数为true,子类中较窄的变量也可能会插入到父类的空隙中。

4.3对齐填充

不是必然存在,没有特别含义,仅起着占位符的作用,由于HotSpot VM自动内存管理系统要求对象的大小必须是八字节的整数倍,而对象头部正好是8字节的倍数(1到2倍),因此对象实例数据部分没有对齐时,就通过对齐补充来补全

5、虚拟机命令

5.1本地线程分配缓存

把内存分配动作按照线程划分在不同空间之中进程,每个线程在java堆中预先分配一小块内存,称为本地线程分配缓(TLAB),只有TLAB用完并分配新的TLAB的时候,才需要同步锁定

-XX:+/-UseTLAB

使用TLAB可以提高创建对象的速度,这篇文章写的很清楚

https://blog.csdn.net/nangeali/article/details/81866132

5.2设置常量池的大小

这个在1.7的时候已经被移动到了堆区,1.8去了元空间在本地内存

-XX:PermSize=20M -XX:MaxPermSize=20

6、对象访问定位

主流访问有两种方式:使用句柄和直接指针两种方法,reference数据就是在java栈中,一个指向对象的引用。

6.1、句柄访问

在这里插入图片描述
Java堆中会划出一块内存为作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

6.2直接指针访问

在这里插入图片描述

java堆中存放到对象类型数据的指针何对象实例数据

使用句柄的好处:稳定的句柄地址,在对象被移动的时候只会改变句柄中的实例数据指针,而reference本身不用移动。

使用直接指针访问方式最大的好处就是速度更快,节省了一次指针定位的时间开销,由于对象访问十分频繁,所以可以诀曰非常可观的执行成本,HotSpot使用的是直接指针访问

7辣鸡回收算法GC(概念算法)

7.1概念

程序计数器、虚拟机栈和本地方法栈随着线程生随着线程灭

Java堆和方法区不一样,这部分内存分配是动态的,只有处于运行期间时才知道会创建哪些对象。

7.2如何判断对象是否已经死亡

7.21引用计数法

给对象添加一个引用计数器,被引用一次就加一,引用失效的时候,计数器就减一,如果为0就是不可能再被使用了。但是主流Java虚拟机里面没有选用这个来管理内存,主要原因时它很难解决对象之间互相循环引用:

objA.instance = objB

ObjB.instance = ObjA

虽然不可能再被访问,但是相互引用导致引用计数器不为0

7.22可达性分析算法

在主流中这个用的比较多,通过可达性分析来检测对象是否存活,这个算法的思路是,通过一系列叫做GC Roots的对象作为起始点,当GC Roots到这个对象不可达的时候,证明对象不可用。

Java中可以作为GC Roots的对象包括下面几种:

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

同时在回收内存的时候,需要两次标记过程,如果对象在GC Roots相连接的引用链,会被标记一次,但是如果这个时候对象被引用了,就不进行回收,如果对象还是不可以到达,就会执行回收。

7.3引用的分类

1.2之后堆引用概念进行了扩充

强引用>软引用>弱引用>虚引用

  • 强引用:类似Object obj = new Object();,只要这个引用还存在就不会回收掉被引用的对象

  • 软引用用来描述还有用,但是非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入第二次回收,1.2之后用SoftReference类实现软引用

  • 弱引用代表不是必需的对象,它的强度比软引用弱,只能生存到下一次垃圾收集发生之前,1.2之后提供了WeakReference类实现。

  • 虚引用被称为幽灵引用,他是最弱的一种引用关系,一个对象有没有虚引用不会对其生存时间产生影响,也无法通过虚引用来取得一个对象,这个唯一目的就是在这个对象被收集器回收的时候收到一个系统通知,PhantomReference类

7.4永久代中的垃圾回收

(永久代在1.8已经被删除了,而且目前EE中用大部分还是1.8,类似于阿里巴巴就自己写了DragonWell JDK,基于java8和java11),所以这一部分内容看看就好了。

永久代的垃圾收集主要回收两部分,废弃常量和无用的类,回收废弃常量和回收java堆中的对象非常类似:

以常量池的字面量回收举例:

一个字符串“abc”进入了常量池,但是没有引用这个字面量,如果这个时候发生内存回收,如果必要,就会被清出常量池。常量池中的其他类、接口、方法、字段的符号引用于此类似。

如何判断一个常量是废弃常量:

  • 该类所有的实例都已经被回收了,也就是java堆中不存在该类的任何实例

  • 加载该类的ClassLoader已经被回收了

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

    这里说明是可以回收,不代表满足上面三个条件必定回收

    可以通过堆虚拟机

7.5垃圾回收算法

7.51标记清除算法(新生代)

算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象,是前面说的两次标记,这是最基础的收集算法,后续算法是基于这种思想,并对其不足进行改进的得到的。

两个不足:

  • 标记和清除两个过程效率不高

  • 另一个就是空间问题,由于回收之后会产生大量不连续内存碎片,空间碎片太多可能会导致以后在程序运行的时候,分配大对象时,引发另一次垃圾收集动作。

7.52复制算法(新生代算法)

为了解决效率问题,一种称为复制的收集算法出现,将内存划为为两半,每次只用一般,然后这边用完了,就将还存活的复制到另外一块,然后清除之前的整个半区,但是将空间缩小了,代价过高.

目前商业虚拟机都采用这中算法来回收新生代,不是1:1划分,将内存分为较大的Eden和Survivor空间,每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor还存活的复制到另外一块Survivor空间上Eden:Survivor的大小比例是8:1每次新生代可用空间为整个容量的90%,只有10%的空间会被浪费。

但是如果幸存下来的内存容量超过新生代的10%,老年代就要进行分配担保

7.53标记-整理算法(老年代算法)

由于复制操作效率变低,50%的内存不能用,老年代一般不采用复制算法。

先标记,然后让标记的对象向以端移动,然后清除掉端边界以外的内存。

7.54分代收集算法(核心算法)

根据对象存活周期的不太能,将内存划分为几块,一般是分成新生代和老年代,然后根据各代采用不同的收集算法。

8HotSpot垃圾回收实际中的算法

8.1枚举根节点(使用OopMap来解决GC Roots查找时间长)

可作为GC Roots的结点在全局性引用(常量或类静态属性)与执行上下文(例如栈帧的本地变量表)中。

从GC找引用链这个操作太消耗时间了,而且还要GC停顿,这个分析工作需要一个能确保一致性的快照汇总进行,不能出现分析过程中对象引用关系还在发生变化的情况,不然分析结果保证性无法保证。GC时必须停顿所有Java执行线程

CMS收集器几乎不会停顿,但是枚举根节点时也是必须要停顿的

使用OopMap的数据结构来辨析哪些地方存放对象引用

8.2安全点(解决OopMap占用空间多)

安全点的概念就是前面说到使用OopMap来帮助快速且准确地完成GC Roots枚举,但是每一条指令都生成对应地OopMap需要大量额外空间,在特定地位置记录这些信息,这些位置称为安全点。程序不能在任何时候停下来GC,只能到达安全点才能暂停

如何让线程跑到安全点上再停顿,这里有两种方案:

  • 抢先式中断:不需要代码主动配合,就是先暂停然后发现不在安全点上,就恢复进程,然后让它跑到安全点上,没有虚拟机用这种方式暂停线程响应GC事件
  • 主动式中断:GC需要中断地时候,不直接对进程操作,简单地设置一个标志,各进程执行地时候区轮询这个标志,发现为真就直接挂起,轮询地地方和安全点式重合地。

8.3安全区域(解决程序不执行下,走不到安全点中断)

程序不执行没有分配CPU事件,典型地例子就是线程处于Sleep或Blocked状态,只时候线程无法响应JVM地中断请求,走到安全点中断挂起,对于这种情况需要安全区域来解决。

安全区域中是指在一段代码片段,引用关系不会发生变化,在区域地任意地方都可以GC,可以把安全区域看作安全点地扩展。

离开安全区域需要接收到可以安全离开地信号,也就是完成了GC动作。

9、垃圾收集器

在这里插入图片描述
垃圾收集器式是内存回收的具体实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s827DHim-1583159923368)(C:\Users\李正阳\AppData\Roaming\Typora\typora-user-images\image-20200124231352651.png)]

连线代表可以搭配使用。

9.1Serial & Serial Old(新生代收集器英文意思连续的)

最基本最悠久的收集器,单线程收集器,必须暂停其他工作线程,直到收集结束。

新生代:Serial:采取复制算法

老年代:Serial Old:采取标记整理算法

客户端模式下默认使用的垃圾收集器

使用一小时暂停五分钟,没有线程交互的开销,简单高效,收集几十MB到一两百MB的新生代,停顿大概在几十MS和一百多MS以内。

9.2 ParNew收集器(和CMS、Serial搭配,新生代)

ParNew是Serial收集器的多线程,除了使用多线程进程GC以外,也可以使用Serial的所有参数,收集算法、停止、对象分配规则、回收策略和Serial收集器完全一样。

ParNew和SerialOld收集器,新生代采取复制算法,老年代依旧是标记整理算法。

是Server模式下虚拟机首选的新生代收集器,除了性能最重要的原因就是可以和CMS配合工作,这款收集器就是HotSpot中第一款真正意义上的并发收集器,第一次实现了用户进程和工作进程同时工作。

这里面的并发和并行的解释:

  • 并行:指多条垃圾收集器线程并行,但是此时用户进程处于等待状态

  • 并发:用户进程和垃圾回收进程同时执行(单不一定是并行,可能会交替执行),用户程序在运行,垃圾收集程序运行在另一个CPU上。

9.3 Parallel Scavenge 收集器(无法和CMS搭配,新生代,吞吐量优先收集器)

使用复制算法的收集器,也是并行多线程收集器,ParNew是并发收集器,拥有自适应调节策略,ParNew没有。

这个收集器关注点是达到一个可控制的吞吐量吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,这个收集器主要是为了高效率利用CPU时间,尽快完成程序运算任务,主要用于后台运行而不需要太多交互任务。

最大垃圾收集停顿时间:-XX:MaxGCPauseMillis参数以及直接设置吞吐量的 -XX:GCTimeRatio参数。

-XX:MaxGCPauseMillis设计的过小可能会导致收集过于频繁。

-XX:GCTimeRatio默认值为99,代表允许最大1%的垃圾回收时间

-XX:UseAdaptiveSizePolicy开关参数,打开就不需要指定

新生代的大小-Xmn

新生代中Eden和Survivor的比例-XX:SurvivorRatio

晋升老年代对象年龄-XX:PretenureSizeThreshold

会根据当前系统的运行情况收集性能监控信息,动态调整参数,这种调节方式称为GC自适应调节策略。

9.4 Serial Old收集器(Serial收集器的老年版)

标记整理算法,给Client模式下的虚拟机使用,

两种用户:

  • 作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure 时使用/

  • 与Parallel Scavenge收集器搭配使用

9.5 Parallel Old(Parallel Scavenge收集器的老年版)

使用多线程和标记整理算法,JDK1.6才开始提供的,之前Parallel Scavenge收集器只能和Serial Old搭配,这种搭配被Serial Old拖累无法充分利用服务器上的多个CPU。所有还不如ParNew + CMS。

直到Parallel Old收集器出现后,在注重吞吐量及CPU资源敏感的场合,“吞吐量优先”收集器可以搭配Parllel Scavenge + Parallel Old收集器搭配。

9.6 CMS收集器(老年代)

CMS收集器是一种获取最短时间回收停顿时间为目标的收集器。主要用于Java应用几种在互联网站或者B/S系统的服务端。这类应用重视响应速度,系统系统停顿时间最短,给用户带来较好的体验。

标记清除算法实现。

过程:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

初始标记、重新标记需要停止系统,初始标记标记GC Roots能直接关联到的对象,速度很快。

并发标记就是 GC Roots Tracing的过程

重新标记阶段是为了修正并发标记期间因为用户程序继续运作导致标记产生变动的那一部分标记记录,停顿时间较长,但是比并发标记短。

并发标记和并发清除过程收集器线程都可以和用户线程一起工作。

所以从总体说,CMS收集器内存回收过程适合用户线程一起并发。

三个缺点

  • 在并发阶段,不会导致用户线程停顿,由于占用了一部分线程,会导致应用程序变慢,总吞吐量会降低,CMS默认启动的回收线程是(CPU数量 + 3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随CPU数量怎加而下降,不足4个,CMS对用户程序影响就很大,因为CPU负载本来就大,分出一半去执行收集器,为了应付这种情况,提供了一种叫做增量式并发收集器的CMS变种i -CMS让GC和用户进程交替运行,对用于程序影响不大。i - CMS以及被淘汰。

  • 无法处理浮动垃圾,可能出现”Concurrent Mode Failure“失败导致另一次Full GC的产生,。由于清理阶段用户线程还在运行,垃圾还在产生,而且本次GC无法处理,只能留给下一次GC,这一部分垃圾被称为浮动垃圾,优化见优化CMS优化建议1.

  • 基于标记清除算法实现,会有大量空间碎片产生。大对象分配很麻烦,所以可以优化,优化见CMS优化建议2

9.7 G1收集器

G1未来可以打算替换CMS收集器,但是测试了多年,面向服务端的应用垃圾收集器。

特点:

  • 并行和并发,其他收集器原本需要停顿java线程执行的GC,G1通过并发使java程序继续执行。
  • 分代搜集,虽然G1可以独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和以及存活了一段时间,获取更好的收集效果。
  • 空间整合:和CMS的标记清除算法不同,从整体上看是基于标记整理,从局部上来看是基于复制算法,不会产生内存空间碎片。
  • 可预测停顿,G1相对于CMS另一个优势,可建立可预测停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时Java垃圾收集器的特征了。

G1不在是老年代和新生代了,而是将堆划分为多个大小相等的独立区域,保留了老年代新生代的概念,但是不再是物理隔离,都是一部分区域(不需要连续)的集合

G1收集器之所以可以建立预测停顿时间模型,是因为它可以有计划的进行全区域垃圾收集,G1会跟踪每个区域里面垃圾堆积的价值大小(所获得空间以及回收所需要的时间),后台会维护一个优先列表,每次更具允许的收集时间,先回收价值最大的区域,确保了有限时间获得尽可能高的收集效率。

为了不对全堆扫描,使用了Remembered Set来避免全堆扫描,G1中每个Region区域都有一个Rememvered Set,虚拟机发现程序堆Reference类型数据写操作,会查看是否老年代对象引用了新生代对象,然后通过CardTable把引用信息记录到对象所需区域的Remembered Set中。

步骤:

  • 初始标记:标记可以直接关联到的对象,停顿
  • 并发标记:可达性分析,并发
  • 最终标记:修正并发中用户程序继续允许导致标记发生变动的那一部分标记需要停顿线程,但是可以并行执行。
  • 筛选回收:对各个区域排序然后选择回收。并发

初始标记和CMS一样

10 GC日记阅读(略,了解)

11 内存发配规则(使用Serial 和 Serial Old)

大多数情况,对象在新生代Eden中分配,出现空间不足,发起一次Minor GC

例子:

分配3个2Mb的对象,一个4MB的对象,新生代为10MB,老年代为10MB,然后分配,并且Eden和Survivor区为8:1,到4MB对象必定无法分配,发生GC,但是无法回收,所以只能老年代担保,然后将6MB放入老年代,Eden中有4MB

11.1 长期存活对象进入老年代。

既然虚拟机采用了分代收集的思想来管理内存,为了实现这点给每个对象定义了一个对象年龄计数器,对象在Eden初始经过第一次Minor GC后仍然存活就能被Survivor容纳,对象年龄设为1,对象每在Survivor中每次熬过一次就增加一岁,默认为15岁就进入老年代。

11.2 动态对象年龄判断

一般没到达MaxTenuringThreshold,也能进入老年代,当Survivor相同年龄所有对象大小综合大于Survivor空间的一半,年龄大于或等于对象就可以直接进入老年代。

11.3 空间分配担保

在发生Minor GC之前,虚拟机先检查老年代可用的连续空间是否大于新生代所有对象总空间,成立 Minor GC可以确保是安全的。不成立就查看HandlePromotionFailure设置查看是否允许担保失败。允许就检查老年代最大可用连续空间是否大于晋升到老年代对象的平均大小,大于就尝试进行Minor GC,小于或者设置不允许冒险就进行 Full GC。

冒险:新生代使用复制算法,如果Eden中对象太大无法进入Survivor,需要老年代进行担保,但是无法确定老年代是否有空间,就需要晋升到老年代平均对象容量作为经验值,与老年代比较剩余空间,决定是否进行Full GC,让老年代腾出空间。

担保失败之后,那就重新发起一次Full GC,一般会打开冒险,避免Full GC频繁。

1.6之后,不会再影响老年代分配担保,规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小就进行Minor GC ,否则就Full GC

12 类加载机制

类被加载到虚拟机内存中开始到被卸载,有七个阶段:

加载、(验证、准备、解析(连接))、初始化、使用、卸载

其中验证、准备、解析被称为连接

加载、验证、准备、初始化、卸载五个阶段顺序是确定的,类加载的过程必须按照这种顺序开始。解析阶段不一定,在某些情况下可以在初始化阶段之后再开始。这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行中调用、激活另一个阶段。

加载是虚拟机来把控的,虚拟机规范规定了5种情况必须要马上对类进行初始化(加载、验证、准备在初始化以前)

  • 遇见new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,生成这4条指令最常见的常见是:使用new关键词实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行放射调用的时候,如果类没有初始化,需要先触发初始化。
  • 当初始化一个类的时候,发现其父类还没有进行初始化,则需要先触发父类的初始化。
  • 当虚拟机启动时候,用户需要指定一个要执行的主类,虚拟机会优先初始化。
  • 当使用JDK1.7动态语言支持时,如果一个java.lang.MethodHandle实例解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄、并且这个方法句柄所对应的类没有初始化,则会先初始化。

只有这5种情况才会触发初始化,所有引用类的方式都不会触发初始化,称为被动引用,有三种引用方式。

  • 通过子类引用父类的静态字段,不会导致子类初始化。非主动使用类字段-XX:+TraceClassLoading参数会导致子类的加载
  • 通过数组定义来引用类,不会触发此类的初始化,因为创建的时候,由虚拟机生成了一个直接继承Object的子类(只能用length属性和clone()方法)都实现在这个类里面。java对数组的访问比C/C++相对安全就是因为这个类封装了数组元素的访问方法。
  • 常量在编译阶段会存入调用类的常量池,本质上没有直接引用定义常量的类,因此不会触发定义常量的类的初始化。

接口只会符合只有情况的第三种。

12.1 类加载的过程

加载是类加载过程的一个阶段,加载中需要完成三件事情

  • 通过一个类全限定名来获取定义此类的二进制字节流

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

    数组类情况不同,数组类本身不通过类加载器创建,由java虚拟机直接创建。但是数组类与类加载器仍然有很密切的关系,因为数组的元素类型最终要靠加载器去创建,数组需要遵守的规则:

    • 如果数组的组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间被标识
    • 如果数组类型不是引用类型,就会把数组C标记为引导类加载器关联。
    • 数组类的可见性与它的组件类型的可见性一致,如果不是引用类型,那数组的可见性默认为public。

    加载完成后,虚拟机外部二进制字节流按照虚拟机格式存储在方法区,也就是1.8中的元空间,然后再内存中实例化一个java.lang.Class类对象(HotSpot虚拟机放在方法区(元空间)。

    加载阶段和连接阶段的部分内容(字节码文件格式验证)交叉运行的。

12.2验证过程

验证是连接阶段的第一步,目的时为了确保Class文件字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

验证阶段的工作量占用了虚拟机的类加载子系统中很大一部分。

有四个阶段:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证
12.3准备

正式为类变量分配内存并设置类变量初始值的阶段

例如 public static int value = 123;

这个值在这个阶段是0,并不会执行任何java方法,只有到初始化阶段才会执行。

public static final int value = 123;

编译时javac会为value生成ConstantVale,再准备阶段就会赋值。

12.4解析

解析阶段是虚拟机将常量池内的符号引用转为直接引用的过程。

符号引用:用一组符号来描述引用的目标,只要能无歧义地定位到目标即可,符号引用和内存布局无关,引用的目标并不一定加载到内存中。

直接引用:直接执行目标的指针,相对偏移量或者是一个能直接定位到目标的句柄。与内存布局相关。同一个符号引用再不同虚拟机翻译出来一般不会相同,直接引用代表目标一定在内存。

虚拟机规范之中未规定解析阶段发生的具体时间,执行16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

对同一个符号引用进行多次解析请求是很常见的事件,虚拟机可以对第一次解析结果进行缓存,在运行时常量池中记录直接引用,并把常量标识未已解析状态,从而避免解析动作重复进行。无论执行了多少次解析,如果一个符号引用直接被解析了,那么后续引用请求就应当一直成功,如果第一次失败了,那么其他解析应该收到相同异常。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等棋类符号引用进行、分别对应于常量池七种常量类型

前面四种引用的解析过程,后面四种与1.7的动态语言相关,故先介绍前四种:

类或接口的解析:当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成解析过程需要以下3个步骤:

  • **如果C不是一个数组类型,那虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C。**在加载过程中,由于元数据验证、字节码验证的需要,还可能触发其他相关类的加载动作。
  • 如果C是一个数组类型,并且数组的元素类型为对象,虚拟机生成一个代表此数组维度和元素的数组对象。
  • 如果没有异常,那么C在虚拟机中实际上已经称为一个有效的类或接口了,但在解析完成之前要进行引用验证,确定D是否对C的访问权限,不具备访问权限要抛出java.lang.lllegalAccessError

字段解析

  • 如果C本身包含了简单名称和字段描述符都与目标相匹配字段,返回这个字段的直接引用,查找结束
  • 否则如果在C中实现了接口,会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标匹配,则返回直接引用,查找结束。
  • 否则,如果C不是Object,将会按照继承关系从下往上递归搜索其父类,如果匹配,则返回这个字段的直接引用,查找结束。
  • 否则查找失败,抛出异常。

查找成功了返回引用会对字段进行权限验证,发现不具备访问就抛出异常。

如果子类在类文件中定义父类的同名字段,就会报错。

类方法解析:

和字段解析一样,也需要解析出类方法表的class_index项中的索引的方法所属类或接口的符号引用。解析成功就用C表示这个类,接下来会进行类方法搜索

  • 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法中发现class——index中索引的C是个接口,就直接泡池异常。
  • 在C中查找是否有简单名称和操作符都与目标匹配的方法,如果有就直接返回直接引用,查找结束。
  • 否则在C的父接口递归查找,直到Object类位置,有就返回,查找解除
  • 否则在C实现的接口列表和父接口中递归查找,存在就说明C是一个抽象类,查找结束抛出异常。
  • 否则宣布查找失败

接口方法解析

同上

  • 与类方法解析不同,如果在接口方法表中发现class_index中索引C是个类而不是接口就直接抛出异常,查找结束
  • 否则,在接口C查找,有就返回,查找结束
  • 否则,C父接口中递归到Object类,有就返回,没有就查找结束
  • 否则,宣告查找失败,抛出异常。
12.5 初始化

类初始化阶段是类加载过程的最后一步,前面类加载过程中,除了在加载阶段用户应用程序可以通过自定义加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中的定义的java代码。

初始化阶段的是执行类构造器()方法的过程,下面是()方法执行过程中一些可能会影响程序运行的特点和细节。

init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。

  • ()方法是由编译器自动收集类中所有类变量赋值动作和静态语句块(static{})中语句合并产生的。 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

  • ()方法与类构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。所以虚拟机中第一个被执行的()方法的类肯定是Object

  • 由于父类()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

  • ()方法对于类和接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成()方法。但接口与类不同的是执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类初始化的也一样不会执行接口的()。

  • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步。如果执行()方法地那条线程退出之后,其他线程不会再进入这个方法,同一个类加载器下,一个类型只会初始化一次

13 类加载器

通过一个类地全限定名来获取描述此类地二进制字节流这个动作放到了java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要地类。

13.1类与类加载器

比较两个类是否相等,前提:两个类是由同一个类加载器加载地前提下才有意义,否则这两个类来源于同一个Class文件,被同一个虚拟机加载,但是加载他们地加载器不同,这两个类必定不相等。

这里地相等,代表Class对象地equals()方法、isAssignableFrom()方法、isInstance()方法地返回结果、也包括使用instanceof关键字做对象所需关系判断等情况。

这个位置地代码例子需要原书。

13.2 双亲委派模型

只存在两种不同地类加载器,一种是启动类加载器,这个加载器用C++语言实现地,是虚拟机自身地一部分,另一种就是所有其他的类加载器。这些类加载器由java语言实现,独立于虚拟机外部。并且全部继承抽象类java.lang.ClassLoader

系统提供的类加载器:

  • 启动类加载器:这个类将器负责将存放再lib目录中,

  • 扩展类加载器

  • 应用程序类加载器

在这里插入图片描述

13.3 破坏双亲委派模型

三种情况:

  • JDK1.2之前,这个模型在JDK1.2之后才被引入,之前还没有双亲委派模型。

  • 自身缺陷导致的,基础类调用回用户的代码。会通过通过上下文加载器加载,也就是父类叫子类加载器去完成类加载的动作。

  • 程序的动态性追求导致的。代码热替换、模块热部署。OSGI实现模块化热部署。每一个程序模块(Bundle)都有一个自己的类加载器,需要更换一个Bundle时,就把Bundle联通类加载器一起换掉,实现代码的热替代。

    OSGI环境下,双亲委派不是树状结构,而是网状结构

    • 将以java.*开头类委派给父类加载器加载
    • 否则,将委派列表名单内的类委派给父类加载器加载
    • 否则将Import列表委派给Export这个类的Bundle类加载器加载
    • 否则查找当前Bundle的ClassPath,使用自己的类加载器加载.
    • 否则,查找类是否放在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
    • 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
    • 否则类查找失败

    题目集合

    1、JDK1.7以后字符串常量池去哪了?

    java jdk1.7中的常量池确实是移到了堆中,同时在jdk1.8中移除整个永久代(方法区),取而代之的是一个叫元空间(Metaspace)的区域,如果想了。

    用jdk1.6运行后会报错,永久代这个区域内存溢出会报:
    Exception in thread “main” java.lang.OutOfMemoryError:PermGen space的内存溢出异常,表示永久代内存溢出。

    在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。

    https://blog.csdn.net/weixin_35663229/article/details/52796157

    移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
    https://blog.csdn.net/qq_16681169/article/details/70471010

    2、为什么要移除永久代

    • 它的大小是在启动时固定好的——很难验证并进行调优。-XX:MaxPermSize

    • HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,非强类型,难以跟踪调试,需要存储元数据的元数据信息。

    • 简化垃圾回收:对每一个回收集使用专门的元数据迭代器。

    • 可以在GC不进行暂停的情况下并发地释放类数据。

    • 使得原来受限于持久代的一些改进未来有可能实现

      这篇文章写的太好了,推荐阅读

      https://blog.csdn.net/qq_16681169/article/details/70471010

      3.为什么paraller scavenge 不能和cms一起用?

      摘抄:
      Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用。更底层的原因在于原文之中:

原文链接:https://blog.csdn.net/qq_33915826/article/details/79672772

优化建议

1.CMS优化建议

1.1CMS不能当老年代使用满了再进行回收,所以,需要再老年代中预留一些空间。

-XX:CMSInitiatingOccupancyFraction 68 就是当老年代使用了68%的时候再GC,在中老年代增强不快,可以调高这个值得参数,降低回收得次数,提高性能,但是设置过高,会导致Concurrent Mode Failure,然后启动Serial Old收集器,大量Concurrent Mode Failure性能反而降低。

1.2 CMS内存碎片,无法分配大对象问题

-XX:+UseCMSCompactAtFullCollection (默认开启)用于CMS因为大对象要FullGC得时候开启内存碎片得合并整理过程

-XX:CMSF7890 .吗,/9ullGCsBeforeCompaction 这个参数用于设置执行多少次不压缩Full GC之后执行一次压缩得的.

2.基础参数

2.1收集器日志参数

-XX:+PrintGCdetails打印收集器日志参数,发生垃圾回收的时候打印内存回收日志,并且在内存退出的输出当前内存区域分配情况。

2.2 设置Eden中对象晋升老年代的年龄阈值

-XX:MaxTenuringThreshold = 年龄设置

2.3设置老年代允许担保失败(1.6之前)

-XX:+HandlePromotionFailure

允许就检查老年代最大可用连续空间是否大于晋升到老年代对象的平均大小,大于就尝试进行Minor GC,小于或者设置不允许冒险就进行 Full GC。

2.4 命令工具

名称主要作用
jps显示指定系统内所有的HotSpot虚拟机进程
jstat用于收集HotSpot虚拟机各方面的运行数据
jinfo显示虚拟机配置信息
jmap生成虚拟机的内存转储快照(heapdump文件)
jhat用于分析heapdump文件,会建立一个HTTP/HTML服务器,让用户在浏览器上查看分析结果
jstack显示虚拟机线程快照

https://www.cnblogs.com/lizhonghua34/p/7307139.html

2.5 获取java堆的快照

-XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机出现OOM异常出现之后自动生成dump文件。

2.6关闭验证阶段的大部分验证措施加快类加载

-Xverify:none

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐