java必备知识进阶

目录

1.java的内存管理

a.主要包括:内存分配和内存回收

2.堆和栈的区别

3.字符串hash方法

4.字符串判等

5.Apache和tomcat的区别

6.nginx和tomcat的区别

7.定时任务

8.RabbitMQ  中实现RPC 的机制是:

9.rabbitmq和activemq和kafka的区别

10.常见单例模式

11.mybatis一级缓存和二级缓存

12.如何保证消息消费时的幂等性

13.翻墙原理

14.volatile关键字如何保证内存可见性

15.Comparable和Comparable的区别

16.kafka吞吐量大的原因

17.mybatis$和#的区别

18、 redis为什么是单线程的

19、gc中新生代和老年代回收算法

20.新生代内存划分

21.java时间转换问题

22.创建对象的四种方式

23.字符串为什么是final类型的


1.java的内存管理

a.主要包括:内存分配和内存回收

b.注意点:java的垃圾回收是不能保证一定发生的,除非jvm内存耗尽,合理的管理对象还是有必要的

c.java程序执行过程:java源文件-》java字节码文件.class-》类加载器-》直接到执行引擎或者先到运行时数据区(内存)再到执行引擎

其中类加载过程有:加载-》链接(验证【为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求】、准备【为类变量分配内存并设置初始值】、【解析虚拟机将常量池的符号引用转为直接引用】)-》初始化(初始化类变量和其它资源、执行类构造器方法的过程)

其中类加载顺序:启动类加载器-》扩展类加载器-》应用程序类加载器

其中运行时内存区域划分图:

图

其中类成员初始化顺序:先静态后普通再构造,先父类后子类

1、父类静态变量和代码块-》子类静态变量和代码块
2、父类普通变量和代码块-》执行父类构造器
3、子类普通变量和代码块-》子类构造器
4、static方法初始化优先于普通方法,静态初始化只有在必要时刻进行且只初始化一次

注意: 子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用supper关键子来调用父类带参数的构造方法,否则编译不能通过。

d.四种垃圾回收器和finalize方法:

java中垃圾回收器可以帮助程序猿自动回收无用对象占据的内存,但它只负责释放java中创建的对象所占据的所有内存,通过某种创建对象之外的方式为对象分配的内存空间则无法被垃圾回收器回收;而且垃圾回收本身也有开销,GC的优先级比较低,所以如果JVM没有面临内存耗尽,它是不会去浪费资源进行垃圾回收以恢复内存的。最后我们会发现,只要程序没有濒临存储空间用完那一刻,对象占用的空间就总也得不到释放。我们可以通过代码System.gc()来主动启动一个垃圾回收器(虽然JVM不会立刻去回收),在释放new分配内存空间之前,将会通过finalize()释放用其他方法分配的内存空间。

四种收集器:Serial收集器(单线程的新生代收集器,必须暂停其它所有工作线程直到收集结束)、Parallel收集器(多线程串行收集器)、CMS收集器(基于标记-清除算法实现,容易产生大量碎片,cpu资源占用大)、G1收集器(标记整理算法实现,不会产生空间碎片,高吞吐量)

finalize工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用并且只能调用一次该对象的finalize()方法(通过代码System.gc()实现),并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果我们重载finalize()方法就能在垃圾回收时刻做一些重要的清理工作或者自救该对象一次(只要在finalize()方法中让该对象重新和引用链上的任何一个对象建立关联即可)。finalize()方法用于释放用特殊方式分配的内存空间,这是因为我们可能在java中调用非java代码来分配内存,比如Android开发中调用NDK。那么,当我们调用C中的malloc()函数分配了存储空间,我们就只能用free()函数来释放这些内存,这样就需要我们在finalize()函数中用本地方法调用它。

e.如何判断java对象需要被回收

引用计数,计数为0表示不可用,引用计数记录着每个对象被其它对象所持有的引用数,被引用一次加1,时效减1;当一个对象被回收后,该对象所引用的其它对象的引用计数都会减少,它很难解决对象之间的循环引用实例

可达性分析算法,从GC ROOT对象向下搜索其走过的路径称为引用链,当一个对象不再被任何的GC ROOT对象引用链相连时说明该对象不再可用;GC ROOT对象包括四种:方法区中常量和静态变量的引用对象,虚拟机栈中变量引用的对象,本地方法栈中引用的对象;解决循环引用是因为GC ROOT是一组特别管理的指针,他们不是对象图里的对象,对象也不可能引用到这些指针

比较,可达性分析避免了循环引用的问题,引用计数算法只需要在每个实例对象之初,通过计数器来记录所有的引用次数即可,而可达性分析需要遍历整个GC根节点来判断是否回收

f.java对象的四种引用

