《天净沙·我·jvm篇》 双非菜鸡奇葩,面试项目框架,java java,卑微学子去哪?


别问 问就是为了面试豁出了老命

Java的内存区域(运行时数据区)

线程共享区

  1. 方法区 (方法区中包含运行时常量池)

线程私有区

  1. 虚拟机栈
  2. 本地方法栈
  3. 程序计数器

    直接内存

    Java1.8之后的变化

    线程共享的方法区糅合到了直接内存中的元空间

为什么每一个线程需要一个程序计数器

程序计数器的作用

是一块比较小的线程空间,可以当作字节码指令的指示器,字节码解释器通过程序计数器
来控制字节码指令,比如循环,跳转,分支,异常处理等

为啥线程需要程序计数器?

由于线程是不断切换的,所以线程在切换后,如何进行哪一步的继续操作,是需要程序计的
同时程序计数器是唯一一个不会出现OutOfMemoryError的内存区域

Java虚拟机栈

虚拟栈其实更通俗的讲也就是线程私有化的方法栈,用于执行线程中Java方法调用的内存模型,每次调用都是通过栈来传递的
其实Java内存中可以区分成栈空间,和堆空间,栈空间就是现在的虚拟机栈等,同共享
区的方法区一样,栈空间的栈帧依然包含局部变量表(各种基本数据类型和引用)
同程序计数器不同的是,虚拟机栈会出现超过栈数目的 StackOutOfFlow 以及超过内存
内存空间的OutOfMemoryError

Java的两种返回方式

  1. return
  2. 异常抛出

方法每一次的调用都会压栈,同时每一次返回都会出栈,上面两个方法的调用都会导致
出栈

本地方法栈

本地方法栈是做什么的?

本地方法栈和虚拟机栈不同,本地方法栈是用来执行Native修饰的方法,但是虚拟机栈
是用来执行Java的方法,但是对于HotSpot虚拟机来说,虚拟机栈和本地方法栈合并了
所以一样的可以推出,本地方法栈也有自己的栈帧等,栈帧里面也相应的有局部变量表
操作数栈,动态链接,出口信息等

堆 (GC堆)

堆是线程共享区的,也是Jvm管理的最大的内存空间,没有之一,几乎所有的对象实例和
都在这里分配内存,当然了线程中的类的实例等,都通过reference进行引用
由于垃圾大多数也都是由堆产生,因此也被称作为 GC堆

堆的分类

堆中可以粗略的说有 新生代和老年代 ,新生代用完以后可能就不会再引用,所以要更多的被释放掉
老年代则趋于稳定,长久的存在或被使用

更加细致的划分

被分为Eden区和From Survivor,To Survivor
大部分情况下,首先会再Eden区进行空间分配,在一次垃圾回收后,对象还存活则年龄加一
当年龄增加到默认的15岁,则进入到老年代,当然晋升的年龄阈值是可以调节的-XX:MaxTenuringThreshold

方法区

属于线程共享的内存区域,用于存放虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码量,主要是用于存放堆中的逻辑操作等。
方法区也就是永久代,永久代不用纠结就是HotSpot规定的一种规范方法区的一种实现

-XX:PermSize=N //⽅法区(永久代)初始⼤⼩
-XX:MaxPermSize=N //⽅法区(永久代)最⼤⼤⼩,超过这个值将会抛出OutOfMemoryError异
常:java.lang.OutOfMemoryError: PermGen

不过到JDK1.8的时候已经被移除了,直接变成了元空间,也就是直接存入内存空间。

运行时常量池

是属于方法区的一部分,Class文件中有类型,方法,接口,版本等信息外,还有一些常量(最特征的就是final修饰的)
但是1.7之后这部分又去到了堆中开辟了一片空间,因此

运行时常量池包含什么

字面量

  1. 文本字符串
  2. final修饰的
  3. 基本数据类型的值

    符号引用

  4. 类和结构完全限定名
  5. 字段名称和描述符
  6. 方法名称和描述符

Java对象的创建过程

1. 类加载检查

从常量池中定位new的符号引用,看看找个类是不是被加载,解析,初始化过。如果没有再执行相应的类加载过程

2. 分配内存

