1、Java内存区域
线程共享:
方法区:存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
堆:负责几乎所有的对象实例的创建和存放实例
线程私有:
虚拟机栈:Java方法执行的时候会压栈,产生栈帧,栈帧中存放的主要是局部变量表,操作数栈,动态链接,方法出口
本地方法栈:不同于虚拟机栈,处理的是Native方法(HotSpot已经将两者合二为一)
程序计数器:记录当前线程的执行位置
2、关于对象gc的时候几种影响回收的引用:
强引用:只有不用了才会被回收
软引用:内存不够用了才回收(即使被使用)
弱引用:下一次gc的时候一定会被回收
虚引用:无法影响GC
3、调用对象的finalize方法一定会把对象GC吗?
不一定:在finalize中gc之前将其赋值就不会gc,有一次抢救机会,finalize只会回收一次
4、垃圾回收的算法
1、标记清除算法:把可回收的对象标记起来,然后把对象回收,但是会产生很多碎片化的内存空间。所以基本不用。
2、复制算法:将内存分为两半:使用的内存和未使用的内存。将可用内存分为两部分,当对象可以回收的时候将存活对象复制到其中的一部分中去,然后把使用的内存宣布释放掉,解决了碎片化的问题。但是存在两个问题:1、内存利用率低(因为被分成了两半);2、当存活对象比较多的时候复制会消耗大量的时间
3、标记整理算法:将存活对象移到内存的一端,把另一端的对象全部回收,可以解决碎片化的问题和效率问题(内存分配的时候连续分配)
5、堆的三代
老年代:年龄大,一直被用,被回收的概率小(标记整理算法)
永久代(JDK1.7,1.8中不再堆中而是放到了磁盘中,因此更加不用关注内存回收):方法区静态变量,基本不太会回收,线程共享,被使用的概率大(基本不会回收)
年轻代:被回收的概率大(复制算法),根据躲过gc的次数进行进化,而且还有问题,当servivor中的相同年龄的对象大小达到servivor空间的一半,大于或者等于该年龄的对象就可以直接进入老年代
6、垃圾收集器
Serial:单线程收集,停止所有运行程序然后收集,收集完了再运行。
ParNew:多线程收集
Parallel Scavenge:JDK7和JDK8默认的,关注点是吞吐量,目标是达到一个可控的吞吐量
=============分界线 上面为新生代,下面是老年代=========
CMS:
特点:
获取最短停顿时间为目的,比较特别,采用标记清除算法,
过程:
初始标记:仅仅是标记一下GCRoot能直接关联到的对象。
并发标记:并发标记是进行GCRoot Tracing的过程
重新标记:重新标记是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分的标记对象。
并发清除:多线程回收
其中并发的两个过程可以和用户线程一起工作,其他两个过程都是stop the world。
缺点:并发处理会影响用户线程的速度、无法处理浮动垃圾(即标记之后重新出现的垃圾)、空间碎片过多(标记清除算法的缺点)
Serial Old:
Parallel Old:老年代默认
=============下面是最新收集器========================
G1:
特点:
并发与并行:充分利用多CPU,多核环境下的优势,使用多个CPU来缩短STW的时间
分代收集:采取不同方式对待处于不同时期的对象
空间整合:整体上像是标记整理算法,局部上很像复制算法
可预测的停顿:虽然降低停顿时间是CMS和G1共同追求的目的,但是G1除了降低停顿时间之外,还能建立可预测的停顿时间模型。能让使用者指定在M毫秒中,最多允许N毫秒进行垃圾收集
问题:
将内存划分为一个个的region的分区之后,怎么判断一个对象是不是被引用了,因为对象不是在一个region中才有引用关系的,每次进行全堆的扫描的话肯定不合适?
解决方案:
在G1收集器中,region之间对象的引用以及其他的收集器中的新生代与老年代之间的对象引用,虚拟机都市使用Remember Set来避免全堆扫描的。G1中的每个region都有一个与之对应的remember set,虚拟机发现程序在对reference类型的数据进行写操作的时候,会产生一个write barrier暂时中断写操作,检查reference引用的对象是否处于不同的region之中,如果是,便通过cardtable把相关引用信息记录到被引用对象所属的region的remember set中。进行回收的时候在gc根节点的枚举范围内加入remembered set即可保证不对全堆扫描也不会泄露
过程:
初始标记:同CMS
并发标记:同CMS
最终标记:修复在并发标记阶段因用户线程继续运作导致标记发生改变的那部分标记记录,虚拟机会把这段时间内对象变化记录在Remembered Set Logs里面,最终标记阶段把数据河道Remembered Set中,这阶段需要停顿线程,但是可并行执行。
筛选回收:先对各Region的回收价值和成本进行排序,根据用户期望的GC时间来制定计划
7、两类类加载
1、启动类加载器:c++实现主要负责将jdk的lib目录下的类加载到内存中
2、其他类加载器:
1、扩展类加载器:负责加载jdk的lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库
2、应用程序类加载器:加载用户类路径的指定类库
双亲委派模型:如果类加载器收到类加载的请求,他首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器,只有当父加载器在自己的搜索范围内找不到制定类的时候子加载器才会自己尝试去加载。
8、JMM java内存模型
线程对主内存中的共享变量进行操作的时候,会先拷贝一份到自己的内存中去生成副本,然后对副本进行操作,操作完了再放回去。
jmm中的八种原子操作:
lock:作用于主内存的变量,把一个变量标识为一条线程独有的状态
unlock:作用于主内存的变量,把一个处于锁状态的变量释放
read:作用于主内存的变量,把一个变量从主内存传输到线程的工作内存中去,供load使用
load:作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的副本中
use:作用于工作内存的变量,工作内存中一个变量的值传递给执行引擎
assign:作用于工作内存的变量,将一个执行引擎接收到的值赋给工作内存的变量
store:作用于工作内存的变量,把一个工作内存的变量的值传送到主内存,供后面的write使用
write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放到主内存的变量中
happens-before八大原则:
程序次序原则:同一线程中,书写在前面的操作先行发生于书写在后面的操作
管制锁定原则:一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量原则:一个volatile变量的写操作先行发生于后面对这个变量的读操作
线程启动原则:线程的start方法先行发生于其他的方法
线程终止原则:线程中所有操作都先行发生于对线程的终止检测
线程中断原则:对线程的interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
9、锁
自旋锁:一个线程将共享资源锁起来的话,后面的线程访问的时候,后面的线程就等待,不阻塞(线程调度消耗资源)。
锁消除:代码级别的优化,如果有没必要的锁的话(比如方法的私有变量),虚拟机会自动将锁消除。
锁粗化:个别情况下频繁的细化锁会造成不必要的性能损耗,因此虚拟机就回把锁粗化,只需加锁一次即可
轻量级锁:利用cas操作降低开销,线程在当前的栈帧中开辟一个名为锁记录(Lock Record)的空间,用于存放锁对象的mark word的拷贝。然后,虚拟机使用cas操作将锁对象的mark word指向lock record。如果更新成功,就表示这个线程拿到了该对象的锁。同时将mark word的标志位转变为00.如果更新失败的话,就检查对象的mark word是否指向当前线程的栈帧,如果是的话,就表示当前线程已经有了该对象的锁,那就可以直接进入同步快继续操作,否则说明对象已经被其他的线程抢占了,如果有两个以上的线程争夺轻量级锁,那轻量级锁就不再有效,膨胀为重量级锁。解锁过程同样使用cas的方式,如果对象的mark word仍然指向线程的锁记录,那就用cas操作把对象当前的mark word和线程中开呗的mark word(displaced mark word)替换回来。如果替换成功的话同步就完成了,如果失败,就表示其他的线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
偏向锁:当锁第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01,即偏向模式。同时使用cas操作把获取这个锁的线程ID记录在对象的mark word中,如果cas操作成功,持有偏向锁的线程以后每次进入与这个锁相关的同步块的时候,虚拟机都不进行同步操作,当其他线程来尝试获取锁的时候,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或者轻量级锁的状态。
10、string.intern()方法
1.6中的intern()方法会把首次出现的字符串复制到永久代中,然后返回引用,stringbuilder()的对象是在堆中的,因此返回两个false
1.7中的intern()方法不会复制,而是在常量池中记录首次出现的引用,因此intern返回的是stringbuilder创建的对象实例,但是当字符串是java等的时候,常量池中已经有引用了,因此是false
11、判断对象是否已死的两种方法
引用计数算法:给对象添加计数器,引用即加一,引用失效即减一,为零表示不可能再被使用(无法解决相互后引用的情况)
可达性分析算法:通过一系列称为GCRoot的对象作为起始点,从该点向下搜索,搜索走过的路径成为引用链,当一个对象到GCRoot当没任何引用链引用时,即不可达,证明对象不可用,可以被当作GCRoot的点(几乎所有对象),虚拟机栈(栈帧中的本地变量表中)引用的对象,本地方法栈中的引用对象,方法区中常量引用的对象,方法区中静态属性引用的对象。
12、java的类加载过程
一共分为五个阶段,但是验证、准备和解析统称为连接。最后两个是类的生命周期,不算在加载之前
加载(三步):
1、通过一个类的全限定名来获取此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个一个代表这个类的java.lang.Class对象
验证(四步):
1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
准备:
为类变量(被static修饰的变量)分配内存并设置类变量初始值(例:public static int value = 123; 在这一阶段后value的值是0,但是 public static final int value = 123;在这一阶段之后value的值就是123)
解析:
完成从符号引用到直接引用的转换工作,解析不一定在初始化之前,也可能在初始化之后。
初始化:
类加载的最后一步,除了在加载阶段用户程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
使用:
卸载:
13、类加载机制
虚拟机把描述类的数据从class文件加载到内存,并对数据进行验证,解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
14、为什么两块Survivor区
因为新生代的划分是两块Survivor区,一块Eden,每次先用其中的Eden和一块Survivor,原因是新生代的对象大部分都是朝生夕亡的。当然,如果Survivor中的空间不够,会由老年代区分担
15、MinorGC和FullGC
MinorGC:当Eden区满的时候,就执行MinorGC
FullGC:
调用System.gc的时候,系统建议执行FullGC,但是不是必须的
老年代空间不足的时候
通过MinorGC之后进入老年代的平均大小大于老年代的剩余对象的时候
堆中分配很大的对象但是老年代没足够的空间的时候
16、判断对象是否被引用(是否存活)
引用计数法:给对象添加一个计数器,然后当对象被引用的时候,计数器的值就加一,取消一个引用就减一,当计数器被减为0的时候,标志着对象已经没有引用了
可达性分析法:通过一系列被称为GCRoot的对象作为起点,然后向下搜索,搜索过的路径成为引用链,当一个对象到GCRoot没有任何引用链相连的时候,表示该对象不可达,也就是说该对象是不可用的