强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”); 不管系统     资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。
          软引用 :通过SoftReference类实现,eg : SoftReference p = new SoftReference(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。
          弱引用 :通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收
         虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现

g.垃圾回收算法

停止-复制算法
这是一种非后台回收算法,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,内存浪费严重.它先暂停程序的运行,然后将所有存活的对象从当前堆复制到另外一个堆,没被复制的死对象则全部是垃圾,存活对象被复制到新堆之后全部紧密排列,就可以直接分配新空间了。此方法耗费空间且效率低,适用于存活对象少。

标记-清扫算法
同样是非后台回收算法,该算法从堆栈区和静态域出发,遍历每一个引用去寻找所有需要回收的对象,对每个找到需要回收对象都进行标记。标记结束之后,开始清理工作,被标记的对象都会被释放掉,如果需要连续堆空间,则还需要对剩下的存货对象进行整理;否则会产生大量内存碎片

标记-整理算法
先标记需要回收的对象,但是不会直接清理那些可回收的对象,而是将存活对象向内存区域的一端移动,然后清理掉端以外的内存。适用于存活对象多。

分代算法
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用停止复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

        h.内存相关问题

内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制(例如你把它的地址给弄丢了),因而造成了资源的浪费。Java 中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,Java堆内也可能发生内存泄露(Memory Leak; 当我们 new 了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露

内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

双亲委派模型:表示类加载器之间的加载顺序从顶至下的层次关系,加载器之间的父子关系一般都是通过组合来实现,而不是继承。可以防止内存中出现多份同样的字节码,并确保加载顺序

双亲委派模型的工作过程是:在loadClass函数中,首先会判断该类是否被加载过,加载过则进行下一步—-解析,否则进行加载;如果一个类加载器收到了类加载器的请求,先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜说范围中没有找到所需的类时,子加载类才会尝试自己去加载)

静态分派和动态分派:静态分派发生在编译阶段,是指依据静态类型(变量声明时定义的变量类型)来决定方法的执行版本,例如方法重载中依据参数的定义类型来定位具体应该执行的方法;动态分派发生在运行期,根据变量实例化时的实际类型来决定方法的执行版本,例如方法重写;目前的 Java 语言(JDK1.6)是一门静态多分派、动态单分派的语言。

动态分派具体实现Java虚拟机是通过在方法区中建立一个虚方法表,通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址,如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。

JDK7和8中内存模型变化:JDK7中把String常量池从永久代移到了堆中,并通过intern方法来保证不在堆中重复创建一个对象;JDK7开始使用G1收集器替代CMS收集器。JDK8使用元空间来替代原来的方法区,并且提供了字符串去重功能,也就是G1收集器可以识别出堆中那些重复出现的字符串并让他们指向同一个内部char[]数组,而不是在堆中存在多份拷贝


2.堆和栈的区别

1、空间分配的不同:栈是由操作系统自动分配释放,存放函数的参数值,局部变量的值等,而堆一般是由程序员分配释放,弱不手动释放,程序结束时可能由os回收

2、缓存方式不同:栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完立即释放,而堆是存放在二级缓存中,生命周期由虚拟机的GC算法决定,并不是一旦成为孤立对象就能被回收

3、数据结构的不同:堆可以被看成是一棵树 ,栈是一种先进后出的数据结构


3.字符串hash方法

•现在我们希望找到一个hash函数,使得每一个字符串都能够映射到一个整数上
•比如hash[i]=(hash[i-1]*p+idx(s[i]))%mod
•字符串:abc,bbc,aba,aadaabac
•字符串下标从0开始
•先把a映射为1,b映射为2,c->3,d->4,即idx(a)=1, idx(b)=2, idx(c)=3,idx(d)=4;
•好!开始对字符串进行hash
假设我们取p=13 ,mod=101
先把abc映射为一个整数
hash[0]=1,表示 a 映射为1
hash[1]=(hash[0]*p+idx(b))%mod=15,表示 ab 映射为 15
hash[2]=(hash[1]*p+idx(c))%mod=97
这样,我们就把 abc 映射为 97 这个数字了。
•用同样的方法,我们可以把bbc,aba,aadaabac都映射到一个整数
•用同样的hash函数,得到如下结果
• abc  ->  97
• bbc  ->  64
• aba  ->  95
• aadaabac  ->  35
•那么,我们发现,这是一个字符串到整数的映射
•这样子,我们就可以记录下每个字符串对应的整数,当下一次出现了一个已经出现的字符串时,查询整数是否出现过,就可以知道 字符串是否重复出现。
•现在要判断两个字符串是否一致,怎么办呢?直接用它们的hash值判断即可,若hash值一致,则认为字符串一致;若hash值不一致,则认为是不同的字符串。
•我们要判断两个字符串是否一致,没有那么麻烦,直接先判断长度是否一致,然后再判断每个对应的字符是否一致即可。
•但,如果要判断多个字符串里有多少个不同的字符串,怎么办呢?
•两两字符串都进行比较?时间复杂度太高
•把每个字符串hash成一个整数,然后把所有整数进行一个去重操作,即可知道答案了。
当遇到冲突时,我们可以想办法调整p和mod,使得冲突概率减小之又小。我们一般认为p和mod一般取素数,p取一个较大的素数即可(6位到8位),mod取一个大素数,比如1e9+7,或者1e9+9。

4.字符串判等

        System.out.printf(String.valueOf(String.valueOf("1")==String.valueOf("1")));   //true,底层直接调用的toString方法,两个字符串的hashcode一样
        System.out.printf(String.valueOf(String.valueOf(1)==String.valueOf(1)));       //false,底层toString方法是new出来的 而字符串的toString又是this地址引用
        System.out.printf(String.valueOf(Integer.valueOf(1)==Integer.valueOf(1)));     //true取得是value值而不是this引用
        System.out.printf(String.valueOf("a".toString()=="a".toString()));             //true toString方法没有新开辟空间
        System.out.printf(String.valueOf("true".toString()==new Boolean(true).toString()));             //true Boolean的toString获取的也是值

5.Apache和tomcat的区别

相同点:都是apache组织开发的;都有http服务的功能;都是开源免费的
不同点:Apache是普通http服务器,本身只支持html这种静态页面,不过可以通过插件支持php;
tomcat是可以支持jsp这种动态网页的;Apache是c语言实现的,支持各种特性和模块从而来扩展核心功能;
tomcat是java编写的,更好的支持servlet和jsp;Apache侧重于http服务;
tomcat侧重于servlet引擎;Apache可以运行一年不重启,稳定性非常好,而tomcat则较差;
Apche和tomcat的整合:
如果客户端请求的是静态页面,则只需要Apache服务器响应请求;
如果客户端请求动态页面,则是Tomcat服务器响应请求,将解析的JSP等网页代码解析后回传给Apache服务器,再经Apache返回给浏览器端。这是因为jsp是服务器端解释代码的,Tomcat只做动态代码解析,Apache回传解析好的静态代码,Apache+Tomcat这样整合就可以减少Tomcat的服务开销,静态网页Apache的处理效率比tomcat高,所以在同时存在静态网页和jsp的时候就需要这两者的整合了。

6.nginx和tomcat的区别

web上的server都叫web server,但是大家分工也有不同的。

nginx常用做静态内容服务和代理服务器(不是你FQ那个代理),直面外来请求转发给后面的应用服务
(tomcat,django什么的),tomcat更多用来做做一个应用容器,让java web app跑在里面的东西
,对应同级别的有jboss,jetty等东西。但是事无绝对,nginx也可以通过模块开发来提供应用功能,
tomcat也可以直接提供http服务,通常用在内网和不需要流控等小型服务的场景。

apache用的越来越少了,大体上和nginx功能重合的更多。
严格的来说,Apache/Nginx 应该叫做「HTTP Server」;而 Tomcat 则是一个「Application Server」
,或者更准确的来说,是一个「Servlet/JSP」应用的容器(Ruby/Python 等其他语言开发的应用也无法
直接运行在 Tomcat 上)。

一个 HTTP Server 关心的是 HTTP 协议层面的传输和访问控制,所以在 Apache/Nginx 上你可以看到
代理、负载均衡等功能。客户端通过 HTTP Server 访问服务器上存储的资源(HTML 文件、图片文件等
等)。通过 CGI 技术,也可以将处理过的内容通过 HTTP Server 分发,但是一个 HTTP Server 始终
只是把服务器上的文件如实的通过 HTTP 协议传输给客户端。

而应用服务器,则是一个应用执行的容器。它首先需要支持开发语言的 Runtime(对于 Tomcat 来说,
就是 Java),保证应用能够在应用服务器上正常运行。其次,需要支持应用相关的规范,例如类库、安
全方面的特性。对于 Tomcat 来说,就是需要提供 JSP/Sevlet 运行需要的标准类库、Interface 等。
为了方便,应用服务器往往也会集成 HTTP Server 的功能,但是不如专业的 HTTP Server 那么强大
,所以应用服务器往往是运行在 HTTP Server的背后,执行应用,将动态的内容转化为静态的内容之
后,通过 HTTP Server 分发到客户端。

7.定时任务

首先来说,定时任务执行时间的配置有两种:
1.cron表达式(绝对时间)
2.fixedRate(相对时间)
这里简单说一下上述的绝对时间意思是:
cron表达式中会涉及到绝对时间比如:每天凌晨2点执行 。那么它计算执行时间的方式是通过系统时间算出来
的,也就是说如果用cron表达式来配置的话那么你更改系统时间就会影响到你的定时任务,举个简单的例子:
比如第一次执行任务的时间为9:00,执行间隔为1小时一次;那么下次执行的时间应该是10:00而如果你修改系
统时间就可能会造成定时任务的失效;

而fixedRate相对时间的意思是:计算执行时间的方式是根据项目启动的时间来算出来的,是从项目启动时间
开始计算的。比如:第一次执行任务的时间为9:00,执行间隔为1小时一次;那么下次执行的时间应该是一个
小时以后。也就是说它是通过记录你上次任务执行了多久来判断下次任务的触发时机。它不关心你的系统时间
是多少。所以这种方式能够很好的解决我们更改系统时间的需求。

不过cron是可以指定到具体什么时候执行并且性能相对较高,fixedRate这种方法缺陷也很明显,就是每分钟都需要启动一次定时任务,看起来很呆。比较浪费资源。

8.RabbitMQ  中实现RPC 的机制是:

客户端发送请求(消息)时,在消息的属性(MessageProperties ,在AMQP 协议中定义了14中properties ,
这些属性会随着消息一起发送)中设置两个值replyTo (一个Queue 名称,用于告诉服务器处理完成后将通知我
的消息发送到这个Queue 中)和correlationId (此次请求的标识号,服务器处理完成后需要将此属性返还,客
户端将根据这个id了解哪条请求被成功执行了或执行失败)
服务器端收到消息并处理
服务器端处理完消息后,将生成一条应答消息到replyTo 指定的Queue ,同时带上correlationId 属性
客户端之前已订阅replyTo 指定的Queue ,从中收到服务器的应答消息后,根据其中的correlationId属性分析
哪条请求被执行了,根据执行结果进行后续业务处理

9.rabbitmq和activemq和kafka的区别

1.从社区活跃度
按照目前网络上的资料,RabbitMQ 、activeM 、ZeroMQ 三者中,综合来看,RabbitMQ 是首选。 

2.持久化消息比较
ZeroMq 不支持,ActiveMq 和RabbitMq 都支持。持久化消息主要是指我们机器在不可抗力因素等情况下挂掉
了,消息不会丢失的机制。

3.综合技术实现
可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统等等。

RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码
实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。

4.高并发
毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。activemq做集群则需
要zookeeper,是通过java中的jms实现的,性能较差不便于集群,但是本身是一个中间件,便于扩展到各种场景
上使用

5.比较关注的比较, RabbitMQ 和 Kafka
RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上,  RabbitMq  胜于  Kafka  (理论上)。

另外,Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个日志(消息)
系统一个重要组件,针对性很强,所以 如果业务方面还是建议选择 RabbitMq 。

还有就是,Kafka 的性能(吞吐量、TPS )比RabbitMq 要高出来很多。

10.常见单例模式

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton() ;
    private Singleton(){
    }
    public static Singleton getInstance() { 
        return  instance ; 
    } 
}

