3-虚拟机篇

Posted 11.π.14

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-虚拟机篇相关的知识,希望对你有一定的参考价值。

一.java JVM 的内存结构

内存:按线程类型分两类

线程共享:

  1. 方法区:存放类的信息
  2. 堆:存放java对象的信息

线程私有:

  1. java虚拟机栈:存放java方法、方法参数和局部变量
  2. 程序计数器:记录程序执行到几行

执行引擎

  1. 解释器:把class 字节码代码解释成机器码,对同一行代码反复解释,比如调用同一个方法多次,就会被解释多次。
  2. JIT即时编译器:把热点代码解释成机器代码,并且缓存起来。
    2.1热点判定的方式有两种:采样热点探测、计数器探测。判定某段代码是否为热点代码,是否要触发即时编译的这种行为称为“热点探测”

二.哪些部分会出现内存溢出

除了程序计数器外,方法区、堆、栈、本地方法栈都会出现内存溢出。
内存溢出分为两种情况:

  1. OutOfMemoryError

    • 堆内存耗尽- 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽- 加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • 虚拟机栈累积- 默认每个线程最多会占用1M内存,线程个数越来越多,而 又长时间运行不销毁时
  2. StackOverflowError

    • 虚拟机栈内部- 方法调用次数过多,比如递归错误,无限制的运行,消耗掉线程内的1M内存。

方法区与永久代、元空间之间的关系

方法区是规范,永久代和元空间都是对方法区的实现。

  1. 方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等

  2. 永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)

  3. 元空间是Hotspot虚拟机对JVM规范的实现(1.8之后),使用本地内存作为这些信息的存储空间

JVM内存参数

对于JVM内存配置参数:

  • -Xmx10240m(最大内存数10G)
  • -Xms10240m(最小内存数10G)
  • -Xmn5120m(新生代5G)
  • -XX:SurvivorRatio=3其最小内存值和Survivor区总大小分别是
  • -Xss 线程的内存 linux 64位默认是1M

JVM垃圾回收算法

  1. 标记清除(都不用了)CMS,在最新的JVM虚拟机中已经废弃了
    分为标记阶段,和清除阶段。
    标记阶段先找到根对象,一定不能回收的对象。例如:局部变量引用的对象,正在被使用或者,静态变量引用的对象。沿着根对象的引用链,找到被引用的对象,标记这些对象。
    清除阶段:没有标记的对象直接清除就好了。
    缺点:标记清除,会导致内存碎片化,内存不连续

  2. 标记整理(适用于老年代垃圾回收)
    标记阶段:沿着根对象的引用链,找到被引用的对象,标记这些对象。
    整理阶段:移动存活对象到一边,解决内存碎片化,不连续的问题

  3. 标记复制(适用于新生代代垃圾回收)
    标记阶段:沿着根对象的引用链,找到被引用的对象,标记这些对象。
    复制阶段:把存活对象复制到空闲区域,直接清理旧的区域就好了

    缺点:占用内存多,经常用于新生代的内存空间。不适合老年代

说说GC 和 分代回收算法!!!!!

  • 1.Gc的目的在于实现无用对象的内存自动释放,减少内存碎片、加快内存分配速度

  • 2.GC要点

    • 1.回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
    • 2.判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
    • 3.GC具体的实现称为垃圾回收器常见的垃圾回收器
    • 4.GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代老年代,不同区域应用不同的回收策略
    • 5.根据GC的规模可以分成Minor Gc(新生代发生了垃圾回收),Mixed GC(老年代发生了垃圾回收),Full GC(时间比较长)
  • 3.分代回收

    • 1.伊甸园eden,最初对象都分配到这里,与幸存区合称为新生代
    • 2.幸存区survivor ,当伊甸园内存不足,回收后的幸存对象到这里,分成from 和 to ,采用标记复制算法
    • 3.老年代old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
  • 4 GC规模

    • 1.Minor GC 发生在新生代的垃圾回收,暂停时间短
    • 2.Mixed GC 新生代+ 老年代部分区域的垃圾回收,G1回收机器特有
    • 3.Full GC 老年代完整垃圾回收,暂停时间长,应尽力避免

