JMM
java内存模型,规定了所有的变量都存储在主内存,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
程序计数器:保存正在执行的线程的字节码指令地址
本地方法区:为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
方法区: 存储类的元数据的,如虚拟机加载的类信息、静态变量、常量、即时编译后的代码。JDK8之前是永久代,JDK8使用的元空间实现,元空间不在共用JVM内存,而是使用的系统内存
栈(后进先出): 存储局部变量表、操作栈、动态链接、方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩 展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
堆(先进先出): 存储着各类生成的对象、数组等,JVM8中把运行时常量池、静态变量也移到堆区进行存储。堆区被细化可以分为年轻代(1/3)、老年代(2/3),而年轻代又可分为Eden区、From Survivor、To Survivor三个区域,比例是8:1:1;内存不足时会抛出OutOfMemoryError异常.
GC算法及回收机制
GC垃圾判断:
引用计数法 : 循环引用问题
可达性分析算法 : 在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
垃圾回收算法:
标记清除算法:产生内存碎片化
复制算法: 浪费空间
标记整理算法 :
分代回收算法:不同生命周期采取不同算法, 新生代复制算法,老年代标记整理算法,分散回收,减少一次GC所有停顿的时间。
垃圾收集器:
SerialGC: 串行GC, 单线程, 复制算法, Client模式下默认新生代GC
ParNew: 并行GC, 多线程, 复制算法, Server模式下默认新生代GC 1.7默认
ParallelScavenge: 多线程, 复制算法, 具有自适应调节策略(优于ParNew) 1.8默认
Serial Old: SerialGC老年代版, 单线程, 标记整理算法
Parallel Old: ParallelScavenge老年代版, 多线程, 标记整理 1.8默认
CMS: Concurrent mark sweep, 多线程, 标记清除, 执行过程: 初始标记(STW)->并发标记->重新标记(STW)->并发清除->线程重置
G1: 多线程, 标记整理算法, 划分内存几个独立区域收集, 优先回收垃圾最多的区域 1.9默认
ZGC: jdk11提供, 着色指针和读屏障, 多层堆和压缩
MinorGC(复制->清空->互换)
Eden区: Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);
然后,清空Eden和ServicorFrom中的对象;
最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。当老年代不足时 OOM异常。
问题:在Minor GC时,存在新生代中没有被其它新生代对象引用,但是被老年代的对象引用了的对象,这种情况是怎么处理的?
比如,在新生代内存区域有一个Remembered Set,里面记录了引用了新生代对象的老年代对象。在进行Minor GC的时候,GC Roots会把Remembered Set中的对象也加进去搜索,这样既不会有遗漏,也不需要全表扫描。
再比如,CMS的remark前新加一次youngGC,把新生代对象先清理一遍,这样扫描的对象就少了,可以有效减少fullGC的时间
再比如,CMS引入了卡表,为老年代的内存空间按照每512B建立卡表,如果一张卡中的对象引用了新生代对象,那么这张卡被标记出来,最后minor gc的时间,除了扫描新生代对象,还扫描卡表中标记的对象。
四种引用类型
强引用:把一个对象赋给一个引用变量。
弱引用:要用SoftReference类来实现,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
虚引用:用WeakReference类来实现,垃圾回收机制运行就会被回收
元引用:PhantomReference类来实现,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
类加载机制
双亲委派加载,收到加载请求,先委托父类加载,父类无法加载,子类再去尝试加载。保证了使用不同的类加载器最终得到的都是同样一个Object对象。
加载:将.class文件加载内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口
验证: 文件格式验证, 元数据验证, 字节码验证, 符号引用验证
准备: 静态变量在方法区分配内存,并设置默认初始值
解析: 将常量池内的符号引用替换为直接引用的过程
初始化:执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并的
使用:
卸载:
![](https://img2018.cnblogs.com/blog/1694759/201906/1694759-20190604174159355-1613904074.png)
JVM调优案例:
CMS调优案列: https://www.toutiao.com/i6898566341914149390
G1调优参数: https://blog.csdn.net/qq_27529917/article/details/86664677
JVM 调优原则是减少GC停顿STW时间,CMS 在初始标记与重新标记 会产生STW时间
CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中,
第一种条件,UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
第二种条件是用户调用了System.gc(),而且DisableExplicitGC没有开启。
第三种条件是young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
OOM原因及排查
主要分为三种:
1. OutOfMemoryError: PermGen space(Metaspace)设置大点
2. OutOfMemoryError: Java heap space
3. OutOfMemoryError:unable to create new native thread
生成进程的内存镜像dump文件,例如: jconsole、jvisualvm、jmap 等JDK自带工具分析,分析日志中大内存对象原因。
问题描述:银联绑卡接口,签名接口中使用BouncyCastleProvider,每次都是new一个。jdk底层JceSecurity这类有个静态Map属性verifyingProviders,这个map每次判断provider是否相等,不相等则放进map中,导致OOM。
解决方案:将BouncyCastleProvider放入成员变量中,初始化一次,后续方法直接引用。
Tomcat类加载机制
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
每个容器可以部署多个应用,不同应用Jar依赖不一样,这样做可以保证每个应用程序类库独立不相互依赖。web容器支持JSP文件修改,每次修改不需要重启,每一个JSP文件对应一个Jsp类加载器。
HashMap
数组+链表/红黑树, 链表长度大于8(且数组长度大于64)时, 链表树化(扩容时, 桶元素小于6, 树退化为链表)
先通过hash值找到桶, 再遍历数据等值比较
初始容量16,扩容因子0.75时,, 数组长度为2的倍数, 数据迁移
并发情况有Infinite Loop 无限循环问题
transient 修饰的字段不会被序列化
HashTable 初始化容量是11
hashMap 扩容大小为什么是2的幂: (n - 1) & hash算法,如果n不为2的幂次方,易产生空间浪费,(n - 1) & hash 约等于 hash % n ,2进制运算远高于取模。
红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次(插入2次)旋转之内解决。这一点是AVL所不具备的 。
ConcurrentHashMap
ConcurrentHashMap 什么时候加锁
JDK7版本:
数组+链表的方式。 Segment 数组(默认16),Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。时间复杂度取决于链表的长度,为 O(n)。
size() 先不加锁计算两次,如果前两次结果不一样再加锁计算一次。
JDK8版本:
数组+链表+红黑树。
当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
put方法,发现当前正在扩容时,其他线程put方法将协助扩容。
size() 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 CounterCell 数组得出 size。
baseCount是无并发时CAS增加的数量, CounterCell数组大小与CPU数量有关,并发时数据写入。
TreeMap
实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器。
在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
Fork/Join框架
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:
1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
在Java的Fork/Join框架中,使用两个类完成上述操作
1.ForkJoinTask:我们要使用Fork/Join框架,首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
a.RecursiveAction:用于没有返回结果的任务
b.RecursiveTask:用于有返回结果的任务
2.ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行
任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)。
Fork/Join框架的实现原理
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool,而ForkJoinWorkerThread负责执行这些任务。
Java创建对象有哪几种
new、工厂模式、反射和克隆
手写动态代理
Jetty 与Tomcat的 区别
1.架构比较
jetty架构是基于Handler来实现的,主要的扩展功能都可以用Handler来实现,扩展简单
tomcat的框架是基于容量设计的,进行扩展是需要了解tomcat的整体设计结构,不易扩展
2.性能比较
jetty和tomcat性能方面差异不大
jetty可以同时处理大量链接而且可以长时间保持链接,适合于javaWeb聊天应用
jetty默认采用NIO结束来处理I/o请求上更占优势,在处理静态资源时,性能较高
tomcat适合处理少数非常繁忙的连接,也就是连接生命周期短的话,tomcat的总体性能更高
tomcat默认采用B/o处理I/o请求,在处理静态资源时,性能较差
重定向和请求转发的区别
1、重定向是两次请求,转发是一次请求,因此转发的速度要快于重定向
2、重定向之后地址栏上的地址会发生变化,变化成第二次请求的地址,转发之后地址栏上的地址不会变化,还是第一次请求的地址
3、转发是服务器行为,重定向是客户端行为。重定向时浏览器上的网址改变 ,转发是浏览器上的网址不变
4、重定向是两次request,转发只有一次请求
5、重定向时的网址可以是任何网址,转发的网址必须是本站点的网址