优点:在未调用getInstance() 之前,实例就已经创建了,天生线程安全

缺点:如果一直没有调用getInstance() , 但是已经创建了实例,造成了资源浪费。

懒汉式

public class Person {
    private static Person person ;
 
    private Person(){
         
    }
     
    public static Person get(){
        if ( person == null ) {
            person = new Person() ;
        }
        return person ;
    }
 
}

优点:get() 方法被调用的时候,才创建实例,节省资源。

缺点:线程不安全。

造成的原因:

线程A希望使用Person,调用get()方法。因为是第一次调用,A就发现 person 是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用 Person ,调用get()方法,同样检测到 person 是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个 Person 的对象——单例失败!

总结:1、可以实现单线程单例   2、多线单例无法保证

改进:1、加锁

synchronized线程安全的懒汉式

public class Person {
    private static Person person ;
 
    private Person(){
 
    }
 
    public synchronized static Person get(){
        if ( person == null ) {
            person = new Person() ;
        }
        return person ;
    }
}

优点:1、满足单线程的单例2、满足多线程的单例

缺点:1、性能差

改进性能的synchronized线程安全的懒汉式

public class Person {
    private static Person person ;
 
    private Person(){
    }
 
    public static Person get(){
        if ( person == null ) {
            synchronized ( Person.class ){
                if (person == null) {
                    person = new Person();
                }
            }
        }
        return person ;
    }
 
}

