Redis
1.Redis为什么这么快?
-
纯内存操作
-
单线程
-
高效的数据结构
-
使用多路 I/O 复用模型,非阻塞 IO;
-
Redis 直接自己构建了 VM 机制
2.Redis 数据结构原理
在 Redis 中,常用的 5 种数据结构和应用场景如下:
- **String:**缓存、计数器、分布式锁等。
- **List:**链表、队列、微博关注人时间轴列表等。
- **Hash:**用户信息、Hash 表等。
- **Set:**去重、赞、踩、共同好友等。
- **Zset:**访问量排行榜、点击量排行榜等。
127.0.0.1:6379> set a 123
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> hmset b name jack age 12
OK
127.0.0.1:6379> type b
hash
127.0.0.1:6379> lpush c 1 2 3
(integer) 3
127.0.0.1:6379> type c
list
127.0.0.1:6379> sadd d 1 2 3
(integer) 3
127.0.0.1:6379> type d
set
127.0.0.1:6379> zadd e 2 a 3 b
(integer) 2
127.0.0.1:6379> type e
zset
127.0.0.1:6379>
3.Redis 数据持久化
Redis中为了保证在系统宕机(类似进程被杀死)情况下,能更快的进行故障恢复,设计了两种数据持久化方案,分别为rdb和aof方式。
-1.Rdb方式持久化
Rdb方式一般为redis的默认数据持久化方式,通过手动(save-阻塞式,bgsave-异步)或周期性方式保存redis中key/value的一种机制,
打开redis.conf文件
# 这里表示每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting(快照)。
save 60 1000
# 持久化 rdb文件遇到问题时,主进程是否接受写入,yes 表示停止写入,如果是no 表示redis继续提供服务。
stop-writes-on-bgsave-error yes
# 在进行快照镜像时,是否进行压缩。yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间。
rdbcompression yes
# 一个CRC64的校验就被放在了文件末尾,当存储或者加载rbd文件的时候会有一个10%左右的性能下降,为了达到性能的最大化,你可以关掉这个配置项。
rdbchecksum yes
# 快照的文件名
dbfilename dump.rdb
# 存放快照的目录
dir /var/lib/redis
- shutdown 命令 关闭redis ,redis在退出的时候会将内存中的数据立即生成一份完整的rdb快照,数据不会丢失
- kill -9粗暴杀死redis进程, 数据丢失了
- 手动调用save(同步保存)或bgsave(异步保存)执行rdb快照生成.然后杀掉redis进程,数据还在
优点
- RDB会生成多个数据文件,是某一个时刻中redis的数据,非常适合做冷备份
- 对外提供的读写服务,影响非常小,可以让redis保持高性能
- 对于AOF,重启和恢复redis进程,更加快速。
缺点
隔一段时间做一次快照,会丢失最近几分钟的数据。
-2.Redis高可用
-
哨兵模式
(1)集群监控 (2)消息通知 (3) 故障转移 (4) 配置中心
-
官方Redis Cluster
Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
-
主从架构
组成Master-Master或者Master(写)-Slave(读)的形式,搭建Redis集群读写分离
-
一台服务器做Redis集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。要保证集群的高可用,需要每个节点都有从节点,,所以Redis集群至少需要6台服务器
-3.Aof方式数据持久化
Aof方式是通过记录写操作日志的方式,记录redis数据,默认是关闭的。
# 是否开启AOF,默认关闭
appendonly yes
# 指定 AOF 文件名
appendfilename appendonly.aof
# Redis支持三种刷写模式:
# appendfsync always #每次收到写命令就立即强制写入磁盘,类似MySQL的sync_binlog=1,是最安全的。但该模式下速度也是最慢的,一般不推荐使用。
appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做平衡,推荐该方式。
# appendfsync no #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不推荐。
#在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。
#设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes
no-appendfsync-on-rewrite yes
#当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。
auto-aof-rewrite-min-size 64mb
- kill -9杀掉redis进程, 重启,从appendonly.aof中加载所有的日志,把内存中的数据恢复回来。
优点
1.最多丢失1秒钟的数据.
2.写入性能非常高,而且文件不容易破损
3.AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
4.AOF日志文件的命令通过易读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。
缺点
1.AOF日志文件通常比RDB数据快照文件更大。
2.支持的写QPS会比RDB支持的写QPS低,
4.缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
-
缓存雪崩 : 由于原有缓存失效,新缓存未到期间,所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。
-
解决办法: 1.使用锁或队列, 2.设置过期标志更新缓存、为key设置不同的缓存失效时间,3.“二级缓存”
-
**缓存击穿:**是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
-
解决办法:
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁
-
**缓存穿透 😗*指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。
-
解决办法:
1.采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉
2.id做基础校验,id<=0的直接拦截,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
-
缓存预热 : 系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!
-
解决办法: 1、写个缓存刷新页面,提前刷新,2.定时刷新缓存
-
缓存更新 :
(1)Redis缓存过期策略(六种)
(2)定时去清理过期的缓存;
-
缓存降级: 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
5.Redis:一致性Hash算法
(63条消息) Redis分布式算法原理—Hash一致性理解_我思故我在 coding很澎湃-CSDN博客
-
存一条数据都有可能存储在任何一组Redis集群中,为方便找到哪台服务器存储了数据
-
Hash算法的缺陷,体现在服务器数量变动的时候,所有缓存的位置都要发生改变!严重会引发缓存雪崩
具体做法:
一致性Hash算法是对2^ 32-1取模,将整个Hash值控件组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-232-1取模(即哈希值是一个32位无符号整型),把这个由232个点组成的圆环称为Hash环。
具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,
公式: hash(服务器A的IP地址)% 2^32
容错性和可扩展性
集群中的 有一台服务器宕机或突然增加,受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
Hash环的数据倾斜问题
服务节点太少时,容易因为节点分部不均匀而造成数据倾斜
解决办法: 凭空的让服务器节点多起来,由实际节点虚拟复制而来的节点来构建"虚拟节点",再进行哈希,还有一步是虚拟节点到实际节点的映射,就解决了服务节点少时数据倾斜的问题。
6.Redis事务
-1.什么是事务?
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
-2.事务管理(ACID)
-
原子性(Atomicity)
事务中的操作要么都发生,要么都不发生。 -
一致性(Consistency)
事务前后数据的完整性必须保持一致。 -
隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行 -
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响**Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。**当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。事务不保证原子性,且没有回滚。执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
-3.Redis事务相关命令
-
MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
-
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
-
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
-
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
-
UNWATCH命令可以取消watch对所有key的监控。
-4.三个阶段
- 事务开始 MULTI
- 命令入队
- 事务执行 EXEC
7.Redis分布式
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
8.如何保证缓存与数据库双写时的数据一致
修改完数据库后,刷新缓存(把缓存的这个key给删除)
JAVA基础面试
1.==与equals区别
1、==:如果是基本类型的话比较的是他们的值是否相等,如果是引用类型的话比较的则是他们的内存地址值.
2、没有重写的equals方法和是一样的,但如果重写后根据具体方法实现来比较内容, 如Integer、String类型内部是对equals重写,比较的是内容值,不是比较内存地址值
2.hashCode 与 equals(重要)
为什么重写equals时必须重写hashCode方法?”
保证两个对象是否一致
hashcode:判断地址值是否一样
equals():判断对象里的数据是否一样
3.方法的重载与重写的区别
方法的重载是指在一个类中定义多个同名的方法,但是每个方法的参数列表不同
方法的重写是继承后,子类对父类功能不满意,需要重写,方法名相同,参数列表相同,并遵循两同两小一大*子类方法的返回值类型<=父类方法的返回值类型
*子类方法抛出的异常类型<=父类方法抛出的异常类型
*子类方法的修饰符>=父类方法的修饰符,
需要用注解@override标记,
4.面向过程与面向对象
面向对象OOP是一种编程思想
较之前的面向过程而言,面向过程强调的是过程,凡事亲力亲为
而对象强调的是结果,我们由之前每件事物的具体执行者转变为了指挥者要做事情,先创建对象出来,通过对象来完成具体业务,提高效率,
什么时候用面向对象?
万物皆对象,正是因为对象无处不在,所以容易被忽略
与其他技术点来说,面向对象的思想无处不在,只要想干活,先创建对象
封装
是隐藏对象的属性和实现细节,仅仅对外提供公共的访问方式,
构造方法
它是一个与类同名且没有返回值类型的方法,功能就是完成对象创建或者初始化,创建对象时构造方法会立即触发
构造代码块
类里方法外,用于提取所有构造方法的共性内容,创建对象时执行且优先与于构造方法加载,
局部代码块
方法里,用于控制变量的作用范围,范围越小越好,调用此局部代码块所在的方法时才会执行
继承
继承多用于功能的修改,子类可以在拥有父类功能的同时,进行功能拓展,extends关键字
父类的私有资源,子类不可用,不可见
多态
即同一个对象,在不同时刻,代表的对象不一样,指的是对象的多种形态。
在使用方法时,方法的声明看的是父类,而具体实现使用的是子类
如果父子类重写了,使用的是子类的方法,其他时刻,当作父类的类型
5.static与final的用法和的区别
static可以类名.方法
被static的资源为静态资源,随着类的加载而加载,优先于对象加载,比对象先加载进入内存,只加载一次,就会一直存在,不再开辟新空间, 直到类消失才一起消失静态变量被所有的对象所共享,
修饰属性,方法,代码块,不能修饰类,但可以修饰内部类,不能和this或者super共用
final
被final修饰的类,不能被继承,方法,不能被重写,字段,值不能被修改,final的作用是用来保证变量不可变
6.String 和 StringBuffer、StringBuilder 的区别是什么 String 为什么是不可变的
StringBuffer 使用了同步锁,所以是线程安全的,性能差(synchronized获取锁和释放锁也还是需要时间的,),用于多线程。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的,性能好,用于单线程
String 类中使用 final 关键字修饰,所以不可变
7. 接口和抽象类的区别是什么
1.所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法
//2.抽象类用关键字abstract class,接口用 interface声明
3.一个类可以实现多个接口,但最多只能实现一个抽象类
//4.一个类实现接口的话要实现接口的所有方法,而抽象类不一定
5.从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
IO的继承结构
1.字节输入流
InputSteam 抽象类,不能new,可以作为超类,学习其所提供的共性方法
1> FileInputStream 子类,操作文件的字节输入流—普通类
2> BufferedInputStream 高效字节输入流—普通类
2.字符输入流
Reader 抽象类,不能new,可以作为超类,学习其所提供的共性方法
1> FileReader 子类,文件字符输入流–普通子类
2> BufferedReader 子类,高效(缓冲)字符输入流— 普通子类
3.字节输出流:
OutputStream 抽象类,不能new,可以作为超类,学习其所提供的共性方法
1> FileOutputStream 文件字节输出流 - 普通子类
2> BufferedOutputStream 高效字节输出流 - 普通子类
4.字符输出流
writer 抽象类,不能new,可以作为超类,学习其所提供的共性方法
1>FileWriter 文件字符输出流-普通子类
2> BufferedWriter 高效(缓冲)字符输出流-普通子类
线程
1.概念
1.线程:运算调度的最小单位, 它被包含在进程之中,是进程中的实际运作单位
2.进程:是程序一次执行的过程
3.程序是静态的代码。
并发:是一个处理器同时处理多个任务 一个人同时吃三个馒头,
并行:多个处理器或者是多核的处理器同时处理多个不同的任务。三个人同时吃三个馒头
并行具有并发的含义,而并发则不一定并行。
1.1 同步
A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
1.2 异步
A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
1.3 CPU处理线程
分配时间段,每个线程运行一个时间段换另一个时间段,从整体看好像都在运行,其实同一时间只在运行一个线程
2.基本状态
-
新建(new):新创建了一个线程对象。
-
可运行(runnable):调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
-
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice)
-
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,
- 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 三). 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,中间导致阻塞的事件完成后,线程重新转入可运行(runnable)状态。
5.死亡(dead):线程run()、main()方法执行结束线程死亡
3.实现线程同步的方法有哪些?
synchronized(Object){ } 同步代码
public synchronized void sale() { } 同步方法
4.三 什么是死锁?
由于多个线程都无法得到相应的锁而造成的所有线程都处于等待锁的状态的现象
锁:synchronized
lock:手动的获取锁和释放锁,如果拿不到锁,就一直处于等待状态,直到拿到锁
tryLock():tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待。
5.多线程有几种实现方法?
多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
1.extends Thread(继承):写法简单,但后续无法继承其他类【Java单继承】,锁对象一般使用本类的字节码对象/
2.implements Runnable(实现接口):多实现,更加灵活且解耦,但是写法相对复杂,还有一些资源借助Thread,不限制锁对象的类型【类型、也可以使用字节码对象】,只需要保持锁对象的唯一,因为目标对象只创建一次
3.线程池创建
4.实现callable:需要重写call();方法
6.创建线程池的方法?
线程池: ThreadPoolExecutor
- corePoolSize(必需):核心线程数, 可超时收回
- maximumPoolSize(必需):线程池所能容纳的最大线程数。
- keepAliveTime(必需):线程闲置超时时长非核心线程被回收,若设置核心线程时效,超时也被收回
- unit(必需):keepAliveTime 参数的时间单位。
- workQueue(必需):任务队列。线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):创建新线程的线程工厂。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略
线程池的工作原理:
1.当前线程数是否小于核心线程数, true 创建新的线程可以执行线程,false执行2
2.判断任务队列是否可以添加任务了? true,放在任务队列中,等待执行,false执行3
3.判断线程池线程池小于最大线程数? true,新建非核心线程执行 ,false执行4
4.执行Handler拒绝策略,不执行,
5.所有true线程执行完所有任务,没有在超时时间内获取任务就会被回收,结束
拒绝策略(handler):
实现 RejectedExecutionHandler 接口
1.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。默认
2.CallerRunsPolicy:由调用线程处理该任务
3.DiscardPolicy:丢弃任务,但是不抛出异常
4.DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
功能线程池
- 定长线程池(FixedThreadPool) 自己规定核心线程数
- 定时线程池(ScheduledThreadPool )核心线程数量固定,非核心线程数无限,执行定时或周期性的任务。
- 可缓存线程池(CachedThreadPool)执行效率快
- 单线程化线程池(SingleThreadExecutor) 只有一个核心线程
6.sleep()和wait()方法的区别
- sleep方法是Thread类里面的方法,而wait是Object里面的方法
- sleep当处于休眠状态的线程获得锁时,其它线程并不能够重新获取锁,而wait方法是释放锁的,其它线程可以获得锁而获取对资源的控制。
- 调用notify唤醒的线程,是任意的一个,处于等待获取锁状态,没有获取对象锁,则将抛出IllegalMonitorStateException异常。
- notifyAll方法 唤醒的是所有线程。
异常
throwable类 超父类
常见运行时异常
-
ArithmeticException:数学计算异常。
-
NullPointerException:空指针异常。
-
NegativeArraySizeException:负数组长度异常。
-
ArrayOutOfBoundsException:数组索引越界异常。
-
ClassNotFoundException:类文件未找到异常。
-
ClassCastException:类型强制转换异常。
-
SecurityException:违背安全原则异常。
(常见非运行时异常)
-
FileNotFoundException: io文件未找到异常
-
NumberFormatException:字符串转换为数字异常。
-
SQLException:操作数据库异常
-
NoSuchMethodException:方法未找到异常。
-
IOException:输入输出异常。
-
EOFException:文件已结束异常。
Java 内存区域
java虚拟机自动内存管理机制,不再需要像C/C++程序开发,new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。
**运行时数据区域:**Java 虚拟机把它管理的内存划分成若干个不同的数据区域。
线程私有的:
- 程序计数器
- java虚拟机栈
- 本地方法栈
线程共享的:
-
堆
-
方法区
-
直接内存
**内存溢出OutOfMemory:**指程序在申请内存时,没有足够的内存空间供其使用
内存泄漏MemoryLeak:指程序在申请内存后,无法释放已申请的内存空间:StackOverFlowError(线程请求栈的深度超过当前Java虚拟机栈的最大深度)
1.程序计数器
1.程序计数器:程序计数器(Program Counter Register),也有称作为PC寄存器。它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
2.程序计数器用于记录当前线程执行的位置,线程被切换回来的能找到
注意:程序计数器是唯一一个不会出现内存溢出的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2.Java 虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期和线程相同,来描述的是 Java 方法执行的内存模型。
每个方法在执行时都会创建一个栈桢存储来方法的信息,每个方法从调用到执行完成,就是栈桢从入栈到出栈的过程
两个异常:StackOverFlowError(线程请求栈的深度超过当前Java虚拟机栈的最大深度) 和
OutOfMemoryError(线程请求栈时内存用完了,无法再动态扩展了)。
3.本地方法栈
本地方法栈:本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的信息
也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4.堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆:由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代,更好地回收内存,或者更快地分配内存。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域,而元空间使用的是物理内存,直接受到本机的物理内存限制)
5.方法区
各个线程共享的内存区域,它用于存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。 非常重要的部分就是运行时常量池
垃圾收集行为在这个区域是比较少出现的,无法再申请到内存时会抛出 OutOfMemoryError 异常。
运行时常量池
运行时常量池是方法区的一部分。在类和接口被加载到JVM后,对应的运行时常量池就被创建出来,
无法再申请到内存时会抛出 OutOfMemoryError 异常。
7.直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
堆—垃圾回收机制
回收流程
新建的对象再新生代中,如果新生代内存不够,就进行Minor GC释放掉不活跃对象;如果还是不够,就把部分活跃对象复制到老年代中,如果还是不够,就进行MajorGC释放老年代,如果还是不够,JVM会抛出内存不足,发生oom,内存泄漏。
一)GC的主要任务:
1.分配内存;
2.确保被引用对象的内存不被错误的回收;
3.回收不再被引用的对象的内存空间
二)垃圾回收机制的主要解决问题
1.哪些内存需要回收?
首先确定对象中哪些是“存活”,哪些是”死亡“
【1】.引用计数算法
每当一个地方引用它时,计数器+1;引用失效时,计数器-1;计数值=0——不可能再被引用,但有例外情况两个对象互相引用就没有回收,很难解决对象之间相互矛盾循环引用的问题。
【2】可达性分析算法:
向图,树图,把一系列“GC Roots”作为起始点,从节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。
2.什么时候回收?
即使在可达性分析法中不可达的对象,也要再进行筛选,至少要经历两次标记过程,当分析出不可达的对象是标记一次,
接下来判断对象中的finalize()方法,是否有必要执行?
1.对象没有覆盖finalize()方法,2.finalize方法已经被虚拟机调用过, 满足以上则没有必要执行,将不被标记
如果有必要执行——放置在F-Queue的队列中——Finalizer线程执行进行第二次标记。
3.如何回收?
垃圾收集算法和垃圾收集器
CMS收集器:业务线程在内存中执行产生新对象的同时,多个垃圾回收线程在获取CPU时间片的时候可以进行回收
其中三色标记算法(对象的三种标记状态)和stw(最终内存快满了解决垃圾回收,需要暂定所有业务线程工作,)
漏标的解决方式,最后有remark重新标记,再清理
G1收集器:把内存区域划分,以多个分区和分代的方式,新生代(伊甸元区Eden ,幸存者区Survivor),老年代,三个区域回收后,可转换成其他两个区域去存放对象
漏标的解决方式,把改变调用链路压入到一个栈,重新扫描漏标的对象,看是否有链路调用它
垃圾收集算法:
1.标记—清除算法
两个阶段:标记,清除;
不足:效率问题;空间问题(会产生大量不连续的内存碎片)
2.复制算法
将可用内存按容量分为大小相等的两块,每次都只使用其中一块;
不足:将内存缩小为了原来的一半
新生代
3.标记—整理算法
标记,清除(让存活的对象都向一端移动)
老年代
4.分代收集算法
般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
新生代中,每次收集都会有大量对象死去,所以可以选择复制算法
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。**
类加载的过程:
加载、验证、准备、解析、初始化