秋招面试知识点----JVM篇

1.发生栈内存溢出。

描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo。

  1. 栈定义
  • 存放基本类型的变量数据和对象的引用,栈是线程私有的,他的生命周期与线程相同。每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  1. 为什么会溢出
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,就会抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请足够的内存空间,就会抛出OutOfMemoryError异常。
  1. 相关参数
  • -Xss 调整JVM栈的大小
  1. 栈溢出
  • 无限递归,没有结束条件。

    public class StackTest {
        public static void main(String[] args) {
            main(args);
        }
    }
    
  • `

2.详解JVM内存模型

思路: 给面试官画一下JVM内存模型图,并描述每个模块的定义,作用,以及可能会存在的问题,如栈溢出等。

img

共享的,包括 Java 堆和方法区

私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。

堆内存(Heap)

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

堆内存是所有线程共有的,可以分为两个部分:年轻代和老年代。

下图中的Perm代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代已经被移除。

img

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: 空间利用率就是90%

方法区(Method Area)

方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。

在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数**-XX:MaxPermSize**来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM默认是85M)。

随着JDK8的到来,JVM不再有 永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory。

虚拟机栈(JVM Stack)

描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个**“栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息**。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈(Native Stack)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

程序计数器(PC Register)

程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。

当执行java方法时候,计数器中保存的是字节码文件的行号;当执行Native方法时,计数器的值为空。

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

直接内存

直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。

JVM内存参数设置

img

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -Xmn:设置年轻代大小
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小
  • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
  • -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

典型JVM参数配置参考:

  • java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
  • -XX:ParallelGCThreads=20
  • -XX:+UseConcMarkSweepGC-XX:+UseParNewGC

-Xmx3550m:设置JVM最大可用内存为3550M。

-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。

3.JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。

思路: 先讲一下JAVA堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。)共享内存区划分

  1. 为什么要分为新生代和老年代?
  • 因为有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。提高效率。
  1. 新生代中为什么要分为Eden和Survivor。
  • 如果没有Survivor区,那么Eden每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发Full GC,Full GC是非常耗时的。当我们加上Survivor后,将Eden区满了的对象,添加到Survivor区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即Survivor相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数

3.为什么要加两个Survivor?

  • 如果只有1个Survivor区,那当Eden区满了之后,就会复制对象到Survivor区,容易产生内存碎片化。严重影响性能。所以使用2个Survivor区,始终保持有一个空的Survivor区,可以避免内存碎片化。

3.判断对象是否存活

· 引用计数法
在一个对象被引用时加一,被去除引用时减一,这样我们就可以通过判断引用计数是否为零来判断一个对象是否为垃圾。这种方法我们一般称之为「引用计数法」。主流的Java虚拟机里面都没有选用引用计数算法来管理内存,因为这太浪费时间空间,计数器要好多,还有分配及空间,循环引用问题

· 可达性分析算法
通过一系列名为**”GC Roots”**的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不会用的。

· GC Roots:
虚拟机栈中引用的对象
方法区静态属性引用的对象
方法区常量引用的对象
JNI引用的对象(Native方法)

4.JVM中一次完整的GC流程

思路: 先描述一下Java堆内存划分,再解释Minor GC,Major GC,full GC,描述它们之间转化流程。

  • 1.大多数情况下,对象在新生代Eden区进行分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGc。把eden,幸存区to都放到from

  • 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串直接进入老年态

  • 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态

  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代

  • 如果老年代执行了Full GC之后发现依然无法进行对象的保存,就会产生**OOM异常“OutOfMemoryError”**然后就是开始调优,扩大堆内存,jprofier看看大对象。

5.垃圾收集器cms和G1

思路: 一定要记住典型的垃圾收集器,尤其cms和G1,它们的原理与区别,涉及的垃圾回收算法。

  1. 垃圾回收器
  • Serial收集器: 针对新生代的单线程收集器,使用复制算法。收集垃圾是会产生较长时间的停顿,但不会产生线程切换的开销

  • Serial Old收集器: 针对老年代的单线程收集器,使用标记整理算法。

  • Parallel Scavenge收集器: jdk 1.8默认的垃圾回收器。新生代回收器,采用复制算法,多线程并行回收,充分利用CPU资源。进行垃圾收集时,必须暂停所有工作线程,直到完成

  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。

  • ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。

  • CMS(Concurrent Mark Sweep) 收集器:

  • 并发低停顿收集器

  • 开启就是个参数,同时开启parnew(新设代)cms(Old)serise(old)

  • 使用标记清除算法

    • 四个阶段

      • 初始标记 (标记GC Roots可以直接关联的对象,速度很快)
      • 并发标记 (进行GC Roots Tracing,判断对象是否存活,不暂停)
      • 重新标记 (校准并发标记对象的存活状态,并发标记又在运行,修正刚才的标记变化)
      • 并发清除 (回收标记的对象)
    • 初始标记和重新标记仍然需要Stop The World

    • 优点:并发地停顿

    • CMS缺点

      • 由于并发带来的CPU资源消耗

      • 由于并发收集在回收过程中产生的浮动垃圾无法清除

      • 使用标记清除算法带来的空间碎片问题

      • 万一没能在老年代用完时进行cms,就会用serise,停顿很高

  • G1收集器:

    • 在G1与其他收集器的区别就是它不再区分新生代或者老年代,它将整个Java堆划分为多个大小相等的独立区域(Region),避免全内存的gc, 不是固定的,一会是老年,一会是新生代。 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,就是GC还是分区的它们都是一部分Region(不需要连续)的集合,**打印的就是只有两块,regin,元空间。**用于服务端,像保证吞吐量,有尽可能减少停顿时间
    • 运作流程主要包括以下:
    • **初始标记,**标记GC Roots可以直接关联的对象
    • **并发标记,**进行GC Roots Tracing,判断对象是否存活
    • **最终标记,**修正并发标记期间因程序继续运行导致标记产生变动的记录
    • 筛选标记 首先对各个Region 的回收价值和成本进行排序,根据用户所期望的G停顿时间来制定回收计划。
  • 收集实现:

    • eden收集,满了就收集,主要是小范围的收集,连续的内存块,避免内存碎片
    • eden移到幸存区,放不开,,eden直接阶变成老年代
    • 幸存区的收集,部分回到老年代
    • eden没东西了,gc结束
  1. CMS收集器和G1收集器的区别:
  • CMS收集器是老年代的收集器,可以配合新生代和ParNew收集器一起使用,老年代的的Serialold;
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用
  • CMS收集器以最小的停顿时间为目标的收集器;
  • G1收集器可预测垃圾回收的停顿时间
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间

相同:

  • 都是可以并发

单cpu就是serice

多cpu,吞吐量要求后台计算就是用parall4

响应速度:cms,parallnew

6.什么是内存屏障

内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

7.强引用、软引用、弱引用、虚引用的区别?

先说一下四种引用的定义,可以结合代码讲一下,也可以扩展谈到ThreadLocalMap里弱引用用处。

强引用

当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~!

强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用于或者显示地将相应(强)引用赋值为null,一般可以认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾回收策略)

强引用小例子:

public class ReferenceDemo {
    public static void main(String[] args) {
        // 这样定义的默认就是强应用
        Object obj1 = new Object();
        // 使用第二个引用,指向刚刚创建的Object对象
        Object obj2 = obj1;
        // 置空
        obj1 = null;
        System.gc();
        System.out.println(obj1);
        System.out.println(obj2);
    }
}

null
java.lang.Object@4554617c

输出结果我们能够发现,即使 obj1 被设置成了null,然后调用gc进行回收,但是也没有回收实例出来的对象,obj2还是能够指向该地址,也就是说垃圾回收器,并没有将该对象进行垃圾回收

软引用

软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:

  • 当系统内存充足时,它不会被回收
  • 当系统内存不足时,它会被回收

软引用通常在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用 的时候就保留,不够用就回收

具体使用

  1. 在内存足够的时候
public class ReferenceDemo {
    public static void main(String[] args) {

        // 这样定义的默认就是强应用
        Object obj1 = new Object();
        Reference<Object> reference=new SoftReference(obj1);
        System.out.println(obj1);
        System.out.println(reference.get());

        System.out.println("----------------------------------------------------------");
        obj1=null;
        // 模拟OOM自动GC
        System.gc();
        System.out.println(obj1);
        System.out.println(reference.get());
    }
}
java.lang.Object@4554617c
java.lang.Object@4554617c
null
java.lang.Object@4554617c
  1. 内存不足的时候

JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
-Xms5m -Xmx5m -XX:+PrintGCDetails

public class ReferenceDemo {
    public static void main(String[] args) {

        // 这样定义的默认就是强应用
        Object obj1 = new Object();
        Reference<Object> reference=new SoftReference(obj1);
        System.out.println(obj1);
        System.out.println(reference.get());

        System.out.println("----------------------------------------------------------");
        obj1=null;
        try {
            // 创建30M的大对象
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(obj1);
            System.out.println(reference.get());
        }
    }
}

我们写了两个方法,一个是内存够用的时候,一个是内存不够用的时候。
我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值。然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收。

当内存不够的时候,我们使用了JVM启动参数配置,给初始化堆内存为5M,但是在创建对象的时候,我们创建了一个30M的大对象。
这就必然会触发垃圾回收机制,这也是中间出现的垃圾回收过程,最后看结果我们发现,o1 和 softReference都被回收了,因此说明,软引用在内存不足的时候,会自动回收。

弱引用

不管内存是否够,只要有GC操作就会进行回收

弱引用需要用 ==java.lang.ref.WeakReference`==类来实现,它比软引用生存期更短

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。