首先判断person 是不是为null,如果为null,加锁初始化;如果不为null,直接返回 person 。整个设计,进行了双重校验。

优点:1、满足单线程单例2、满足多线程单例3、性能问题得以优化

缺点:第一次加载时反应不快,由于java内存模型一些原因偶尔失败

volatitle关键字解决双重校验带来的弊端

public class Person {
 
    private static volatile Person person = null ;
 
 
    private Person(){
 
    }
 
    public static Person getInstance(){
        if ( person == null ) {
            synchronized ( Person.class ){
                if ( person == null ) {
                    person = new Person() ;
                }
            }
        }
 
        return person ;
    }
 
}

假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 person = new Person(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行 person 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 person 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整 (没有完成初始化)的 Person对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。

     补充:看了图片加载框架 Glide (3.7.0版) 源码,发现glide 也是使用volatile 关键字的双重校验实现的单例,可见这种方法是值得信赖的

静态内部类

public class Person {
 
    private Person(){
 
    }
 
    private static class PersonHolder{
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Person instance = new Person();
    }
 
 
    public static Person getInstance() {
        return PersonHolder.instance;
    }
 
}

优点:

1、资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法

枚举类保证线程安全

package com;
 
public enum Singleton {
    INSTANCE ;
 
    public void show(){
        // Do you need to do things
    }
}

测试方法

public class A1  {
     
    public static void main(String[] args) {
         
        for ( int i = 0 ;  i < 20 ; i ++ ) {
            new Thread( new Runnable() {
                 
                @Override
                public void run() {
                    System.out.println( Singleton.INSTANCE.hashCode() ) ;
                }
            }).start(); ;
             
        }
    }
}

11.mybatis一级缓存和二级缓存

一级缓存和二级缓存的主要区别在于一级缓存的作用域是sqlsession二级缓存的作用域是映射文件中的命名空间
,可能存在多个sqlsession引用同一个namespace,共同特点就是碰到相同的sql都会先从缓存获取,获取不到
才查询数据库,当对数据进行更新的时候会更新缓存,二级缓存是需要手动到映射文件配置的,默认是关闭的

12.如何保证消息消费时的幂等性

其实消息重复消费的主要原因在于回馈机制(RabbitMQ是ack,Kafka是offset),在某些场景中我们采用的回
馈机制不同,原因也不同,例如消费者消费完消息后回复ack, 但是刚消费完还没来得及提交系统就重启了,这
时候上来就pull消息的时候由于没有提交ack或者offset,消费的还是上条消息。
那么如何怎么来保证消息消费的幂等性(多次处理和一次处理效果是一样的)呢?实际上我们只要保证多条相同的数据过来的时候只处理一条或者说
多条处理和处理一条造成的结果相同即可,但是具体怎么做要根据业务需求来定,例如入库消息,先查一下消
息是否已经入库啊或者说搞个唯一约束啊什么的,还有一些是天生保证幂等性就根本不用去管,例如redis就是
天然幂等性。
还有一个问题,消费者消费消息的时候在某些场景下要放过消费不了的消息,遇到消费不了的消息通过日志记
录一下或者与应用程序绑定的存储写入再来处理,但是一定要放过消息,因为在某些场景下例如spring-rabbitmq的默
认回馈策略是出现异常就没有提交ack,导致了一直在重发那条消费异常的消息,而且一直还消费不了,这就尴
尬了,后果你会懂的。

一、丢失数据的应对方法
(一)RabbitMQ
(1)生产者弄丢了数据
  生产者将数据发送到RabbitMQ的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能。此时
可以选择用RabbitMQ提供的事务功能,就是生产者发送数据之前开启RabbitMQ事务(channel.txSelect),
然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会收到异常报错,此时就可以回滚事务
(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务
(channel.txCommit)。但是问题是,RabbitMQ事务机制一搞,基本上吞吐量会下来,因为太耗性能。
所以一般来说,如果你要确保说写RabbitMQ的消息别丢,可以开启confirm模式,在生产者那里设置开启
confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ会给你回
传一个ack消息,告诉你说这个消息ok了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你
这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一
定时间还没接收到这个消息的回调,那么你可以重发。
事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是
confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步
回调你一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用confirm机制的。
(2)RabbitMQ弄丢了数据
就是RabbitMQ自己弄丢了数据,这个你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪
怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,
RabbitMQ还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
设置持久化有两个步骤,第一个是创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化
queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,
就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才
行,RabbitMQ哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。
而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack
了,所以哪怕是在持久化到磁盘之前,RabbitMQ挂了,数据丢了,生产者收不到ack,你也是可以自己重发
的。
哪怕是你给RabbitMQ开启了持久化机制,也有一种可能,就是这个消息写到了RabbitMQ中,但是还没来得及
持久化到磁盘上,结果不巧,此时RabbitMQ挂了,就会导致内存里的一点点数据会丢失。
(3)消费端弄丢了数据
RabbitMQ如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那
么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。
这个时候得用RabbitMQ提供的ack机制,简单来说,就是你关闭RabbitMQ自动ack,可以通过一个api来调用就
行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有
ack?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息
是不会丢的。


二、如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小
时怎么解决?
(一)、大量消息在mq里积压了几个小时了还没解决
几千万条数据在MQ里积压了七八个小时,从下午4点多,积压到了晚上很晚,10点多,11点多
这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复consumer的问题,让他恢复
消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。
一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟是18万条,1000多万条,所以如果你积压了几百
万到上千万的数据,即使消费者恢复了,也需要大概1小时的时间才能恢复过来。
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉。
新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量。
然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,
直接均匀轮询写入临时建立好的10倍数量的queue。
接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据。
这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据。
等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息。
(二)、消息队列过期失效问题
假设你用的是rabbitmq,rabbitmq是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时
间就会被rabbitmq给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而
是大量的数据会直接搞丢。
这个情况下,就不是说要增加consumer消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可
以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就
直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。
这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面
去,把白天丢的数据给他补回来。也只能是这样了。
假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次。
(三)、消息队列满了怎么搞?
如果走的方式是消息积压在mq里,那么如果你很长时间都没处理掉,此时导致mq都快写满了,咋办?这个还有
别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,
都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

三、重复消费问题
1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作,然后另外开辟线程去读取这批数据进行业务处理。
3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
4.消息发送的时候先存储到redis或者其他第三方存储,发送的消息中只包含消息的唯一id,然后程序根据唯一id去查询数据然后消费,一旦消费就删除数据

13.翻墙原理

实际上就是你使用vpn连接到国外的一台服务器A上,你电脑的所有请求都会发送到这台服务器A,这台服务器A再
把你的请求转发到国外被墙的网站,响应内容先返回A,再由A返回给你。中间多了这么一层,只要服务器A没有被
墙,你就可以访问国外被墙的网站了。

自己搭建VPN的话就从国外购买一台服务器就好,不过买之前要确定国内是可以快速访问该服务器才行

14.volatile关键字如何保证内存可见性

在java并发编程中,一定绕不开volatile、synchronized和lock几个关键字,其中volatile关键字是用来解决共享变量(类成员变量、类的静态成员变量等)的可见性问题的,非共享变量(方法的局部变量)是分配在JVM虚拟机的栈中,是线程私有的,不涉及可见性问题。那么什么是可见性?
可见性:在JAVA规范中是这样定义的:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。通俗的将就是如果有一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值。那么可见性规范要求T2读取到的必须是T1修改后的值,而不能在T2读取旧值后T1修改为新值。volatile关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。那么底层实现是通过机制保证volatile变量读写可见的?
Volatile的实现机制:在说这个问题之前,我们先看看CPU是如何执行java代码的。


首先编译之后Java代码会被编译成字节码.class文件,在运行时会被加载到JVM中,JVM会将.class转换为具体的CPU执行指令,CPU加载这些指令逐条执行。

以多核CPU为例(两核),我们知道CPU的速度比内存要快得多,为了弥补这个性能差异,CPU内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非volatile变量当CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新会内存之前,由于是共享变量,那么CORE2中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。volatile关键字就会解决这个问题的,如何解决呢,首先被volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以lock为前缀的指令,当CPU发现这个指令时,立即做两件事:
1.将当前内核高速缓存行的数据立刻回写到内存;
2.使在其他内核里缓存了该内存地址的数据无效。
第一步很好理解,第二步如何做到呢?
MESI协议:在早期的CPU中,是通过在总线加LOCK#锁的方式实现的,但这种方式开销太大,所以Intel开发了缓存一致性协议,也就是MESI协议,该解决缓存一致性的思路是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。
以上这些就是volatile关键字的内部实现机制,使用Volatile有什么好处呢?
使用volatile的好处:从底层实现原理我们可以发现,volatile是一种非锁机制,这种机制可以避免锁机制引起的线程上下文切换和调度问题。因此,volatile的执行成本比synchronized更低。
volatile的不足:使用volatile关键字,可以保证可见性,但是却不能保证原子操作,例如:
public class TestVolatile {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(test.inc);
    }
}
这里我们用10个线程,每个线程+1000,预期应该是10000,实际上编译执行这段代码,输出值都会小于10000。为什么会这样?因为,自增操作并不是原子操作,它含括读,加1,写入工作内存三步操作。这三步是分开操作的,当inc的值为5,thead1执行自增操作,当thread1读到5之后,还没有来得及写入就被阻塞了,那么thead2读取的依然是原值。所以volatile的非锁机制只能保证修饰的变量的可见性,而当对变量进行非原子操作时,volatile就无法保证了。这种时候就需要使用synchronzied或lock。

15.Comparable和Comparable的区别


Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。
而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
我们不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器

16.kafka吞吐量大的原因

生产者(写入数据)

生产者(producer)是负责向Kafka提交数据的,我们先分析这一部分。
Kafka会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度Kafak采用了两个技术, 顺序写入 和 MMFile 。

顺序写入

因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨
厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。

上图就展示了Kafka是如何写入数据的, 每一个Partition其实都是一个文件 ,收到消息后Kafka会把数据插入
到文件末尾(虚框部分)。
这种方法有一个缺陷—— 没有办法删除数据 ,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个
消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。

上图中有两个消费者,Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个
Partition);Consumer2有一个offset对应Partition2。这个offset是由客户端SDK负责保存的,Kafka的
Broker完全无视这个东西的存在;一般情况下SDK会把它保存到zookeeper里面。(所以需要给Consumer提供
zookeeper的地址)。

