数据库
Redis
概念:是一个C开发的、开源的、高性能键值对的内存数据库。是一种Nosql(非关系型)数据库。
-
内存数据库:数据保存在内存中,读写速度巨快,不存在写错,不存在数据不完整性,数据结构丰富。
-
磁盘数据库:数据存在硬盘当中。
应用:数据库、缓存、消息中间件
持久化:按照一定的时间周期策略把内存的数据以快照的形式存进硬盘,重启后读回内存
优点:
- 数据保存在内存中,读写速度巨快,支持并发10w QPS(每秒查询率)。
- 单进程单线程,是线程安全的,采用IO多路复用机制
- 丰富的数据类型,支持字符串(strings)、散列(hashes)(键值的集合)、列表(lists)(简单的字符串列表)、集合(sets)(String类型的无序集合)、有序集合(sorted sets)(不允许重复元素的set)等
- 支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载
- 主从复制,哨兵,高可用
- 可以用作分布式锁
- 可以作为消息中间件使用,支持发布订阅
**使用:**和Springboot搭配使用,和mysql搭配使用,作为mysql的缓存使用
- RedisTemplate
- String cache集成Redis(就是注解)
缓存穿透:(使用布隆过滤器)
查询的数据不存在,没办法进行缓存
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
缓存击穿/缓存雪崩:((加锁)设置热点数值永不过期,其他过期时间随机)
热点数据过期导致
数据库的事务
事务是作为一个逻辑单元执行的一系列操作,一个逻辑工作单元必须有四个属性,称为 ACID(原子性(要么全执行、要么全不执行)、一致性(所有数据保持一致)、隔离性(禁止并发)和持久性(不会改变))属性
Redis有事务
- 四命令MULTI(标记事物块)
- EXEC(执行命令)
- DISCARD(取消事务,放弃执行)
- WATCH(监视一个或多个key,如果执行之前key被改动就打断事务)
Redis不支持回滚机制
MySql
索引
索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据。
常见的MySQL主要有两种结构:Hash索引和B+ Tree索引。
InnoDB默认B+树。
Where和Having
having可以和聚合函数搭配使用,比如groupby,sum(),count(),avg(),max(),min()等。
where不可以和聚合函数搭配使用
final和static的区别:
final是常量,初始化后不能被修改,修饰的方法不能被重写,修饰的类不能被继承
static是静态的,不能修饰局部变量,且被它修饰的变量不会生成副本,所有的对象公用一个。
- extends T>:上界通配符:实例化时的类只能是定义时类本身或其子类,也就是说T是它的上界。
- superT>:下界通配符:实例化时的类只能是定义时类本身或其父类,也就是说T是它的下界。
日志
- 错误日志(默认开启无法被禁止,在数据库的数据文件Data目录中,名字为服务器主机名.err)
- 服务器启动和关闭过程中的信息(不一定是错误信息)
- 服务器运行过程中的错误信息
- 事件调度器运行一个事件时产生的信息
- 从服务器上启动服务器进程时产生的信息
- 查询日志(记录数据库执行过的所有不管是否正确的命令)
- 慢查询日志(导致CPU、IOPS、内存消耗过高的日志,超过指定时间的日志)
- 事务日志(重做日志redo和回滚日志undo)(为了保证事务的ACID性,在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。)
- redo日志:(用来保持持久性)一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。InnoDB使用日志来减少提交事务时的开销。
- undo log有两个作用:提供回滚和多个行版本控制(MVCC)。undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
- 二进制日志(主要记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的所有操作。不记录SELECT、SHOW等那些不修改数据的SQL语句。用来恢复(恢复数据)、复制(实行多个数据库数据同步)、审计(判断是否有注入攻击))
- 逻辑日志,记录sql语句,通过追加方式写入,通过max_binlog_size参数设置日志文件大小。
- binlog刷盘时机:通过sync_binlog参数控制。默认为1
- 参数为0:不去强制要求,由系统自行判断何时写入磁盘;
- 参数为1:每次commit的时候都要将binlog写入磁盘;
- 参数为n:每N个事务,才会将binlog写入磁盘。
- binlog日志格式:
- STATEMENT:基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。(优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO;缺点:在某些情况下会导致主从数据不一致,比如执行sysdate()、sleep()等)
- ROW:基于行的复制(row-based replication, RBR),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了。(优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题;缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨)
- MIXED:基于STATMENT和ROW两种模式的混合复制(mixed-based replication, MBR),一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog
- 中继日志(relay log是复制过程中产生的日志)
MySQL的两大数据库引擎:InnoDB 支持事务,MyISAM 不支持事务
服务器
Nginx
nginx运行原理
概念:轻量级的反向代理web服务器。(进行负载均衡)
同类产品:早期的Apache,不支持高并发
Nginx支持的负载均衡算法:
- weight轮询:根据权重挨个问一遍
- ip_hash:按照客户端的ip的hash结果进行匹配(解决了j集群部署环境下Session共享问题)
优点:内存占用少,启动极快,高并发能力强,跨平台,
Nginx的Master-Worker模式
Master进程的作用:
- 读取并验证nginx.conf;
- 管理worker进程。
Worker进程的作用:
- 每一个进程都维护一个线程(避免线程切换),处理连接和请求。
- Worker进程的个数由nginx.conf决定,一般和CPU个数相关(有利于进程切换)
热部署方式:
- 修改配置文件后,master负责推送给worker更新,worker受到信息后,更新进程内部的线程信息。
- 修改配置文件后,重新生成新的worker,以新的配置进行请求处理,新的请求都交由新的worker处理,老的worker处理完之前的请求后kill掉。(Nginx的方式)
Nginx的高并发处理:
采用Linux的epoll模型,epoll模型基于事件驱动机制,它可以监控多个事件是否准备完毕,如果完成,那么放入epoll队列中(或者是双向链表?),这个过程是异步的,worker只需要从epoll队列循环处理即可。
如何预防Nginx挂了?
Keepalived + Nginx
- 请求打到Keepalived上(虚拟ip)
- keepalived监控Nginx生命状态,从而实现Nginx故障切换。
反向代理服务器
代理服务端,对客户端而言是透明的。经常用于分布式部署服务器,解决高并发
Keepalived
是一种高性能的服务器高可用或热备解决方案,用来防止服务器单点故障的发生。
通过vrrp协议实现:VRRP(Virtual RouterRedundancy Protocol)虚拟路由器冗余协议,将多台路由器设备虚拟成一个设备,对外提供虚拟路由IP(一个或多个),在路由器组内部,
- 实际拥有对外IP的路由器叫做Master,或者通过算法选举产生,master实现针对虚拟路由器IP的各种网络功能,如ARP请求,ICMP,以及数据的转发等。
- 其他路由器不拥有该虚拟IP,状态是BUCKUP,出了接收master的VRRP状态通告信息外,不执行对外的网络功能。master失效时才接管网络功能。BUCKUP只接受VRRP,不发送数据,一段时间内没有接收到master的信息就宣告自己为master,并重新选举。
工作方式:
- 抢占式:master挂了,buckup就抢,谁抢到就谁的,master复活了还得还回去。
- 非抢占式:master挂了,buckup就抢,谁抢到就谁的,master掀了棺材板复活了也不还。(减少了设备恢复后切换造成的延迟)
JAVA
数据类型
-
整型
byte(1):默认值:0,取值:-2的7次方,到2的7次方-1
short(2):默认值:0,取值:-2的15次方到2的15次方-1
int(4):默认值:0,取值:-2的31次方到2的31次方-1
long(8):默认值:0.0L,取值:-2的63次方到2的63次方-1
-
浮点型
float(4):默认值:0.0F
double(8):0
-
字符型
char(2):\u000
- 初始化方式:可以是'a'(也可以是汉字)/1010(十进制数)/'\0'(结束符)
- java用unicode来表示字符,一个中文字符占两个字节。
-
布尔型
boolean(1)
数据类型还有参考类型:类、数组、接口
数据类型之间的转换:
- boolean型不和其他7种类型进行转换
- 转换类型:
- 自动转换(隐式):无需任何操作
- 强制转换(显式):需使用转换操作符(type)
- 转换顺序:double > float > long > int > short > byte
- 从小到大隐式转换
- 从大到小或者char和其他6中数据类型转换为显式转换。
- 自动转换:
- 较大的类型保存较小的类型会进行数据转换
- 将字面值保存到byte、short、int、char时候,也会进行数据转换。
- 强制转换:
- 较小的类型保存较大的类型需要强制转换
- short与char之间转换需要强制转换(缩小转换)
- 将byte转为char不属于缩小转换
- 从byte到char实际上是byte到int再到char
- 字面值赋值:可以将int赋值给byte、short、char、int,这将进行自动转换,如果将long类型赋值给byte,将需要强制转换。
- 表达式中的自动类型提升:在表达式中
- 所有byte、short、char都被提升为int
- 如果有一个操作数为long,整个表达式提升为long。flort和double同理。
反射机制(reflect)
概念:
- 运行中的程序检查自己和软件运行环境的能力。
- 能够在程序运行时动态访问、修改某个类中任意属性(状态)和方法(行为)的机制。java反射机制提供的功能:
- 在运行时判断一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法。
反射的四个核心类:
- java.lang.Class.java:类对象;
- java.lang.reflect.Constructor.java:类的构造器对象;
- java.lang.reflect.Method.java;类的方法对象;
- java.lang.reflect.Field.java;类的属性对象。
反射有什么用:(反射的初衷是为了写代码的时候更加灵活,降低代码的耦合度,提高代码的自适应能力)
- 操作因访问权限限制的属性和方法;
- 实现自定义注解
- 动态加载第三方jar包,解决android开发中方法不能超过65536个的问题;
- 按需加载类,节省编译和初始化APK的时间’;
反射工作原理:
- java项目编译流程:将java文件编译为 .class 格式文件,这些class对象承载了这个类的所有信息,包括父类、结构、构造函数、方法、属性等。这些class文件会在程序运行时被classLoader加载到虚拟机中。当一个类被加载以后,Java虚拟机就会在内存中自动生成一个Class对象,我们通过new的方式创建对象实际上就是通过class来创建。
- 工作原理:借助Class.java、Constructor.java、Method.java、Field.java这四个类在程序运行时动态访问和修改任何类的行为和状态。
反射的特点:
- 优点:灵活、自由度高
- 缺点:
- 性能问题:通过反射操作远慢于直接操作
- 安全性问题:可以随意访问和修改类的所有状态和行为,破坏了类的封装性。
- 兼容性问题:反射会涉及到直接访问类的方法名和实例名,不同版本的API发生变化时,找不到对应的属性和方法会报异常。
通过一个已有的类来得到一个Class对象的方法:
- 方法一:
- 通过JVM查找并加载制定的类;
- 通过newInstance()方法让加载完的类在内存中创建对应的实例,并赋值。
- 方法二:
- 内存中新建一个实例,然后用通过对象调用getClass()方法返回对象所对应的Class对象
- 通过newInstance()方法让加载完的类在内存中创建对应的实例,并赋值。
动态加载类:
程序运行分为编译器和运行期,编译期加载一个类就是静态加载类,运行期加载一个类就是动态加载类。
STREAM流
流,是一系列数据项,它不是一种数据结构。
流可以进行相关的算法和计算,只是它并没有显式地表现,而是在内部进行隐式地操作。
挑选出质量小于500g的鹅卵石,并按照质量从大到小的顺序将它们排成一条线。
Stream 流的使用总是按照一定的步骤进行:
数据源(source) -> 数据处理/转换(intermedia) -> 结果处理(terminal )
数据源:
Collection.stream(); 从集合获取流。
Collection.parallelStream(); 从集合获取并行流。
Arrays.stream(T array) or Stream.of(); 从数组获取流。
BufferedReader.lines(); 从输入流中获取流。
IntStream.of() ; 从静态方法中获取流。
Stream.generate(); 自己生成流
数据处理:
数据处理/转换(intermedia)
步骤可以有多个操作,在这个步骤中不管怎样操作,它返回的都是一个新的流对象,原始数据不会发生任何改变
收集结果:
结果处理(terminal )
是流处理的最后一步,执行完这一步之后流会被彻底用尽,流也不能继续操作了。
Stream的方法:
- forEach:用于遍历流
- map/flatmap:把对象一对一映射成另一种对象或形式
- filter:数据筛选
- findFirst:查找第一个元素
- collect / toArray:转换成其他数据结构
- limit/skip:获取或丢弃前n个元素
- statistics:统计最大值、最小值、个数、数据和、平均数等。
- groupingBy:分组聚合功能,和数据库的GroupBy一致。
**惰性计算:**数据处理/转换(intermedia)` 操作 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等这些操作,在调用方法时并不会立即调用,而是在真正使用的时候才会生效,这样可以让操作延迟到真正需要使用的时刻。
HashMap和ConcurrentHashMap的区别:
HashMap不是线程安全的,不能在多线程下使用,速度快;
ConcurrentHashMap是线程安全的,可以在多线程下使用,但是速度慢一点。
HashMap在put的时候,插入的元素超过容量会触发rehash(扩容操作),然后将原数组中的内容重新hash到新的数组中,在多线程的时候,可能会出现闭环链表,导致get出现死循环。
HashTable线程安全但在线程竞争激烈的时候效率非常低下。
Hashtable只有一把锁,线程竞争。
ConcurrentHashMap将数据分组,多把锁同时竞争。
需要跨组访问时按顺序锁定所有段。
ConcurrentHashMap是由Segment数组结构(扮演锁)和HashEntry数组结构(存储键值对)组成。
一个ConcurrentHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。
jdk1.8之后放弃了segment,使用数组+链表+红黑树的方式。
红黑树:自平衡二叉树。
IOC和AOP
什么是IOC:控制反转
传统的方式:在类A中手动newB的对象
IOC开发方式,通过IOC容器实例化对象。
IOC容器:具有依赖注入功能的容器,可以创建对象的容器。IoC容器负责实例化、定位、配置应用程序中的对象并建立这些对象之间的依赖。
IOC解决的问题:
- 对象之间的耦合度或者说依赖程度降低;
- 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
什么是AOP:面向切面编程,是OOP(面向对象)的一种延续。
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装。
AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。
要功能:日志记录,性能统计,安全控制,事务处理,异常处理等。
JVM
java虚拟机
JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。
一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
JAVA问题合集
接口和抽象类有什么区别?
- 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 构造函数:抽象类可以有构造函数;接口不能有。
- main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
- 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
final在java中的作用?
- final 修饰的类叫最终类,该类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
String、StringBuffer和StringBuilder的异同?
- 相同点:
- 都是final类,不允许被继承
- 不同点:
- StringBuffer是线程安全的,可以不需要额外的同步用于多线程中
- StringBuilder是非同步,运用于多线程就需要使用单独同步处理,但是效率更高,非线程安全
String和其他类的区别
- 重写了toString()方法,返回this
- 重写了equals()方法,Object中的equals()返回的是对象的内存地址,而String的equals()方法比较的是对象的具体值。
多线程
并行和并发的区别?
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
线程和进程的区别?
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程是进程的一个实体,是CPU调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
JVM线程?
JVM分为守护线程和普通线程。
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
创建线程的方式?
- 继承Thread类创建线程类:
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过Runnable接口创建线程类:
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
Runnable和callable的区别?
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
-
创建状态(New)。在生成线程对象,**并没有调用该对象的start()**方法,这是线程处于创建状态。
-
就绪状态(Runnable)。当**调用了线程对象的start()**方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
-
运行状态(Running)。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run()方法当中的代码。
-
阻塞状态(Blocked)。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep(),suspend(),wait()等方法都可以导致线程阻塞。
-
死亡状态(Dead)。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。
有两个原因会导致线程死亡:
- run方法正常退出而自然死亡,
- 一个未捕获的异常终止了run方法而使线程猝死。
sleep()和wait()的区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify(),notifyAll()方法来唤醒等待的线程.
线程的run()和start()的区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个方法,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通方法而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
创建线程池的方式?
-
newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
-
newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
-
newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
-
newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:
- Running:在当前状态,能够接受新任务,以及对已添加的任务进行处理。线程池一旦被创建就是Running状态,且线程池中的任务数为0.
- ShutDown:在当前状态不再接收新任务,但能处理已添加的任务。
- Stop:当前状态不接收新任务,也不处理已添加的任务,并且会中断正在处理的任务。
- Tidying:当所有的任务都已终止,线程池记录的任务数量为0,线程池会变成当前状态。当线程为当前状态时,会执行钩子函数terminated()。
- Terminated:线程池彻底终止。
在java程序中如何保证多线程的运行安全?
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作。
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到。
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序。
JVM
JVM的组成部分有哪些,分别有什么作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
组件的作用(JVM的运行原理):
- 通过类加载器(ClassLoader)把java代码转换成字节码(.class文件)。
- 运行时数据区(Runtime Data Area)再把字节码加载到内存中。
- 而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令。
- 再交由CPU执行,这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
java内存区域
运行时数据区:
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
每一个线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
Java虚拟机栈
java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表
存放方法参数和局部变量的区域
操作栈
操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。
i和i的区别:
- i++(不是原子操作):从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
- ++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
动态链接
每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。
方法返回地址
方法执行时有两种退出情况:
- 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
- 异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置。
退出可能有三种方式:
- 返回值压入上层调用栈帧。
- 异常信息抛给能够处理的栈帧。
- PC计数器指向方法调用后的下一条指令。
本地方法栈
本地方法栈为虚拟机使用到的 Native 方法服务。
Java堆
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
java垃圾回收
判断哪些对象需要被回收?
- 引用计数法
给对象添加一引用计数器,被引用一次计数器值就加 1;当引用失效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被使用的,简单高效,缺点是无法解决对象之间相互循环引用的问题。 - 可达性分析算法
通过一系列的称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上述循环引用的问题。
可作为 GC Roots 的对象包括下面几种:
a. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
b. 方法区中类静态属性引用的对象。
c. 方法区中常量引用的对象。
d. 本地方法栈中 JNI(Native方法)引用的对象。
强、软、弱、虚引用
-
强引用(永久有效):就是指在程序代码之中普遍存在的,类似"Object obj=new Object()"这类的引用,垃圾收集器永远不会回收存活的强引用对象。
Object ojb = new Object();
-
软引用(内存不足):还有用但并非必需的对象。在系统 将要发生内存溢出异常之前 ,将会把这些对象列进回收范围之中进行第二次回收。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj =null; sf.get();//有时候会返回null
-
弱引用(再次YGC):也是用来描述非必需对象的,被弱引用关联的对象 只能生存到下一次垃圾收集发生之前 。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj =null; wf.get();//有时候会返回null wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
-
虚引用(即时失效):是最弱的一种引用关系。 无法通过虚引用来取得一个对象实例 。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); obj=null; pf.get();//永远返回null pf.isEnQueued();//返回从内存中已经删除
可达性分析算法
不可达的对象将暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行
finalize()
方法。 - 当对象没有覆盖
finalize()
方法,或者finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,直接进行第二次标记。 - 如果这个对象被判定为有必要执行
finalize()
方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。(并不承诺一定会执行完成,如果执行缓慢,将可能一直阻塞队列,甚至导致内存回收系统崩溃)
java堆永久代的回收:
-
废弃常量的回收
假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做"abc"的,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
-
无用类的回收
- 类需要同时满足下面 3 个条件才能算是“无用的类”:
a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b. 加载该类的 ClassLoader 已经被回收。
c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 类需要同时满足下面 3 个条件才能算是“无用的类”:
垃圾收集算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:
- 效率问题,标记和清除两个过程的效率都不高;
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(解决效率问题)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
不足:
- 内存缩小为原来的一半
- 在存活率较高时要进行较多的复制操作
标记整理算法
标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
Q.E.D.