在堆中直接分配内存,在类加载后会直到该对象需要分配的大小,分配方法有两种(指针碰撞 和 空闲列表),分配方式的选择是由Java堆是否规整决定的

指针碰撞

就是内存如果工整,那么直接就移动指针分配空间

空闲列表

内存不工整,类加载确定出空间后,进行“见缝插针”

线程安全问题

CAS乐观解决

就是继续利用CAS算法尝试去获取内存空间,直到成功,虚拟机就是这么做的

TLAB解决

在堆中给每个线程分配一丢丢空间,让他自己独有,分配的时候就先分配给TLAB空间,
但是这么做的后果就是空间浪费问题

3. 初始化零值

不包括对象头 ,就是给一一些值赋0或者null

4. 设置对象头

对象的hash码,分代年龄等,同时个synchronized锁也在对象头

5. 执行init()方法

可以理解为Jvm已经创建完成,但是这里才是按照程序员的意愿开始创建,有点感觉是构造方法执行

对象的访问方式

  1. 句柄
  2. 直接访问

二者的区别在于对于对象实例数据的处理上,句柄是先访问实例苏话剧的指针,在访问实例数据
直接指针是直接去Heap中去访问实例数据,不过相应的二者访问类型数据都是先去堆中访问类型数据指针再去方法区去拿到类型数据

优缺点分析

如果是读取的话,看上面过程也就直到,直接指针直接读取会更快一些,但是同样的如果
进行删除操作等,需要一个一个的更改数据,效率差,如果是句柄的话直接地址赋null值
就可以了,这个赋null值其实和c++的回收是一样的,直接赋null,就证明这一块空间又
可以被分配利用了

对象的分配策略

大对象,长期存活对象分配到老年代,但是一般的对象有限分配到Eden区

Minor GC, Full GC的区别

MinorGC也就是新生代的垃圾回收,很频繁,而且速度快
FullGC是指老年代的垃圾回收,不是很频繁

对象的死亡判断

程序计数器法

就是一个对象被引用一次那么计数器加一,如果引用失效那么计数器减一,如果为0,则
直接回收

可达性分析法

形象的来说就是以GC Root作为起点,然后查看各个对象到GCRoot是否有一条路劲可以
连起来,如果连不起来则直接回收

补充:一般哪些可以作为GCRoot呢:

通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root
处于激活状态的线程
栈中的对象
JNI(Java Native Interface)栈中的对象
JNI中的全局对象
正在被用于同步的各种锁对象
JVM自身持有的对象,比如系统类加载器等

引用

Java1.2之前定义引用就是reference如果指向的是内存空间的起始地址。

强引用

大多数使用的基本都是强引用,垃圾回收器则不会去回收他,及时空间不足,直接oom,也不会去回收强引用

软引用

区别于强引用,当发生内存不足的时候,才可以被垃圾回收掉。 也正是这样,可以用来
处理对内存比较敏感的高速缓存

弱引用

区别于软引用,只要垃圾回收器扫描到这部分视作垃圾,那么就直接回收掉
可以配合一个引用队列,来查看是是否被GC回收

虚引用

虚引用的实际用途不在乎是引用了什么对象,粗略的说可以说成一种 即将被GC回收的标
志,也是一种跟踪GC回收的一种方法,虚引用必须配合 引用队列来使用,即在某对象回
收之前,则虚引用入队,告诉Jvm该对象即将要被回收,可以在某对象被回收前做一些操作

废弃常量的判定方法

如果是在常量池,而且没用String对象引用,那么就说他是废弃常量,如果内存回收需要的话,直接清理出去

如何判定一个类的废弃

ClassLoader

ClassLoader是一个类加载器,它的工作是将一个类的全限名在Jvm外部进行转化成一个
二进制流,转成二进制流是为了Jvm读取,但是是在虚拟机外部就转化成,这样的目的是
让应用程序自己可以选择这个类,这也是为啥在对象回收时,Jvm Rooter可以是
Classroader的原因

java.lang.Class

反射机制的核心,也就就是所有类自身的一个独有的镜子,在一个类被编译成.class文件的时候,在jvm中运行,会同时自动生成一个和自己创建的类想匹配的Class类