public class ReferenceDemo {
    public static void main(String[] args) {

        Object obj = new Object();
        Reference<Object> reference = new WeakReference<>(obj);
        System.out.println(obj);
        System.out.println(reference.get());
        System.out.println("-----------------------------");
        obj=null;
        System.gc();
        System.out.println(obj);
        System.out.println(reference.get());
    }
}

java.lang.Object@4554617c
java.lang.Object@4554617c
-----------------------------
null
null

我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了

WeakHashMap是什么?

WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,HashMap中的key会进行回收,下面我们使用例子来测试一下
我们输入一个Key-Value键值对,然后让它的key置空,然后在查看结果

public class WeakHashMapDemo {
    public static void main(String[] args) {
        myHashMap();
        System.out.println("==========");
        myWeakHashMap();
    }

    private static void myHashMap() {
        Map<Integer, String> map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }

    private static void myWeakHashMap() {
        Map<Integer, String> map = new WeakHashMap<>();
        Integer key = new Integer(1);
        String value = "WeakHashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }
}

最后输出结果为:

{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}

从这里我们看到,对于普通的HashMap来说,key置空并不会影响,HashMap的键值对,因为这个属于强引用,不会被垃圾回收。

但是WeakHashMap,在进行GC操作后,弱引用的就会被回收

虚引用

概念

虚引用又称为幽灵引用,需要java.lang.ref.PhantomReference 类来实现

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用。

虚引用的主要作用和跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前,做必要的清理工作

这个就相当于Spring AOP里面的后置通知

8.对象创建过程

第一次使用
1.进行判断
java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。加载并初始化类完成后,再进行对象的创建工作。

分为加载并初始化类创建对象

2.加载并初始化类

java是使用双亲委派模型来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程:
双亲委托模型的工作过程是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类,并且不会重写String这种基层类。
2. 验证

格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
3. 准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
被final修饰的static变量(常量),会直接赋值;
4. 解析
**将常量池中的符号引用转为直接引用(**得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
解析需要静态绑定的内容。 // 所有不会被重写的方法和域都会被静态绑定
5. 初始化(先父后子)

  • 静态变量赋值
  • 执行static代码块
    注意:static代码块只有jvm能够调用
       **如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,**其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也有有的,是默认值
最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

3.创建对象

  • 1.在堆区分配对象需要的内存
    分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
  • 2.对所有实例变量赋默认值
    将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值
  • 3.执行实例初始化代码
    1.初始化顺序是先初始化父类再初始化子类2.初始化时先执行实例代码块然后是构造方法
  • 4.如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它
  • 注意,dcl单例就是要volital,防止2,3步指令重排,DCL,DoubleCheckLock双重检查

9.为对象分配内存的方式

指针碰撞空闲列表

  • 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump thePointer)。如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(FreeList)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

