简述下双亲委派原则?
答:
1.概念: 如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
代码实现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步上锁
synchronized (getClassLoadingLock(name)) {
// 先查看这个类是不是已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
// 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果还是没有获得该类,调用findClass找到类
long t1 = System.nanoTime();
c = findClass(name);
// jvm统计
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 连接类
if (resolve) {
resolveClass(c);
}
return c;
}
}
2. 需要的原因:
(1):如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。
(2):防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
(3):防止官方自定义的类被覆盖,因为在查看父类已经加载了某些类,那么就不会再次加载
3. 具体的加载器: jvm提供了三种系统加载器:
- 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载<JAVA_HOME>/lib下的类。
- 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载<JAVA_HOME>/lib/ext下的类。
- 系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
4. 破坏双亲委派原则: (1)原因: 我们需要加载自定义的类,比如下列例子
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
- 比如JNDI,JDBC,DriverManager是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有Driver的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
- 比如Tomcat:我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
oom和栈溢出分析?
答:
Java 虚拟机栈用来描述 Java 方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被回收。
栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。
栈存的是分配给一个线程使用的,而堆是整个虚拟机使用的,这两个异常都是因为存的东西太多溢出了产生的异常,那么首先我们要分析他里面存的是什么,然后才能分析原因,如下。调整参数大小是最后步骤,我们要先排查是否有错
虚拟机栈会产生两类异常:
StackOverflowError: 线程请求的栈深度大于虚拟机允许的深度抛出。
- 原因: JVM 线程栈存储了方法的执行过程、基本数据类型、局部变量、对象指针和返回值等信息,这些都需要消耗内存。一旦线程栈的大小增长超过了允许的内存限制,就会抛出 java.lang.StackOverflowError 错误。一般是因为递归错误,导致一直递归调用方法,如果是因为实际应用的话,可以通过调大JVM参数
OutOfMemoryError: 如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。
-
原因:
1. java.lang.OutOfMemoryError:Java heap space(java堆内存溢出) 发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。堆大小可以通过虚拟机参数-Xms,-Xmx等修改(堆区域用来存放Class的实例(即对象)这里是new出来的)。
2. java.lang.OutOfMemoryError: PermGen space(Java永久代(元数据)溢出,即方法区溢出了)------>发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。(方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。这里是编码写的Class)
3. OutOfMemoryError:unable to create new native thread 这种错误在Java线程个数很多的情况下容易发生。
引用的几种方式?
答:
1. 强引用: 强引用是在程序代码之中普遍存在的引用赋值,类似Object o = new Object()这种引用关系。无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
2. 软引用: 用来描述一些还有用但非必须的对象。只被软引用关联的对象,在系统要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收,如果这次回收还是没有足够的内存,才会抛出溢出异常。应用场景:做缓存(浏览器的后退按钮)
3. 弱引用: 也是用来描述那些非必须对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
4. 虚引用: 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
jvm内存区域?
答:
1、程序计数器: 执行字节码的行号指示器,线程私有,没有OOM
2、Java虚拟机栈: 存局部变量表、栈帧,线程私有
3、本地方法栈: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
4、Java堆: GC堆,存放对象实例,所有线程共享
5、方法区: 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。以及常量池子
如何判断对象是否是垃圾?
- 引用计数法: 设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,导致内存泄漏,一般不使用这种方法。
//循环引用例子
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc(); 1
ReferenceCountingGc objB = new ReferenceCountingGc(); 1
objA.instance = objB;2
objB.instance = objA;2
objA = null;1
objB = null;1
//两个对象到最后引用都是1,但是都获取不到了,所以导致内存泄漏了
}
}
可达性分析: 通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,(也可以说是遍历图)如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。
-
可用做GCroot的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
枚举根节点的方式:
在HotSpot的实现中,是使用一组称为OOPMap的数据结构来达到这个目的的,首先在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描的时候,就可以根据OOPMap上记录的信息准确定位到哪个区域中有对象的引用,这样大大减少了通过逐个遍历来找出对象引用的时间消耗。
垃圾回收的分代算法(垃圾收集算法及各自的优缺点)?
答:
1、标记-清除算法(老年代)
- “标记-清除”算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 缺点:
- 执行效率不稳定;标记和清除两个过程的执行效率随对象数量增长而降低。
- 内存空间的碎片化问题;标记、清除之后会产生大量不连续的内存碎片,导致当需要分配较大对象时无法找到足够的连续空间而不得不提前触发另一次垃圾收集动作。
2、标记-复制算法(针对新生代)
标记-复制算法将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
- 优点:
- 分配内存时不用考虑空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
- 缺点:
- 将可用内存缩小为了原来的一半,空间浪费多。
3、标记-整理算法(针对老年代)
复制算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制算法。
根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动对象都存在弊端,移动对象操作必须全程暂停用户应用程序才能进行("Stop The World"),不移动对象会影响应用程序的吞吐量。
JDK1.8用的垃圾回收器?
CMS收集器是怎样的?
答:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,是一款老年代收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。
整个工作流程包括四个步骤:
1. 初始标记: 仅仅标记GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
2. 并发标记: 从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
3. 重新标记: 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”
4. 并发清除: 清理删除标记阶段判断的已经死亡的对象,不需要移动存活对象,可以与用户线程同时并发。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点: 并发收集、停顿低
缺点:
- 对CPU资源敏感,总吞吐量会降低。,因为在并发阶段他虽然不会停顿用户线程,但是会因为占用一部分线程而导致应用线程变慢,降低吞吐量,如果应用程序负载本来就很高,好要分出一部分处理能力去执行垃圾收集,就会导致应用的执行速度大幅降低
- 无法处理浮动垃圾。CMS在并发阶段和并发清除阶段由于用户线程一直在运行,会导致新的对象产生,而此时垃圾收集已经结束,而这部分对象只能在下次被回收,而且还要预留部分内存空间给用户线程使用,导致他并不能和其他垃圾收集器一样,等到老年代快满的时候采取垃圾收集,而是在CMS是在达到68%的时候就会激活垃圾收集。
- 标记-清除算法导致空间碎片
cms重新标记的时候三色算法了解吗(并发标记的算法)?
答:三色标记法可以并发的原因,因为是并发标记的时候,是可能会线程切换的,那么这个时候为了恢复现场,你就需要保存足够的信息,足以你恢复的时候去查看,假设这样,如果没有三色标记法,那么只要你这个节点没遍历。
三色标记法的底层目的
三色标记法好处
三色标记算法
答:
三色标记法将对象的颜色分为了黑、灰、白,三种颜色。
- 黑色: 该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)
- 灰色: 该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)
- 白色: 该对象没有被标记过。(对象垃圾)
从我们main方法的根对象(JVM中称为GC Root)开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象
扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World),再次进行扫描,此时因为黑色对象的属性都也已经被标记过了,所以只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多)
此时程序继续执行,GC线程扫描所有的内存,找出被标记为白色的对象(垃圾)清除
- 问题:
1.错标: 最开始A指向B,B指向D,但是在A完成标记(变成黑色)后,遍历到B,B指向D的引用消失(D访问不到,会被标记为白色), 而A又指向了D(本应该重新被标记为黑色,但是因为A已经标记过,不会再次标记),此时因为A标记为黑色表示所有子对象都被标记,所以D不会被检测到,从而导致。此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。
2. 漏标: 假设GC已经在遍历对象B了,而此时用户线程执行了A.B=null的操作,切断了A到B的引用。
本来执行了A.B=null之后,B、D、E都可以被回收了,但是由于B已经变为灰色,它仍会被当做存活对象,继续遍历下去。
最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分。
实际上,这个问题依然可以通过「写屏障」来解决,只要在A写B的时候加入写屏障,记录下B被切断的记录,重新标记时可以再把他们标为白色即可。
G1 收集器(JDK9 默认垃圾收集器,可以收集老年代和新年代,采用了一种新的分区)
参考链接
答:
以前垃圾回收器是 新生代 + 老年代,用了CMS效果也不是很好,为了减少STW对系统的影响引入了G1(Garbage-First Garbage Collector),G1是一款面向服务端应用的垃圾收集器。
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
- 并行与并发: G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集: 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合: 与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制(相关的两块Region)”算法实现的。
- 可预测的停顿: 这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
- G1 收集器的运作大致分为以下几个步骤:
- 初始标记 (stop the world事件 CPU停顿只处理垃圾);标记GC Roots 可以直接关联的对象,该阶段需要线程停顿但是耗时短
- 并发标记 (与用户线程并发执行);寻找存活的对象,可以与其他程序并发执行,耗时较长
- 最终标记 (stop the world事件 ,CPU停顿处理垃圾);并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行
- 筛选回收 (stop the world事件 根据用户期望的GC停顿时间回收)。对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划
SATB(snapshot-at-the-beginning): 上面global concurrent marking提到了STAB算法,那这个STAB到底为何物?STAB全称为snapshot-at-the-beginning,其目的是了维持并发GC的正确性。GC的正确性是保证存活的对象不被回收,换句话来说就是保证回收的都是垃圾。如果标记过程是STW的话,那GC的正确性是一定能保证的。但如果一边标记,一边应用在变更堆里面对象的引用,那么标记的正确性就不一定能保证了(并发标记使用的CMS算法,这里采用了STAB算法来防止漏标和错标)。
RememberedSet: G1 收集器使用的是化整为零的思想,把一块大的内存划分成很多个域( Region )。但问题是,难免有一个 Region 中的对象引用另一个 Region 中对象的情况。为了达到可以以 Region 为单位进行垃圾回收的目的, G1 收集器也使用了 RememberedSet 这种技术。G1中每个Region都有一个与之对应的RememberedSet ,在各个 Region 上记录自家的对象被外面对象引用的情况。当进行内存回收时,在GC根节点的枚举范围中加RememberedSet 即可保证不对全堆扫描也不会有遗漏。并加快扫描效率。进行垃圾回收时,如果Region1有根对象A引用了Region2的对象B,显然对象B是活的,如果没有Rset,就需要扫描整个Region1或者其它Region,才能确定对象B是活跃的,有了Rset可以避免对整个堆进行扫描。
为了解决这个问题,STAB的做法在GC开始时对内存进行一个对象图的逻辑快照(snapshot),通过GC Roots tracing 参照并发标记的过程,只要被快照到对象是活的,那在整个GC的过程中对象就被认定的是活的,即使该对象的引用稍后被修改或者删除。同时新分配的对象也会被认为是活的,除此之外其它不可达的对象就被认为是死掉了。这样STAB就保证了真正存活的对象不会被GC误回收,但同时也造成了某些可以被回收的对象逃过了GC,导致了内存里面存在浮动的垃圾(float garbage)。
G1把Java内存拆分成多等份,多个域(Region),逻辑上存在新生代和老年代的概念,但是没有严格区分。每一个Region,它要么是young的,要么是old的。还有一类十分特殊的Humongous。所谓的Humongous,就是一个对象的大小超过了某一个阈值——HotSpot中是Region的1/2,那么它会被标记为Humongous。如果我们审视HotSpot的其余的垃圾回收器,可以发现这种对象以前被称为大对象,会被直接分配老年代。而在G1回收器中,则是做了特殊的处理。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
一句话总结G1思维:每次选择性的清理大部分垃圾来保证时效性跟系统的正常运行。
G1垃圾回收器详解参考
CMS和G1的区别?
CMS 和G1 的区别 - 简书 (jianshu.com)
简述Java类加载机制?
- 当需要某个类的时候,jvm会加载.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。
1. 加载: ClassLoader通过全类名查找类的字节码文件并创建一个class对象
2. 验证: 对文件格式,元数据,字节码,符号引用等验证正确性。
3. 准备: 为类变量(static修饰)分配内存并赋初始值:例子1、static int i = 5这里只是将i赋值为0,初始化的阶段再把i赋值为5。2、不包含final修饰的static,因为final在编译的时候就已经分配了
4. 解析: 将符号引用(只是一段唯一标志目标的常量,目标可以是方法,接口,类)转化为直接引用(目标真正的地址,也就是指针)。
5. 初始化: 如果该类有父类就对父类进行初始化
这里可以提一下双亲委派原则
Java对象的创建过程?
答:
- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
- 通过检查通过后虚拟机将为新生对象分配内存。
- 完成内存分配后虚拟机将成员变量设为零值
- 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
- 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
GC的主要区域?
答:
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
-
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC): 收集整个 Java 堆和方法区。
Java的内存泄漏是什么?threadlocal为什么内存泄漏?
答:
-
内存泄漏: 对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 -
threadlocal内存泄漏:
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
JVM内存结构和Java内存模型?
答:JVM内存结构指的是5个区域,Java内存模型是JMM。
简述Parallel Scavenge和 Parallel Old 收集器(JDK8默认垃圾收集器)?
答:
ParNew :收集器其实就是 Serial 收集器(最简单是收集器,暂停程序,收集垃圾,启动程序)的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。新生代采用标记-复制算法,老年代采用标记-整理算法。它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
Parallel Scavenge:Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
GC流程(垃圾收集算法是思想,垃圾收集器是实现)?
答:核心思想就是根据各个年代的特点不同选用不同到垃圾收集算法(新生代用标记-复制,老年代用标记-整理)。
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC年龄就会增加1岁,当它的年龄增加到一定次数(默认15次)时,就会被移动到年老代中。年轻代的垃圾回收算法使用的是复制算法。
JVM常用的参数?
答:
- -Xms(最小) 和 -Xmx(最大):指定堆内存
- 设置垃圾回收器:
Serial垃圾收集器(新生代)
开启:-XX:+UseSerialGC
关闭:-XX:-UseSerialGC
//新生代使用Serial 老年代则使用SerialOld
ParNew垃圾收集器(新生代)
开启 -XX:+UseParNewGC
关闭 -XX:-UseParNewGC
//新生代使用功能ParNew 老年代则使用功能CMS
Parallel Scavenge收集器(新生代)
开启 -XX:+UseParallelOldGC
关闭 -XX:-UseParallelOldGC
//新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
ParallelOl垃圾收集器(老年代)
开启 -XX:+UseParallelGC
关闭 -XX:-UseParallelGC
//新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
CMS垃圾收集器(老年代)
开启 -XX:+UseConcMarkSweepGC
关闭 -XX:-UseConcMarkSweepGC
G1垃圾收集器
开启 -XX:+UseG1GC
关闭 -XX:-UseG1GC
- GC并行执行线程数:-XX:ParallelGCThreads=16
G1出现主要是为了优化CMS的哪些不足?
CMS没有采用复制算法,所以它不能压缩,最终导致内存碎片化问题。而G1采用了复制算法,它通过把对象从若干个Region拷贝到新的Region过程中,执行了压缩处理。
在G1中,堆是由Region组成的,因此碎片化问题比CMS肯定要少的多。而且,当碎片化出现的时候,它只影响特定的Region,而不是影响整个堆中的老年代。
而且CMS必须扫描整个堆来确认存活对象,所以,长时间停顿是非常常见的。而G1的停顿时间取决于收集的Region集合数量,而不是整个堆的大小,所以相比起CMS,长时间停顿要少很多,可控很多。