一个类废弃的判定方法

  1. 所有的实例已经被回收掉,堆中不再有这个类的实例
  2. 该类的Classloader也已经被回收
  3. 该类对应的java.lang.class文件没有在任何地方被引用,也无法在任何地方通过反射机制来访问该类

垃圾回收机制的算法

无脑清空法 – 标记清除算法

标记需要回收的对象,然后标记完后,通过一轮回收直接把标记的位置回收

  1. 效率差
  2. 清理完空间不连续

复制填坑法 – 复制算法

将内存空间分成两份,然后将不需要回收的内存(活着的对象)复制到另一块空间去,保证整洁

  1. 需要两份一样的空间
  2. 复制的时候消耗太大

洁癖整理法 – 标记整理算法

也是先标记,但是不同的是,标记以后直接向某一端直接移动,然后GC直接回收边界的无用的空间

分代收集算法

分配空间的空间按照新生代和老年代区别,新生代可以用复制算法,老年代的内存大,而且多,可以使用 标记整理算法,也可以酌情标记清空算法

垃圾收集器

Serial (年轻代)

  1. 单线程收集器
  2. 当进行单线程回收垃圾的时候,其它线程必须停止
  3. 但是对一个客户端的还不错,毕竟切换没那么复杂,而且单线程自然可以调用的资源更多

    ParNew (年轻代)

    上述Serial的一个升级,其实就是升级成了多线程

  4. 多线程收集
  5. stop the world ,还是要停止所有的线程工作

    Parallel Scavenge (年轻代)

    为了吞吐量而减少收集时间,提高收集次数的一种收集算法,(吞吐量:程序运行时间/CPU使用时长)
    适合执行批量处理、订单处理

    Serial Old

    Parallel Old

CMS (老年代)

HotSpot的第一款真正意义上的并发垃圾回收机制,是用的标记清除算法

过程

初始标记

暂停掉所有的线程,标记一下与GC root相连的对象(可达性分析里面的那个GC Root)

并发标记

继续类似可达性分析,标记一系列可达的的队形 GC Root Trancing

重新标记

并发标记是并发的,重新标记是为了修正重新标记期间的用户变动

并发清除

开始对标记的区域做清理

  1. 对CPU资源敏感
  2. 无法清理浮动垃圾
    3 标记清除算法有太多的空间碎片

    G1

    面向服务器的垃圾回收器,针对配备多核处理起的cpu
    宏观上是复制,微观上有标记整理算法
    不区分内存块 Eden surviovor old在微观上还是被保留的

    过程

    初始标记
    并发标记
    最终标记
    筛选回收

过程其实大同小异,相比于CMS其实更多的在于标记的时候多了Remember Set,到了最终标记还有Remeber Set Log合并到了Remeber Set因此避免了浮动垃圾问题
Region在宏观是其实是复制原则,但是在Region的内部其实是标记整理算法
Region内部除了Eden s0 s1 old 还有超大对象,面对超大对象,会选择移动老年代的
对象来为超大内存的对象提供内存

Jvm类加载模式

  1. 加载

    这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口

  2. 验证

    了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求

  3. 准备

    是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

  4. 解析

    虚拟机将常量池中的符号引用替换为直接引用的过程

  5. 初始化

    初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

类加载器

  1. 启动类加载器

    负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被
    虚拟机认可(按文件名识别,如 rt.jar)的类

  2. 扩展类加载器

    负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类

  3. 应用程序类加载器

    负责加载用户路径(classpath)上的类库
    利用双亲委派模型进行类的加载,通过继承java.lang.classloader实现自定义类加载器

                    启动类加载器(Bootstrap ClassLoader)
                                    /\
                                    ||
                    扩展类加载器 (Extension ClassLoader)
                                    /\
                                    ||
                    应用程序类加载器 (Application ClassLoader)
                            /\                  /\
                            ||                  ||
    自定义加载器(User ClassLoader)    自定义加载器(UserClassLoader)
    

双亲委派模型

当一个类收到类加载的请求,他首先不会尝试自己去加载这个类,而是把请求委托给父类去完成,每一层的类加载器都是如此(如上图,箭头向上),因此最终所有的加载类都应该送到启动类加载器去完成,只有当父类的加载器返回自己无法加载这个请求的时候,子类加载器才会自己尝试去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载
器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载
器最终得到的都是同样一个 Object 对象