1、JVM的内存模型
(1)虚拟机栈
虚拟机栈是程序方法的存储模型,该系统中所有方法的调用都是存储在虚拟机栈中,每一个方法的调用都是以栈帧的形式存放在虚拟机栈。
栈帧是由局部变量表、操作数栈、动态链接、方法出口组成,其中使用到最多的就是操作数栈和局部变量表。
public class App(){
public int add(){
int a = 10;
int b = 20;
int c = (a + b) * 100;
return c;
}
}
public static void main(String[] args){
App app = new App();
int result = app.add();
System.out.println(result);
}
当创建APP对象之后,调用add方法,就会执行add方法中的语句。
int a = 10; 首先是将整型数字10压入操作数栈,然后将10存入局部变量表,将操作数栈的10出栈;
int b = 20; 操作一致。
程序运行到第3行,首先计算(a + b),那么会将局部变量表的两个整型数压入操作数栈,执行加法操作,然后将结果重新压入操作数栈;
后续 * 100,属于带符号的整型,会将其压入操作数栈,执行乘法操作,然后在将结果 c = 3000压入局部变量表,所以局部变量表就是来保存这些数据引用,而操作数栈主要是来做算术操作。
执行完之后,从方法出口中出来,进入main函数,然后执行打印工作,相当于从一个方法到了另一个方法,这就是方法出口的作用。
(2)堆
堆主要说的就是分代垃圾回收,为什么要分代垃圾回收?为了更少的STW(只有在老年代Full GC时才会STW)。
如何对JVM调优,让其几乎不发生Full GC,减少STW?
如上图,堆内存中新生代和老年代的比例为1:2,假设新生代1G,老年代2G,那么Eden区800M,S0区100M,S1区100M;
调优前:假设当前线程每秒创建60M的对象,那么13s后,Eden区就会装满,开启Minor GC,假设前12s的对象均不可达被回收,那么还剩下60M的对象就得放到S0区,Age + 1;但是在JVM中,超过S区内存50%的对象将会被直接放入老年代,所以60M的对象不会放入S0区,会直接放入老年代,如此一来,Full GC不可避免。
所以在JVM调优的时候,可以设置新生代和老年代的比例,之前默认是1:2,那么现在设置为2:1,那么Eden去就是1.6G,S0是200M,S1区200M,老年代1G;
调优后:还是之前的线程,此次在25s后Eden区填满,Minor GC后,因此60M的空间不超过200M的50%,因此放入了S0区,在S0和S1交换时采用复制算法,Age不断的+1,直到Age = 15时,才会放入老年代,这种情况下Full GC触发的频率就非常低。
2、线程池
线程池有一个顶级接口Executor,实现类Executors,可以创建多种类型的线程,例如:
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable(){
//执行方法
});
使用Executors接口实现的线程池(建议不要使用),内部底层的实现其实是ThreadPoolExecutor
,当执行Executors.newCachedThreadPool()
时,其实就是给ThreadPoolExecutor
各参数赋值。
当参数赋值完成之后,就调用execute方法,开启线程执行任务,在执行时分为4种情况:
(1)新建的任务在核心线程数空闲时,创建Worker对象调用addWorker将线程放入核心线程执行run方法;
(2)当核心线程数满了的时候 ,就将任务放到WorkQueue任务队列当中,然后可以从队列中拿到(get) Task任务后,执行run方法;
(3)当WorkQueue队列也满了的时候,就将线程放入非核心线程池(maxiumPoolSize);
(4)等到所有的任务队列都满了的时候,当再次加入任务的时候,就会调用线程拒绝策略,不再接收异步任务。
3、Volatile的底层原理
Volatile底层是通过MESI缓存一致性原理以及总线嗅探机制实现。
当线程1读取主内存的变量到工作内存中时,修改共享变量,然后重新写入到主内存中,其他线程因为总线嗅探机制,会去向主内存再次获取最新的数据,但是这个时候线程1会对主内存lock加锁,这也是防止其他线程读取到还未更新的数据,等到线程1将数据更新到主内存后,unlock,保证其他线程能获取最新的已更新数据。
4、HashMap的其他小知识点
(1)为什么负载因子是0.75f
如果是1.0f,也就是说要在加入16个元素的时候,才会去扩容,那么这个时候,可能会存在hash冲突,在扩容时,因为需要rehash,将之前的数据全部复制过去,影响效率;如果是0.5f,那会浪费空间。
(2)为什么数组大小要是2的整数次幂
在定位tab位置的时候,是通过位运算实现的,这种方式和取模一致,所以为了保证在位运算得到的table位置在0-15之间,必须在2次幂的前提下,否则可能会出现数组下标越界。
(3)为什么选择位运算
使用位运算的主要原因就是,在扩容的时候,需要将原数组复制到新数组,需要不断的rehash,如果在数据量达到万甚至10万级的数据的时候,位运算就为了提高效率。
(4)jdk 1.8什么情况下会转红黑树?
当数组长度不超过64(MIN_TREEIFY_CAPACITY
)的时候,是不会转的,只会数组扩容;当数组的长度超过64的时候,如果链表长度超过8的时候(总长度为9),会将链表转换为红黑树。
5、常量池
(1)静态常量池
静态常量池其实就是一个静态文件,当Java代码被编译成字节码后,会将这些类信息保存到常量池,不仅包含字符串、数字这些字面量,还包括类、方法信息,占据class文件的大部分。
(2)运行时常量池
属于方法区的一部分,是一块内存区域;运行时常量池在运行时将符号引用转换为直接引用,会将描述引用对象的符号变为直接指向引用对象内存地址的直接引用。
(3)字符串常量池
是运行时常量池的一部分,在jdk1.6之前是放在永久代,在jdk 1.7之后,就是放在java堆中,jdk1.8之后,出现了元空间,运行时常量池放在了元空间。
(4)包装类对象池
包装类对象池是保存-128到127的整数的常量池;