三色标记与并发漏标问题

  1. 用三种颜色记录对象的标记状态

    • 1.黑色 - 已标记====>沿着 根对象的引用链,已经找到这个对象了,并且内部的其他引用已经完成了,就标记黑色
    • 2.灰色 - 标记中 ====>沿着 根对象的引用链,已经找到这个对象了,但是内部还有其他引用未完成了,就标记灰色
    • 3.白色 - 还未标记 ====>未被处理的都是白色标记
  2. 漏标问题-记录了 标记过程中变化 来解决漏标问题

    • 1.incremental Update(增量更新) 只要赋值发生,被赋值的对象就会被记录,重新标记成灰色。然后再做一遍处理,重新标记。
    • 2.Snapshot at the Beginning,SATB
      • 1.新增加对象会被记录
      • 2.被删除引用关系的对象也被记录

几个重要的垃圾处理器

  1. Parallel GC特点有如下
    • 1.eden内存不足发生Minor GC,标记复制 STW
    • 2.old内存不足发生Full GC,标记整理STW
    • 3.注重吞吐量
    • 4.虽然会暂停,但是会多个线程并行执行垃圾回收,所以时间比较短
  2. ConcurrentMarkSweep GC
    • 1.old 并发标记,重新标记时需要STW,并发清除
    • 2.Failback Full GC 并发失败,清除速度 < 对象产生的速度,就会并发失败,就会触发 Full GC
    • 3.注重响应时间
  3. G1 GC 从JDK9开始作为默认GC
  • 1.响应时间与吞吐量兼顾
  • 2.划分多个区域,每个区域都可以充当eden,survivor,old,humongous(存放大对象的区域)
  • 工作流程可以分为3个阶段
    • 1.新生代回收:eden内存不足,标记复制STW,复制到survivor,如果幸存对象到达了晋升阈值,就复制到老年代
    • 2.并发标记:old在堆内存中的占比超过45%,触发并发标记,重新标记时需要STW
    • 3.混合收集:并发标记完成,开始混合收集,参与复制的有eden、survivor、old,其中old会根据暂停时间目标,选择部分回收价值高(存活对象少)的区域,复制时STW
    • 4.Failback Full GC

项目中什么情况下会出现内存溢出,怎么解决的

  1. 误用线程池导致的内存溢出
//	案例1 主要是由于等待队列成撑爆了内存
public class TestThreadPool 
    public static void main(String[] args) 
        ExecutorService executor = Executors.newFixedThreadPool(2);
        while (true)
            executor.submit(()->
                try 
                    LoggerUtils.get().debug("send sms");
                    TimeUnit.SECONDS.sleep(30);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            );
        
    

主要是由于等待队列成撑爆了,不要用工具类创建线程池,自己调用 ThreadPoolExecutor ,控制拒绝策略和线程数上限
//	由于线程数没有上限,导致的内存溢出
public class TestThreadPool 
    public static void main(String[] args) 
        case2();
    

    static AtomicInteger c = new AtomicInteger();
    private static void case2()
        ExecutorService executor = Executors.newCachedThreadPool();
        while (true)
            System.out.println(c.incrementAndGet());
            executor.submit(()->
                try 
                    TimeUnit.SECONDS.sleep(30);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            );
        
    


  1. 查询数据量太大导致的内存溢出

  2. 动态生成类过多导致的内存溢出