    img

img

1.tlab,阻塞锁的地方,减少冲突

10 说一下 JVM 有哪些垃圾回收算法?

1.复制算法

缺点:不会产生空间碎片,但内存折半

  • 复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制 到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。两个幸村区就是因为这个。
  • 如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合
  • 例如:在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
    紧接着进行GC,在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移 动到年老代中,没有达到阈值的对象会被复制到“To”区域。
    经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新 的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区 域是空的。
    GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

2.标记清除法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
标记:从根节点开始标记引用的对象。
清除:未被标记引用的对象就是垃圾对象,可以被清理。
产生碎片,遍历两边

3.标记压缩算法

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后 清理边界以外的垃圾,从而解决了碎片化的问题。

又边理了一遍

4.分代算法

前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点 进行选择,才是明智的选择。
分代算法其实就是这样的,根据回收对象的特点进行选择**,在jvm中,年轻代适合使用复制算法,老年代适合使用 标记清除或标记压缩算法。**

5.hotspot:枚举根节点

可达性分析就是找Gcroot节点引用的操作,但是可达性分析要寻找全局性的引用,执行上下文,就是站真的本地变量表,印用太多了,消耗时间,而且会有GC停顿,就是对象之间的引用是不能改变的,所以会停止所有的线程这就是“stop world”,cms在枚举根节点时也会停顿

所以就有了oop MAp,知道那存放着对象,直接扫描这东西将快速准去的完成GC root的枚举,可能会导致引用关系变化,或是说oopmap的变换子陵非常多,为了节约成本,只在特定安全点设置了oopmap

11.调优命令

我用过的

//-xss 1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

//-Xms 1m 初始化内存分配 默认内存的1/64

//-Xmx 8m 最大分配内存 默认1/4

//-XX:+PrintGCDetails //打印具体信息

//-XX:+HeapDumpOnOutOfMemoryError //oom 下载Dump文件

-XX:SurvivorRatio 8:1:1

newRatio 1:2

jstat -gc 19570 就是幸存区总大小,已用

jsp -l

命令 jinfo -flags pid

-Xms20m Java堆初始容量
-Xmx20m Java堆最大容量
-Xmn10m Java堆年轻代大小
-XX:+PrintGCDetails 打印GC信息

-XX:+PrintGCDateStamps 打印GC时间
-XX:SurvivorRatio=8 n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如8,表示Eden:Survivor=8:2,一个Survivor区占整个年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/logs

jps [options] [hostid]

options参数解释:
-l : 输出主类全名或jar路径**
-q : 只输出LVMID
-m :输出JVM启动时传递给main()的参数
-v : 输出JVM启动时显示指定的JVM参数

jinfo命令

options

-flag**** **打印指定名称的参数
-flag [+|-]打开或关闭参数
-flag =设置参数
-flags打印所有参数
-sysprops:打印系统配置
打印上面两个选项

查看 JVM 参数

jstat [option] LVMID [interval] [count]

jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。

线程状态
想要通过jstack命令来分析线程的情况的话,首先要知道线程都有哪些状态,下面这些状态是我们使用jstack命令查看线程堆栈信息时可能会看到的线程的几种状态:

top -HP 22877 找出此进程中cpu最高得线程

jmap -histo:live pid>a.log
可以观察heap中所有对象的情况(heap中所有生存的对象的情况)。包括对象数量和所占空间大小。 可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出GC回收了哪些对象。

jmap -dump:live,format=b,file=a.log pid
说明:内存信息dump到a.log文件中。

这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值