如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件
大小。具体配置可以参看它的配置文档。

Memory Mapped Files

即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并 不是实时的写入硬盘 ,它充分
利用了现代操作系统 分页存储 来利用内存提高I/O效率。

Memory Mapped Files(后面简称mmap)也被翻译成 内存映射文件 ,在64位操作系统中一般可以表示20G的数据
文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的
操作会被同步到硬盘上(操作系统在适当的时候)。

通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。

使用这种方式可以获取很大的I/O提升, 省去了用户空间到内核空间 复制的开销(调用文件的read会把数据先放
到内核空间的内存中,然后再复制到用户空间的内存中。)也有一个很明显的缺陷——不可靠, 写到mmap中的数据
并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。 Kafka提供了一个
参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer
叫 同步 (sync);写入mmap之后立即返回Producer不调用flush叫 异步 (async)。

mmap其实是Linux中的一个函数就是用来实现内存映射的,谢谢Java NIO,它给我提供了一个mappedbytebuffer
类可以用来实现内存映射(所以是沾了Java的光才可以如此神速和Scala没关系!!)

消费者(读取数据)

Kafka使用磁盘文件还想快速?这是我看到Kafka之后的第一个疑问,ZeroMQ完全没有任何服务器节点,也不会使
用硬盘,按照道理说它应该比Kafka快。可是实际测试下来它的速度还是被Kafka“吊打”。 “一个用硬盘的比用内
存的快”,这绝对违反常识;如果这种事情发生说明——它作弊了。

