文章目录
一、JVM体系结构

二、Java内存
1.Java内存划分
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
线程私有的: 程序计数器、虚拟机栈、本地方法栈。
线程共享的: 堆、方法区。
程序计数器
唯一不会出现outofmemoryerror的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
多线程情况下,程序计数器记录当前线程执行的位置,从而当线程被切换回来后知道线程上次运行的位置。
Java虚拟机栈
生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
本地方法栈
与java虚拟机栈的区别是,java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的native方法服务,在HotSpot虚拟机中,这两个栈合二为一。
堆
Java虚拟机管理的最大一块内存,这个区域唯一目的是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆,从垃圾回收角度,可分为新生代和老生代。目的是为了更好的回收内存和分配内存。
方法区
存储已经被虚拟机加载的类信息、常量、静态变量等数据,方法区也被称为永久代,常量池是方法区的一部分。
三、类加载和对象创建过程
1.类加载过程

加载
将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
准备
为类变量(static)分配内存,赋予初始值。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
2.对象创建流程

类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
分配内存
虚拟机为新生对象分配内存。
初始化零值(默认值)
内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值,保证对象实例字段在不赋初始值就可以直接使用。
设置对象头
初始化零值后,虚拟机需要对对象进行必要设置,例如这个对象是哪个类的实例、对象的hashcode、对象的GC分代年龄等信息,存入对象头。
执行init方法
把对象按照程序员意愿进行初始化。
3.类加载
(1)类加载器
加载的作用就是将 .class文件加载到内存。
三个类加载器
BootstrapClassLoader(启动类加载器) :最顶层的加载类,它用来加载 Java 的核心类。由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。不是ClassLoader子类。
ExtensionClassLoader(扩展类加载器) :主要负责加载JRE的扩展目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
(2)类加载机制
双亲委派机制

如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成。每个层次的类加载器都是如此。因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中。只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时。子加载器才会尝试自己去加载。
双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系,避免了类的重复加载。
例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.
(3)如何自定义类加载器
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
4.Java程序初始化顺序
1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法
四、垃圾回收
1.GC回收机制
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
缺点:它很难解决对象之间相互循环引用的问题。
- 可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
可作为GC ROOT的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- Thread活跃线程
2.垃圾回收算法
标记-清除算法、标记-复制算法、标记-整理算法、分代收集算法(主流)。
标记-清除算法
该算法分为标记和清除两个阶段:首先标记处所有不需要回收的对象,在标记完成后统一回收掉没有被标记的对象。
缺点:效率低下;空间问题,在标记-清除后会产生大量不连续的碎片。
标记-复制算法
将内存分为大小相同的两块,每次使用其中一块。当这一块内存使用完毕后,将存活的对象复制到另一块,然后把使用的空间一次性清理。
标记-整理算法
在标记阶段,将所有对象标记为存活和死亡两种状态;在整理阶段将所有存活的对象向一起聚拢,清理到其他地方的内存。
分代收集算法
将Java堆分为新生代和老生代,根据年代的特点选择合适的垃圾收集算法。
新生代对象中,每次收集都会有大量对象死去,所以选择 标记-复制算法
老年代对象中,对象存活几率比较高,并且没有额外的空间对它进行分配担保,选择 标记-清除 或者 标记-整理算法
3.垃圾收集器
(1)GMS
只能回收老年代,需要配合一个年轻代收集器。
以获取最短回收停顿时间为目标的收集器。非常适合在注重用户体验的应用上使用。这是第一款真正意义的并发收集器,实现了让垃圾收集进程和用户进程基本上同时工作。基于标记-清除算法实现。
步骤:
初始标记:暂停其他所有线程,记录下与root相连的对象。
并发标记:同时开启GC线程和用户线程。用一个闭包结构去记录可达对象。这个阶段结束不能保证包含当前所有的可达对象。因为用户线程可能会不断更新引用域,因此GC线程无法保证可达分析性的实时性。
重新标记:暂停其他所有线程,重新标记阶段是为了修正并发标记期间用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记阶段长,但是比并发标记阶段短。
并发清除:开启用户线程,同时GC线程回收所有垃圾对象。
缺点:
没法处理浮动垃圾(并发标记时用户线程产生的垃圾)
会占用大量CPU资源
会产生大量碎片空间
note:对重新标记无法处理浮动垃圾的理解:
由于标记阶段是从 GC Roots 开始标记可达对象,那么在并发标记阶段可能产生两种变动:
本来可达的对象,变得不可达了。
本来不可达的内存,变得可达了。
第一种变动会产生所谓的浮动垃圾,第二种变动怎么回事呢?重点在于miss。
如果并发标记阶段用户线程里 new 了一个对象,而它在初始标记和并发标记中是不会能够从 GC Roots 可达的,也就是were missed。如果没有重新标记阶段来将这个对象标记为可达,那么它会在清理阶段被回收,这是严重的错误,是必须要在重新标记阶段来处理的,所以这就是重新标记阶段实际上的任务。
相比之下,浮动垃圾是可容忍的问题,而不是错误。那么为什么重新标记阶段不处理第一种变动呢?也许是由可达变为不可达这样的变化需要重新从 GC Roots 开始遍历,相当于再完成一次初始标记和并发标记的工作,这样不仅前两个阶段变成多余的,浪费了开销浪费,还会大大增加重新标记阶段的开销,所带来的暂停时间是追求低延迟的CMS所不能容忍的。
(2)G1
G1是一款面向服务器的垃圾收集器,主要针对配有多颗处理器和大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具有高吞吐量性能特征。
步骤:初始标记,并发标记,最终标记,筛选回收。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。保证了G1的收集效率。
G1优点:
不会产生内存碎片;
五、引用
从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用:
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果强引用对象不使用时,即不与GC ROOT相连,才可以被回收。
软引用:
使用 SoftReference 修饰,软引用在内存足够的时候,GC不会回收它。 只有当JVM认定内存空间不足时才会去回收软引用指向的对象。
弱引用:
使用 WeakReference 修饰,GC在扫描它所管辖的内存区域时,只要发现弱引用的对象,不管内存空间是否有空闲,都会立刻回收它。
虚引用:
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。主要用于跟踪对象被垃圾回收器回收的活动。
参考资料
尚硅谷JVM视频教程
狂神JVM视频教程
JavaGuide
本文详细介绍了JVM的体系结构,包括线程私有的和共享的数据区域,如程序计数器、虚拟机栈、本地方法栈、堆和方法区。阐述了类加载和对象创建的全过程,包括加载、验证、准备、解析和初始化。探讨了垃圾回收机制,如引用计数法和可达性分析法,以及GMS和G1垃圾收集器的工作原理。此外,还讨论了Java程序的初始化顺序和四种类型的引用。

被折叠的 条评论
为什么被折叠?



