JVM
JVM,JDK和JRE的区别
- (1) JVM是运行java字节码的虚拟机,JVM有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。
(2) JDK是功能齐全的java sdk,拥有JRE所拥有的一切,还有编译器和工具,能够创建和编译程序。
(3) JRE是java运行时环境,包括jvm,java类库,java命令和其他一些基础构建。
JVM运行时数据区

线程共享的区域:方法区,堆。 线程私有的区域:虚拟机栈,本地方法栈,程序计数器
程序计数器
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置。当线程被切换回来时,能够知道该线程上次运行到哪里了。
- 程序计数器是唯一一个不会出现outofmemory的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
java内存可以粗糙的分为堆内存和栈内存,其中栈就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
局部变量表,操作数栈,动态链接,方法出口
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。
本地方法栈
- 虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。
方法区
- 存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。jdk1.8时,方法区被彻底移除了,取代的是元空间,元空间使用的是直接内存。
堆
- 堆用来存放对象实例,几乎所有对象实例以及数组都在这里分配内存。
- 分为年轻代和老年代,比例为1:2
- 年轻代分为Eden区,survivor0区,survivor1区,比例为8:1:1
- 年轻代使用标记复制算法,老年代使用标记清除算法。
堆中minorGC的过程
- 初始阶段,新创建的对象被分配到Eden区
- enden区满了之后,触发minor GC, 将存活对象复制到S0区,不存活的对象被回收。
- 当enden区再次满了之后,将enden区和s0区的存活对象复制到s1区,不存活对象被回收。交换s0和s1区。
- 后面则重复第二步。
堆和栈的区别
- (1) 栈是线程私有的,堆是线程共享的。
(2) 栈存放的是局部变量表,操作数栈,动态链接和方法出口。而堆存储对象,分为新生代和老年代。
(3) 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
方法区存了什么?
- 存储被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。
局部变量表存了什么?
- 8种基本数据类型,对象引用类型。
方法区会溢出吗?
- string.intern()方法的作用是,如果字符串常量池中已经包含了此String对象的字符串,则返回引用。否则将string对象添加到常量池中,再返回引用。
- 如果使用intern()方法向常量池中放入了太多的字符串对象,就很容易填满常量池,发生OutOfMemory异常。
对象的构成
- (1) 一个对象分为3个区域,对象头、实例数据、对齐填充(8字节的整数倍)。
- 对象头主要包括两部分,1.存储自身运行时数据比如hash码,分代年龄,锁标记等。2.指向类的元数据指针。
- 实例数据部分是对象真正存储的有效信息,也是在程序中定义的各种类型的字段内容。
- 对齐填充仅仅起占位作用。因为hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,而对象头正好是8字节的整数倍,因此当对象实例数据部分没有对齐是,就需要对齐填充来进行补全。
对象的访问方式
使用句柄. java堆中会专门有一块区域作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体地址信息。

直接指针,reference中存储的是对象地址,而对象中存放了类型数据的相关信息。

总结:
- 使用句柄来访问的好处是reference存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要修改。
- 使用直接指针的而好处是速度快,它节省了一次指针定位的开销。
JVM对象创建到销毁的过程

(1) 对象创建需要经历类加载->分配空间->初始化零值->设置对象头->执行构造函数的init方法
(2) 当一个对象不再被引用,它就会被GC回收.类加载:
- 虚拟机遇到一条new指令时,首先检查该指令的参数是否能在常量池中定位到这个类的符号引用。并且检查这个符号引用代表的类是否已经被加载过,没有则必须执行相应的类加载过程。
分配内存
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存大小在类加载后便可确定。分配方式有”指针碰撞”和“空闲列表”。
- 指针碰撞:堆规整(没有内存碎片) 复制算法
- 空闲列表:堆内存不规整的情况下,虚拟机会维护一个列表,该列表会记录哪些内存块是可用的。 标记清除算法
并发问题
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
初始化零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在java代码中可以不赋初值就可以直接使用。
设置对象头
- 对象头中存放类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。
执行init方法
- 执行new指令后会接着执行方法,把对象按照程序员的意愿进行初始化。
JVM类加载顺序
- 加载,获取类的二进制字节流
- 校验,文件格式校验
- 准备,设置类变量的初始值,即数据类型默认的初始值。
- 解析,将常量池中的符号引用替换为直接引用的过程
- 初始化,为类的静态变量赋予正确的初值
类加载过程
加载
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的class对象,作为方法区这些数据的访问入口
验证
文件格式验证,主要验证class文件是否规范。
元数据验证:对字节码描述的信息语义分析等。
字节码验证:确保语义是ok的。
符号引用验证:确保解析动作能执行
准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
public static int value=111,在准备阶段value的值是0。public static final int val = 111,如果加了final关键字,那么value的值就是111
解析
- 虚拟机将常量池中的符号引用替换为直接引用的过程。
初始化
- 执行类构造器方法
<clinit>的过程
- 执行类构造器方法
类加载器
bootstrapclassloader根加载器extensionClassLoader扩展类加载器AppClassLoader应用程序类加载器
双亲委派机制