没错,Kafka“作弊”。无论是 顺序写入 还是 mmap 其实都是作弊的准备工作。
如何提高Web Server静态文件的速度
仔细想一下,一个Web Server传送一个静态文件,如何优化?答案是zero copy。传统模式下我们从硬盘读取一
个文件是这样的

先复制到内核空间(read是系统调用,放到了DMA,所以用内核空间),然后复制到用户空间(1,2);从用户空间重新复制到内核空间(你用的socket是系统调用,所以它也有自己的内核空间),最后发送给网卡(3、4)。

Zero Copy中直接从内核空间(DMA的)到内核空间(Socket的),然后发送网卡。

这个技术非常普遍,The C10K problem 里面也有很详细的介绍,Nginx也是用的这种技术,稍微搜一下就能找到很多资料。

Java的NIO提供了FileChannle,它的transferTo、transferFrom方法就是Zero Copy。

Kafka是如何耍赖的

想到了吗?Kafka把所有的消息都存放在一个一个的文件中, 当消费者需要数据的时候Kafka直接把“文件”发送给
消费者 。这就是秘诀所在,比如: 10W的消息组合在一起是10MB的数据量,然后Kafka用类似于发文件的方式直
接扔出去了,如果消费者和生产者之间的网络非常好(只要网络稍微正常一点10MB根本不是事。。。家里上网都是
100Mbps的带宽了),10MB可能只需要1s。所以答案是——10W的TPS,Kafka每秒钟处理了10W条消息。