类加载过程、双亲委派机制

  • 类加载分为三个阶段
    1. 加载
      • 1.将类的字节码载入方法区,并创建类.class对象
      • 2.如果此类的父类没有加载,先加载父类,接口也是一样的。spring bean 也是一样的,创建当前bean时,先创建其依赖的bean
      • 3.加载是懒惰执行。正在用到类的时候,才会加载。否则不会加载到方法区
    2. 链接
      • 1.验证 - 验证类是否符合Class规范,合法性、安全性检查
      • 2.准备 - 为static 变量分配空间
      • 3.解析 - 将常量池的符号引用解析为直接引用
      • 4.final 静态变量赋值
    3. 初始化
      • 1.执行静态代码块为非final 静态变量赋值
      • 2.初始化是懒惰执行的
  • 何为双亲委派
    • 所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
    • 1.能找到这个类,由上级加载,加载后该类也对下级加载器可见
    • 2.找不到这个类,则下级加载器才有资格执行加载

一道错误的面试题解答

能不能自己写一个类叫 java.lan.System?

  • 错误的回答
    答:通常是不可以的,但可以采取另类方法达到这个需求。
    解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用java系统提供的System,自己写的System类根本就没有机会得到加载。
    但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委派机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统给的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
  • 错在哪里了?
    自己编写类加载器就能加载一个假冒的java.lang.System吗?
    不行
    • 1.假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的java.lang.System,自然不会加载假冒的
    • 2.假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的java.lang.System时,它需要先加载父类java.lang.Object,而你没有用委派,找不到java.lang.Object所以加载会失败
    • 3.以上仅仅是假设。实际操作你就会发现自定义类加载器加载以java.大头的类时,会抛出安全异常,在jdk9以上版本这些特殊包名都与模块进行了绑定,更连编译都过不去
  • 双亲委派的目的是什么?
    • 1.上级加载器加载的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类
    • 2.让类的加载有优先次序,保证核心类优先加载

对象的引用类型分为哪几种

  • 1.强引用
    • 1.普通变量赋值即为强引用,如 A a = new A();
    • 2.通过GC Root 的引用链,如果强引用找不到该对象,该对象才会被回收
  • 2.软引用(SoftReference)
    • 1.例如:SofReference a = new SoftReference(new A());
    • 2.如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
    • 3.软引用自身需要配合引用队列来释放
    • 4.典型例子是反射数据
  • 3.弱引用(WeakReference)
    • 1.例如: WeakReference a = new WeakReference(new A());
    • 2.如果仅有弱引用引用了该对象时,只要发生垃圾回收,就会释放该对象
    • 3.弱引用自身需要配合引用队列来释放
    • 4.典型例子是ThreadLocalMap中的Entry对象
  • 4.虚引用(PhantomReference)
    • 1.例如:PhantomReference a = new PhantomReference(new A())
    • 2.必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由Reference Handler 线程释放其关联的外部资源
    • 3.典型例子是Cleaner释放DirectByteBuffer占用的直接内存

finalize的理解?

  • 一般的回答:它是Object中的一个方法,子类重写它,垃圾回收此方法会被调用,可以在其中进行一些资源释放和清理工作
  • 较为优秀的方法:将资源释放和清理放在finalize方法中是非常不好的,非常影响性能,严重时甚至会引起oom,从java9开始就被标注为@Deprecated,不建议被使用
  • 但是,为什么?性能不好
    • 1.非常不好
      • 1.FinalizerThread是守护线程,代码很有可能没有来得及执行完,线程就结束了,造成资源没有正确释放
      • 2.异常被吞掉了,这个就太糟了,你甚至不能判断 有没有在释放资源时发生错误
    • 2.影响性能
      • 1.重写了finalize方法的对象在第一次被GC的时候,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次gc时才能真正释放内存
      • 2.可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾累积过多就会容易full gc,full gc 后释放速度如果仍跟不上创建新对象的速度,就会OOM
    • 3.质疑
      • 1.有的文章提到【Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread的优先级较普通线程更高,赶不上步伐的原因应该是finalize执行慢等原因导致的

以上是关于3-虚拟机篇的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题-虚拟机篇

Java面试知识点之虚拟机篇

3-虚拟机篇

面试重点:Java虚拟机篇

JAVA语言开发基本原理

java语言开发环境搭建