您的当前位置:首页正文

java 八股文

2024-11-08 来源:个人技术集锦

(网上好心人给的md资料,发在csdn上方便学习,侵删)

JVM其他

Java体系结构包括四个独立但相关的技术:

  • Java程序设计语言
  • Java.class文件格式
  • Java应用编程接口(API)
  • Java虚拟机

我们再在看一下它们四者的关系:

当我们编写并运行一个Java程序时,就同时运用了这四种技术,用Java程序设计语言编写源代码,把它编译成Java.class文件格式,然后再在Java虚拟机中运行class文件。当程序运行的时候,它通过调用class文件实现了Java API的方法来满足程序的Java API调用

什么是GC

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。其体现在追踪所有正在使用的对象,并且将剩余的对象标记为垃圾,随后标记为垃圾的对象会被清除,回收这些垃圾对象占据的内存,从而实现内存的自动管理;

如果不进行垃圾回收,内存迟早都会被消耗完,垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存在C/C++中,释放无用变量内存空间的事情要由程序员自己来解决。Java有了GC,就不需要程序员去人工释放内存空间。当Java虚拟机发觉内存资源紧张的时候,就会自动地去清理无用变量所占用的内存空间

没有GC就不能保证应用程序的正常运行。而经常造成STW的GC又跟不上实际的需求,所以才会不断的进行GC优化。

内存分配与回收

这两篇博文对于JVM整体更为透彻:

年轻代与年老代

存储在JVM的Java对象分为两类:

  • 一类是生命周期比较短的瞬间,这类对象的创建和消亡都比较迅速
  • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
    Java堆区进一步细分的话,分为YoungGen和OldGen。

配置YoungGen和OldGen在堆结构的占比
默认 -XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。

在HotSpot中,Eden空间和另外两个Survivor空间省所占比例是8:1:1。开发人员可以通过-XX:SurvivorRatio=8来调整这个空间比例。
-XX:-UseAdaptiveSizePlicy : 关闭自适应的内存分配策略。

几乎所有的Java对象都是在Eden区被new出来。

绝大部分的Java对象的销毁都是在新生代进行的。

可以使用"-Xmn"设置新生代最大内存大小,这个值一般默认就好了。

Minor GC、Major GC、Full GC

GC检索哪些是垃圾时,会导致用户线程暂停,所以希望GC出现的情况少,这里主要对Major GC、Full GC进行调优。因为它们两个GC的时间是Minor GC的10倍以上。

JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收,大部分时候回收的是新生代。

针对HotSpot VM的实现,他里面的GC按照回收区域分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)。

  • 部分收集(Partial GC)

    • 新生代(Eden、S0、S1)进行回收采用(Minor GC/ YGC)
    • 老年代进行回收采用(Major GC/ OGC)
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
  • 整堆收集(Full GC)

    • 收集整个java堆和方法区的垃圾收集(Full GC)。
Minor GC的触发机制:
  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代指得是Eden代满,Survivor满不会触发GC。
  • Minor GC非常频繁,一般回收速度也比较快。
  • Minor GC会引发STW,暂停其他用户的线程,等垃圾回收线程结束,线程才恢复运行。
老年代GC(Major GC/Full GC)触发机制:
  • 指发生在老年代的GC,对象从老年代消失。
  • 出现了Major GC,经常会伴随至少一次的Minor GC。
  • 如果Major GC后,内存还不足,就报OOM。
Full GC触发机制:
  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行。
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。

Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区

类加载机制

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

加载(Loading) 验证(Verification) 准备(Preparation) 解析(Resolution)

初始化(Initialization) 使用(Using)卸载(Unloading) 如果要深入JVM推荐此博文:

JDK中提供了三个ClassLoader,根据层级从高到低为:

JVM加载类的实现方式,我们称为 双亲委托模型

如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父加载器,每一层的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的Bootstrap ClassLoader中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。

  • 首先从底向上的检查类是否已经加载过,就是这行代码:Class<?> c = findLoadedClass(name);
  • 如果都没有加载过的话,那么就自顶向下的尝试加载该类。

双亲委托模型的重要用途是为了解决类载入过程中的安全性问题。 另一方面避免类重复字节码加载,节约内存

假设有一个开发者自己编写了一个名为***.lang.Object*的类,想借此欺骗JVM。现在他要使用自定义ClassLoader来加载自己编写的java.lang.Object类。然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在Bootstrap ClassLoader的路径下找到java.lang.Object**类,并载入它

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
   
   //检查类是否被加载过
    Class c = findLoadedClass(name);
    if(c == null){
   
        try{
   
            if(parent != null){
   
                c = parent.loadClass(name, false);
            }else{
   
                //父加载器为空默认使用启动类加载器
                c = findBootstrapClassOrNull(name);
            }
        }catch (ClassNotFoundException e){
   
         //   父类无法处理抛出ClassNotFoundException
        }
        if(c == null){
   
         //   父类无法处理便调用本身findClass方法进行类加载
            c = findClass(name);
        }
    }
    if(resolve){
   
        resolveClass(c);
    }

    return c;
}

JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。JVM 实现时必须保证下面介绍的每种操作都是 原子的(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外 )。

  • lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
  • load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。
  • assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。

volatile 禁止指令重排序(内存屏障) 以及保证可见性 具体可见第二章多线程常用方法volatile部分

原子性(Atomicity)

使用Java内存模型直接保证原子性变量包括read、load、assign、use、store和write六个,即大致可以认为基本数据类型的访问、读写都是具备原子性的;

虚拟机为了保证原子性,提供了两个高级的字节码指令 monitorentermonitorexit隐式使用lock与unlock操作。这两个字节码,在 Java 中对应的关键字就是 synchronized。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

可见性(Visibility)

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

JMM 是通过 "变量修改后将新值同步回主内存变量读取前从主内存刷新变量值" 这种依赖主内存作为传递媒介的方式来实现的。

Java 实现多线程可见性的方式有:

  • volatile 保证新值能立即同步到主内存,以及每次使用立即从主内存刷新
  • synchronized 对一个变量执行unlock操作前必须将此变量同步到主内存中(执行store、write操作)
  • final 被final修饰的字段一旦被初始化完成,且构造器没有把this的引用传递出去(this引用逃逸会导致其他线程访问到初始化一半的对象),那么在其他线程中就能看见final字段的值

有序性(Ordering)

有序性规则表现在以下两种场景: 线程内和线程间

  • 线程内 - 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。
  • 线程间 - 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized 关键字修饰)以及 volatile 字段的操作仍维持相对有序。

在 Java 中,可以使用 synchronizedvolatile 来保证多线程之间操作的有序性。实现方式有所区别:

  • volatile 关键字会禁止指令重排序。
  • synchronized 关键字通过互斥(lock)保证同一时刻只允许一条线程操作。

JVM高频点

Ⅰ-① Jvm内存模型 112

红色区域线程私有不会出现线程竞争关系;蓝色区域线程共享,堆中存对象,方法区存类信息,常量,静态变量等;锁的主要应用范围是数据共享区

程序计数器(Program Counter Register)

线程私有,各条线程之间的计数器互不影响,用于在线程执行时充

Top