可能你说:不可能把整个文件发出去吧?里面还有一些不需要的消息呢?是的,Kafka作为一个“高级作弊分子”自
然要把作弊做的有逼格。Zero Copy对应的是sendfile这个函数(以Linux为例),这个函数接受

out_fd作为输出(一般及时socket的句柄)
in_fd作为输入文件句柄
off_t表示in_fd的偏移(从哪里开始读取)
size_t表示读取多少个

没错,Kafka是用mmap作为文件读写方式的,它就是一个文件句柄,所以直接把它传给sendfile;偏移也好解
决,用户会自己保持这个offset,每次请求都会发送这个offset。(还记得吗?放在zookeeper中的);数据量
更容易解决了,如果消费者想要更快,就全部扔给消费者。如果这样做一般情况下消费者肯定直接就被 压死了 ;
所以Kafka提供了的两种方式——Push,我全部扔给你了,你死了不管我的事情;Pull,好吧你告诉我你需要多少
个,我给你多少个。

总结

Kafka速度的秘诀在于,它把所有的消息都变成一个的文件。通过mmap提高I/O速度,写入数据的时候它是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。阿里的RocketMQ也是这种模式,只不过是用Java写的。

单纯的去测试MQ的速度没有任何意义,Kafka这种“暴力”、“流氓”、“无耻”的做法已经脱了MQ的底裤,更像是一个暴力的“数据传送器”。 所以对于一个MQ的评价只以速度论英雄,世界上没人能干的过Kafka,我们设计的时候不能听信网上的流言蜚语——“Kafka最快,大家都在用,所以我们的MQ用Kafka没错”。在这种思想的作用下,你可能根本不会关心“失败者”;而实际上可能这些“失败者”是更适合你业务的MQ。

17.mybatis$和#的区别

 1 #是将传入的值当做字符串的形式,eg:select id,name,age from student where id =#{id},当前端把id
值1,传入到后台的时候,就相当于 select id,name,age from student where id ='1'.
 2 $是将传入的数据直接显示生成sql语句,eg:select id,name,age from student where id =${id},当前
端把id值1,传入到后台的时候,就相当于 select id,name,age from student where id = 1.
 3 使用#可以很大程度上防止sql注入。(语句的拼接)
 4 但是如果使用在order by 中就需要使用 $.
 5 在大多数情况下还是经常使用#,但在不同情况下必须使用$. 

我觉得#与的区别最大在于:#{} 传入值时,sql解析时,参数是带引号的,而的区别最大在于:#{} 传入值
时,sql解析时,参数是带引号的,而{}穿入值,sql解析时,参数是不带引号的。

一 : 理解mybatis中 $与#
    在mybatis中的$与#都是在sql中动态的传入参数。
    eg:select id,name,age from student where name=#{name}  这个name是动态的,可变的。当你传
入什么样的值,就会根据你传入的值执行sql语句。
二:使用$与#
   #{}: 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个
参数占位符 。
   ${}: 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。
  name-->cy
 eg:  select id,name,age from student where name=#{name}   -- name='cy'
      select id,name,age from student where name=${name}    -- name=cy

18、 redis为什么是单线程的

1、先说一下为什么出现进程,线程

进程:在计算机发明之初就发现,在输入数据时(I/O速度慢),CPU是空闲的,这样就浪费了CPU资源,为了
充分利用CPU资源,发明了进程,在输入程序A的数据时,程序B在占用CPU资源进行计算。
线程:为了减少进程的上下文切换的损耗,满足人机交互的实时性,同时保留进程充分利用CPU资源的优点,出
现了线程。

2,redis为什么不用多线程(不划算呗)

(1)纯内存操作;
(2)多线程仍然会有上下文切换的损耗,虽然比进程切换损耗小;
(3)采用了非阻塞I/O多路复用机制
题外话:我们现在要仔细的说一说I/O多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么意思。博主打一个比方:小曲在S城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。
经营方式一
客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方
式存在下述问题几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁
就能去送快递随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了快递员之间的协调很花时间
综合上述缺点,小曲痛定思痛,提出了下面的经营方式

