垃圾回收器有哪些
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS、G1
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部系统资源,此时应用程序的处理的吞吐量将受到一定影响。
7款经典收集器与垃圾分代之间的关系:
七种收集器之间的组合关系:
注:这个关系不是一成不变的,由于维护和兼容性测试的成本,在 JDK 8 时将 Serial+CMS、ParNew+Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK 9 中完全取消了这些组合的支持(JEP 214)。
Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器, ...
什么是垃圾?
什么是垃圾(Garbage)呢?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要回收的垃圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出。
为什么需要GC
对高级语言来说,一个基本的认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断的分配内存空间而不进行回收,就好像不停地生产生活垃圾而不打扫一样。
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占的堆内存移动到堆的一端,以便jvm将整理出的内存分配给新的对象。
现在应用程序所对应的业务,用户群体日益强大,没有GC就不能保证应用程序的正常进行
垃圾回收算法
垃圾判别阶段算法
在堆里存放着几乎所有的java实例对象,在GC执行垃圾回收之前,首需要区分内存中哪些对象是活的,哪些对象是已死亡只有被标记了已死亡的对象,GC才会在执行垃圾回收时,释放掉其所占有的内存空间,因此这个过程可以称为垃圾标记阶段。
1.引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数 ...
对象的实例化
创建对象的几种方式
new关键字:最常见的方式 变形1:Xxx的静态方法(如Class.forName()) 变形二:Xxx的builder/XxxFactory的静态方法(构建者模式、工厂模式)
Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
Constructor的newInstance(Xxx):反射的方式,可以调用空参,带参的构造器,权限没有要求,实用性更好
使用clone():不调用任何构造器;当前类需要实现Cloneable接口,实现clone(),默认浅拷贝
使用反序列化:从文件、数据库、网络中获取一个对象的二进制流,反序列化为内存中的对象
第三方库Objenesis,利用asm字节码技术,动态生成Constuctor对象
创建对象的步骤
判断对象对应的类是否加载、链接、初始化
虚拟机遇到一个new指令,首先会去检查这个指令的参数能否在元空间(Metaspace)的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、链接和初始化。(即判断类元信息是否存在)
如果没有, ...
为什么会OOM
在《Java 虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(下文称 OOM)异常的可能。表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误
为什么会出现 OOM,一般由这些问题引起
分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽
内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出
内存泄漏持续存在,最后一定会溢出,两者是因果关系
java堆溢出
Java 堆用于储存对象实例,我们只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
...
堆内存图:
⚠️这里的堆是指逻辑上的堆,也就是包括方法区(元空间)
GC的分类
JVM在进行GC时,并非每次都对上面的三个内存(新生代、老年代和方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为俩大种类型:
一种是部分收集(Partial GC)
一种是整堆收集(Full GC)
部分收集:不是完整的收集整个java堆的垃圾收集。其中可分为:
新生代收集(Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾回收,注意只有当Eden区满的时候才会进行回收。
老年代收集(Major GC/Old Gc):只是老年代的收集。
❗目前只有CMS垃圾回收器会有单独收集老年代的行为,因为老年代的回收往往伴随着新生代的回收,正因为此,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前只有G1垃圾回收器会有这种行为
整堆回收(Full GC):收集整个java堆和方 ...
针对String,StringBuffer,StringBuilder的区别可以从四个方面来回答:
可变性:String内部的value值是经过final关键字修饰的,所以它是一个不可变的类,所以每一次修改String的值时,相当于重新创建一个String对象;而StringBuffer和StringBuilder是可变类,字符串的变更不会产生新的对象。
线程的安全性:因为String是个不可变的类,所以它是线程安全的;而StringBuffer里面的每个操作方法都用了一个synchronized一个同步关键字修饰,所以它也是线程安全的;而StringBuilder不是线程安全的。所以在多线程环境下对字符串进行操作时候,应该要使用StringBuffer。
性能方面:String是性能最低的,因为不可变,意味着做字符串拼接或者修改的时候都要重新创建String对象以及分配内存。其次StringBuffer性能要比String好一些,因为它的可变性意味着在字符串拼接和修改的时候不用去创建新的对象可以直接被修改。最后StringBuilder的性能最好,因为StringBufffer加了 ...
JVM
未读java中的常量池
jvm中常量池主要分为Class文件常量池、运行时常量池、字符串常量池以及基本类型包装类对象常量池。
1.Class文件常量池
class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译成.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。class文件中存在常量池(非运行时常量池),其在编译阶段就已经确定,jvm规范对class文件结构有着严格的规范,必须符合此规范的class文件才能被jvm认可和加载。
简而言之,就是class文件中除了包含类的版本、字段、方法、接口等信息。还有一项信息就是常量池(constant pool table),它用于存放编译器生成的**各种字面量(Literal)和符号引用(Symbolic References)。**字面量比较接近java语言层面常量的概念,比如文本字符串,被声明final的常量值。符号引用是编译原理方面的概念,包括了如下三种类型的常量:1.类和接口的全限定名2.字段的名称和描述符3.方法的名称和描述符
2.运行时常量池
运行时常量池是方法区 ...
jvm内存架构
jvm架构主要分为三个部分:
jvm内存模型,主要包括了方法区,堆,虚拟机栈,程序计数器,本地方法栈。
执行引擎,包括最核心的解释器和GC垃圾回收器,还有JIT即时编译器
本地方法接口和库
JVM内存模型
堆
在java中,堆被分为俩个不同的区域:新生代(Young)和老年代(Old),新生代又被划分为三个区域:Eden(伊甸园区),From Survior(幸存者1区)和To Survivor(幸存者2区)
主要存储内容:
对象实例
类初始化生成的对象
基本数据类型的数组也是对象实例
字符串常量池。字符串常量池原本在方法区中,jdk1.8之后开始放置于堆中。
静态变量。1.static修饰的静态变量,jdk1.8时从方法区迁移至堆中;2.线程分配缓冲池(Thread Local Allocation Buffer),是线程私有的,但不影响堆的共性,增加线程分配缓冲池是为了提升对象分配时的效率。
💔堆是java虚拟机所管理的内存中最大的一块区域,也是被各个线程共享的区域,该内存区域存放了对象的实例以及数组(但不是所有对象实例都在堆中,原因在于由于即时编译 ...
类的生命周期
类从被加载到虚拟机内存中开始到卸载出内存为止,他的整个生命周期总共有七个阶段:加载(loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以被统称为连接(linking)。
如下图所示:
类加载过程
class文件需要加载到虚拟机之后才能运行和使用,系统加载class类型的文件主要分为三步:加载—>连接—>初始化。连接过程又可分为:验证—>准备—>解析
1.加载
类加载过程的第一步,主要完成下面3件事情:
通过全类名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表该类的class对象,作为方法区这些数据的访问入口
加载这一步主要是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。
2.验证
该阶段主要是为 ...
1.Hash算法和Hash表
了解Hash冲突首先要了解Hash算法和Hash表
如图所示:1.Hash算法就是吧任意长度的输入通过散列函数算法变成固定长度的输出,这个输出结果是一个散列值。2.Hash表又叫做“散列表”它通过key直接访问到内存存储位置的数据结构,在具体的实现上,我们通过Hash函数,把key映射到表中的某个位置,来获取这个位置的映射,从而加快数据的查找。
2.Hash冲突
所谓的hash冲突是由于哈希算法被计算的数据是无限的,而计算后的结果的范围是有限的,所以总存在不同的数据,经过计算之后得到的值是一样的,那么这种情况下就产生了所谓的hash冲突。
3.解决hash冲突的四种方法
通常解决hash冲突的方法有四种,分别是开放地址法(也叫线性探测法)、链式寻址法、再Hash法、建议公共溢出区。
3.1 开放地址法
开放地址法也被称作线性探测法,就是在发生冲突的那个位置开始,按照一定次序从hash表中找到一个空闲位置然后把爱发生冲突的元素存入到这个位置,而在java中,ThreadLocal就用到了线性探测法来解决hash冲突。
如图所示,在Hash表索引1的位置存 ...