- 在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把请求委派给父类加载器的
loadClass()处理,因此所有请求最终都应该传送到顶层的根加载器中,当父类加载器无法处理时,向下递归,最后才由自己定义的类加载器进行加载。 好处:
- 避免类的重复加载,同一个类由不同类加载器加载出来是不同的类。
- 保证了java的核心API不被篡改。
什么情况下会进行类加载?
遇到
new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。发生这4条指令最常见的java代码场景是:- 使用new关键字实例化对象时。
- 读取或设置一个类的静态字段以及调用一个类的静态方法时。(被final修饰的字段除外)
- 反射创建对象时
- 初始化一个类时,发现父类还没有初始化,需要先进行父类的初始化。
- 虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
JMM内存模型
(1) JMM内存模型规定所有变量都存储在主内存中,每个线程都有自己的工作内存,线程的工作内存中保存了被该线程使用的主内存副本,线程对变量的所有操作都在自己的工作内存中完成,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量。
(2) 8种内存间交互操作。
① Lock/unlock 把一个变量标识为一条线程独占的状态/将一个处于锁定状态的变量释放出来。
② Read/write 将主内存中的变量传输到线程的工作内存中/将工作内存中的变量写入到主内存中
③ Load/store 将read操作的变量放入工作内存的变量副本/将工作内存的变量传输到主内存中,以便write操作使用
④ Use/assign 将工作内存的变量传递给执行引擎/将从执行引擎接收的值赋值给工作内存的变量
GC的安全点
- (1) 循环的末尾
- (2) 方法返回前
- (3) 调用方法后
- (4) 抛出异常的位置
minor GC和full GC的触发条件
- Eden区满时,触发minor GC
FULL GC触发条件:
- 调用system.gc时,系统建议执行full gc
- 老年代空间不足
- 方法区空间不足
- 通过minor GC后,进入老年代的平均大小大于老年代的可用内存
为什么要把大的对象直接放入老年代
- (1) 避免在eden区和两个survivor区之间来回复制,产生大量的内存复制操作。
长期存活的对象进入老年代
- 如果对象在eden出生并经过第一次minor GC后,仍能存活,将其移动到survivor空间中,并将对象年龄设置为1,对象在survivor区每熬过一次minor gc,年龄就增加1岁。当它的年龄增加到15岁时,就会被晋升到老年代。
如何判断对象死亡?
引用计数法
- 给对象加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效,计数器减1,任何时候计数器为0的对象就是不可能再被使用的。
- 缺点:不能解决对象之间相互循环引用的问题。
可达性分析
- 通过一系列的称为GC Roots的对象作为起点,从这些结点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连的时候,证明此对象是不可用的。
可以被作为GC Roots的对象有哪些?
- (1) 虚拟机栈中引用的对象
(2) 在方法区中类静态属性引用的对象
(3) 在方法区中常量引用的对象
(4) 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象还有系统类加载器。
(5) 所有被同步锁持有的对象
什么是内存泄漏?如何防止?
内存泄露指应用程序不再使用的对象,垃圾收集器无法删除它们,因为它们正在被引用。
造成原因:
① 静态集合类引起内存泄露,静态集合类引用的对象不能被释放。
② 未能及时关闭资源,比如未能及时关闭数据库连接。
③ 非静态内部类持有外部引用(隐式引用)。
④ 单例模式持有外部的引用,单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么外部的引用不能被GC回收,导致内存泄露。
如何防止:
- ① 尽早释放无用对象的引用,在使用临时变量时,让引用变量在退出活动域后置为null.
- ② 尽量少用静态变量,因为静态变量时全局的,放在方法区,GC不会回收。
- ③ 避免集中创建对象,尤其是大对象,可以的话尽量使用流操作。
- ④ 尽量不要在循环中创建对象。
引用类型
强引用
- new出来的对象都是强引用,无论任何情况,只要强引用关系还存在,GC就永远不会回收掉被引用的对象。
软引用
- 如果内存空间足够,GC就不会回收软引用。如果内存空间不足,就会回收这些对象的内存。软引用可以用来实现内存敏感的高速缓存,比如缓存图片。
弱引用
- 第二次minor GC会被回收。
虚引用
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
- 虚引用主要用来跟踪对象被垃圾回收的活动。
如何判断一个常量是废弃常量?
- 运行时常量池主要回收的时废弃的常量。假如在常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量”abc”就是废弃常量,如果此时发生内存回收的话,“abc”就会被请理出常量池。
如何判断一个类是无用的类?
- 该类的所有实例已经被回收
- 加载该类的classloader已经被回收
- 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
标记—复制算法
- 将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完之后,就将还存活的对象复制到另外一块去,然后再把使用的空间一次请理掉。每次内存回收都是对内存空间的一半进行回收.
- 优点:没有内存碎片
- 缺点: 浪费空间,只使用了一半空间
标记-清除算法
- 该算法分为”标记”和”清除”阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象.
- 优点:不需要额外的空间
- 缺点:两次扫描,严重浪费时间,会产生内存碎片.
标记-整理算法
- 在标记-清除算法的基础上,让所有存活对象往一端移动,然后直接请理掉边界以外的内存.
- 优点:空间比较规整
- 缺点:移动让内存回收更加复杂
- 年轻代一般使用标记-复制算法,老年代一般使用标记清除或标记整理算法.
垃圾收集器