经营方式二
小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。

对比
上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢。在上述比喻中:

每个快递员------------------>每个线程
每个快递-------------------->每个socket(I/O流)
快递的送达地点-------------->socket的不同状态
客户送快递请求-------------->来自客户端的请求
小曲的经营方式-------------->服务端运行的代码
一辆车---------------------->CPU的核数
于是我们有如下结论
1、经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。
2、经营方式二就是I/O多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。

19、gc中新生代和老年代回收算法

 复制算法是针对Java堆中的新生代内存垃圾回收所使用的回收策略,解决了”标记-清理”的效率问题。
 复制算法将堆中可用的新生代内存按容量划分成大小相等的两块内存区域,每次只使用其中的一块区域。当其中
一块内存区域需要进行垃圾回收时,会将此区域内还存活着的对象复制到另一块上面,然后再把此内存区域一次性
清理掉。发生在新生代的垃圾回收成为Minor GC,Minor GC又称为新生代GC,因为新生代对象大多都具备朝生夕
灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快

复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以在栈的老年代不适用复制算法。
针对老年代对象存活率高的特点,提出了一种称之为”标记-整理算法”发生在老年代的GC称为Full GC,又称为
Major GC,其经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full 
GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上

20.新生代内存划分


新生代中98%的对象都是”朝生夕死”的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内
存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块
Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。
当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden
和刚才用过的Survivor空间。
当Survivor空间不够用时,则需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 
8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
 1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发
Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制
到To区域,并将Eden和From区域清空。
 2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden
和To区域清空。
 3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参
数默认是15),最终如果还是存活,就存入到老年代。

对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(前面篇章中有介绍过Minor GC)。但也有一种情况,在内存担保机制下,无法安置的对象会直接进到老年代。

大对象直接进入老年代
大对象时指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的就是避免在Eden区及两个Survivor区之间发生大量的内存复制。

长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1 。对象在Survivor区中没经过一次Minor GC,年龄就加1岁,当年龄达到15岁(默认值),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

接下来我们来回答JVM的分代年龄为什么是15?而不是16,20之类的呢?
真的不是为什么不能是其它数(除了15),着实是臣妾做不到啊!

事情是这样的,HotSpot虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark word”。

例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0 。

明白是什么原因了吗?对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可能为16,20之类的了。

动态对象年龄判定
为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求兑现过的年龄必须达到了MaxTenuringThreshold才能晋升老年代。

满足如下条件之一,对象能晋升老年代:
1.对象的年龄达到了MaxTenuringThreshold(默认15)能晋升老年代。

2.如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

很多文章都只是注意到了上面描述的情况(包括阿里中间件公众号发的一篇文章里也只是这么简单的介绍,当时给它们后台留过言说明情况),但如果只是这么认识的话,会发现在实际的内存回收中有悖于此条规定。

举个小栗子,如对象年龄5的占34%,年龄6的占36%,年龄7的占30%,按那两个标准,对象是不能进入老年代的,但Survivor都已经100%了啊?

大家可以关注这个参数TargetSurvivorRatio,目标存活率,默认为50%。大致意思就是说年龄从小到大累加,如加入某个年龄段(如栗子中的年龄6)后,总占用超过Survivor空间*TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即栗子中的年龄6,7对象)。动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。

空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC 。

上面说的风险是什么呢?我们知道,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
 

21.java时间转换问题

java日期工具类SimpleDateFormat 将字符串转成Date对象的时候,如果字符串的精确度不够,但是pattern格式比较高的情况下,会导致,转换出的时间不具备幂等性,就是说每次转换出来的不一样,这一点需要注意


例如:
    public static Date parseTime(String ldapTime) {
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        Date date ;
        try {
            date = sdf2.parse(ldapTime);
        } catch (Exception e) {
            date= new Date();
        }
        return date;
    }
		
        System.out.println(sdf2.format(parseTime("2019-07-12T16:40:02+08:00")));

22.创建对象的四种方式

        //new对象
        ClassInstance ci01 = new ClassInstance("01"); 
        ci01.fun();

        //反射机制
        ClassInstance ci02 = (ClassInstance) Class.forName("com.xagy.lianxi.ClassInstance").newInstance();
        ci02.fun();

       //序列化后的克隆
       ClassInstance ci03 = (ClassInstance) ci01.clone();
       ci03.fun();

       //二进制流
       FileOutputStream fos = new FileOutputStream("D:\\ci.txt");
       ObjectOutputStream oos = new ObjectOutputStream(fos);
       oos.writeObject(ci01);
       oos.close();
       fos.close();
       FileInputStream fis = new FileInputStream("D:\\ci.txt");
       ObjectInputStream ois = new ObjectInputStream(fis);
       ClassInstance ci04  = (ClassInstance) ois.readObject();
       ois.close();
       fis.close();
       ci04.fun();

23.字符串为什么是final类型的

  因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因
为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现
,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

    如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入
来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,
所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

    因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程
安全问题而使用同步。字符串自己便是线程安全的。

    因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适
合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

alexander137

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值