Serial收集器
- 它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
- 新生代采用复制算法,老年代采用标记-整理算法
ParNew收集器
- parNew收集器是serIal的多线程版本,除用多线程进行垃圾收集外,其余行为和serial收集器完全一样.
parallel scavenge收集器
- 关注点是吞吐量,吞吐量是CPU中用于运行用户代码的时间和CPU总消耗时间的比值.
serial Old 收集器
- serial收集器的老年代版本
parallel Old收集器
- parallel scavenge收集器的老年代版本,在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS收集器
- CMS(concurrent mark sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器.它第一次实现了让垃圾收集线程与用户线程基本上同时工作.
CMS收集器的工作过程分为以下四个步骤:
- 初始标记: 暂停所有其他线程,并记录直接与GC root相连的对象,速度很快.
- 并发标记:从GC root直接关联对象开始遍历整个对象图的过程,耗时长,但不需要停顿用户线程.
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录.停顿时间比初始标记稍长,但远比并发标记短.采用多线程并发执行来提高效率. CMS用增量更新来实现,即将黑色对象到白色对象的引用关系记录下来,黑色对象变为灰色,需要被重新扫描.
- 并发清除: 清理删除掉标记阶段判断的已经死亡的对象,可以与用户线程同时工作.
缺点:
对CPU资源十分敏感
- 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低.
无法处理浮动垃圾
- 在并发清除时,用户线程新产生的垃圾,称为浮动垃圾.
- 使得并发清除需要预留一定的内存空间,不能像其他收集器在老年代几乎填满才进行收集.
产生大量内存碎片
- 由于CMS基于标记-清除算法,清除后不进行压缩操作,空间碎片过多时,将会给大对象分配带来麻烦.
G1收集器
并行与并发: G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU来缩短 Stop-The-World 停顿时间。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
可预测停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型.
工作过程:
- 初始标记 仅标记一下GC Roots能直接关联到的对象
- 并发标记 耗时较长,但不能保证可以标记出所有存活对象.
- 最终标记 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录. G1使用原始快照记录引用关系变动的对象.
- 筛选回收 首先排序各个Region的回收价值和成本,然后根据用户期望的GC停顿时间来制定回收计划.回收时采用复制算法,从一个或多个region复制存活对象到堆的另外一个空的region,并在此过程中压缩和释放内存.
GC性能调优
- 设置堆的最大值和最小值 -Xmx -xms
导致full GC的原因
- 老年代空间不足
- 永久代空间不足
- 显示调用system.gc()
Full GC内存泄漏排查
- jps 获取java进程号
- jmap -dump 生成堆内存快照
- 使用可视化分析工具如javavisum进行排查
CPU100%排查
- 使用top命令找出cpu占用最高的进程。
- Top -hp pid 查看该进程的各个线程运行情况
- Jstack pid 查看当前线程cpu高的原因
