
java源代码经过编译 ---> java字节码 ---> JVM 创建 main 主线程,使用的内存由虚拟机栈分配 --->
类加载子系统将类的原始信息加载到方法区 ---> 实例对象存储在堆中 ---> 局部变量和方法的参数存到虚拟机栈
--->普通java方法的调用存在虚拟机栈 ---> 本地方法的调用存在本地方法栈 ---> 当前线程执行到第几行代码
存在程序计数器中---> 当对象不再使用,当内存不足时,会被垃圾回收回收
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:方法区、堆
虚拟机栈默认大小:1M
只有程序计数器不会出现内存溢出
(1)OutOfMemoryError的情况:
堆内存耗尽 -- 对象越来越多,又一直在使用,不能被垃圾回收
方法区内存耗尽 -- 加载的类越来越多,很多框架都会在运行期间动态产生新的类
虚拟机栈累积 -- 每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁
(2)StackOverflowError
虚拟机栈内部 -- 方法调用次数过多
(1)方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
(2)永久代是Hotspot虚拟机对JVM规范的实现(JDK1.8之前)
放在堆上,是方法区的实现
(3)元空间是Hotspot虚拟机对JVM规范的实现(JDK1.8之后),使用本地内存作为这些信息的存储空间
放在直接内存。是方法区的实现
(1)硬件的发展,从之前的32位机发展为64位机,就不用担心内存不够用
(2)将方法区放在堆上,需要手动的调整需要空间的大小
最小是20.75M,最大无限。
最小大小和最大大小要设置为一样,防止内存抖动。
元空间建议设置为物理内存的1/32

标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,
如果找到了某个对象,就加标记,在垃圾回收时不会回收
清除:
没有加标记的对象直接释放内存
问题:被释放的内存不连续,会存在过多的内存碎片

标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,
如果找到了某个对象,就加标记,在垃圾回收时不会回收
整理:
没有加标记的对象直接释放内存,将标记的对象移动到一端
问题:效率低,解决了内存碎片问题
常用于老年代(存活对象较多的区域)的垃圾回收

标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,
如果找到了某个对象,就加标记,在垃圾回收时不会回收
复制:
将被标记的对象复制到另一个区域,将其他清除
问题:相比于标记整理效率提高了,但是占用了额外的内存
常用于新生代(存活对象较少的区域)的垃圾回收,不适用于老年代(存活对象多的区域)的垃圾回收

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

(1)三色标记

(2)并发漏标
用户线程对垃圾回收线程的影响
解决漏标问题:
(1)Incremental Update
只要赋值发生,被赋值的对象就会被记录。最后对这些被记录的对象进行重新标记
(2)Snapshot At The Beginning,SATB
新加对象会被记录,
被删除引用关系的对象也会记录。最后对这些被记录的对象进行重新标记
(1)Parallel GC
伊甸园区内存不足发生 Minor GC,标记赋值 STW
老年代内存不足发生 Full GC,标记整理 STW
注重吞吐量
(2)ConcurrentMarkSweep GC
老年代并发标记,重新标记时需要STW,并发清除
Failback Full GC(清除速度不及需要创建新的速度时,开始老年代和新生代都释放)
注重响应时间
(3)G1 GC
响应时间与吞吐量兼顾
把堆内存划分成多个区域,每个区域都可以充当伊甸园区、幸存区、老年区
新生代回收:伊甸园区内存不足时,标记复制 STW,复制到幸存区
并发标记:老年代达到堆内存的45%会触发并发标记,老年代并发标记,重新标记时需要 STW
混合收集:并发标记完成,开始混合收集,参与复制的有伊甸园区、幸存区、老年代,其中老年代会根据暂停时间目标,
选择部分回收价值高的区域,复制时 STW
Failback Full GC


(1)误用固定大小线程池
它的工作队列可以放整数最大值数量的任务对象,任务对象占用的内存会导致内存溢出
使用线程池的时候,根据实际情况,定义有大小限制的任务队列
(2)误用带缓冲线程池
对最大救急线程数量没有限制,因此会造成内存溢出
自己控制最大救急线程数量
(3)查询数据量太大导致
注意不要一次性查询所有数据
(4)动态生成类过多导致内存溢出
(1)加载
将类的字节码载入方法区,并创建类.class对象(在堆内存)
如果此类的父类没有加载,先加载父类
加载时是懒惰执行
(2)链接
验证--验证类是否符合Class规范,合法性、安全性检查
准备--为static变量分配空间,设置默认值
解析--将常量池的符号引用解析为直接引用
(3)初始化
执行静态代码块与非final静态变量的赋值
初始化是懒惰执行
final修饰的基本类型变量使用时不会进行类的加载,会将变量直接复制到自己的类中
final修饰的引用类型变量使用时会进行类的加载
class Student{
static final int age=1;
static final String name="lisi";
}
class test{
System.out.printlf(student.age);//不会加载student类信息
System.out.printlf(student.name);//会加载student类信息
}
类加载器加载时的工作方式:优先委派上级类加载器进行加载,如果上级类加载器
能找到这个类,由上级加载,加载后该类也对下级加载器可见
找不到这个类,则下级类加载器才能有资格执行加载
目的:
让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类
让类的加载有优先次序,保证核心类先加载

普通变量赋值即为强引用,如:Dog dog = new Dog();
通过 GC Root 的引用链,如果强引用找不到该对象,该对象才能被回收

例如:SoftReference a = new SoftReference(new A());
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
软引用自身需要配合引用队列来释放
典型例子是反射数据

例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放
典型例子是 ThreadLocalMap 中的 Entry 对象

例如:PhantomReference a = new PhantomReference(new A());
必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由 Reference Handler 线程
释放其关联的外部资源
典型例子:Cleaner 释放 DirectByteBuffer 占用的直接内存

(1)它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些
资源释放和清理工作
(2)但是将资源释放和清理放在finalize方法中不太好,非常影响性能,严重时甚至会引起OOM(内存溢出),
从java9开始就被标注为@Deprecated,不建议使用了
非常不好:
(1)FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
(2)会吞掉代码里的异常,导致不能判断在释放资源时是否出现错误
影响性能:
(1)重写了finalize方法的对象在第一次 GC 时,并不能及时释放它占用的内存,因为要等着
FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,
第二次 GC 时才能真正的释放内存
(2)finalize的调用很慢,当内存不足时不能及时的释放内存,对象释放不及就会逐渐移入老年代,
老年代对象积累过多就会容易 full GC ,导致速度很慢。甚至 full GC 后如果释放的速度仍然跟不上
创建新对象的速度,就会OOM(内存溢出)


(1)引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库
(2)扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
(3)应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载自己写的类
(4)自定义类加载器:负责加载用户自定义路径下的类
(1)就是先找父类加载器进行加载,加载不到再由子类加载器自己加载
(2)比如我们自己写的一个类,最先会找应用程序类加载器,应用程序类加载器会委托扩展类加载器,
扩展类加载器再委托引导类加载器;引导类加载器再自己的类路径没找到你写的类,则向下退回
加载类的请求,扩展类加载器收到回复就自己加载,在自己的类路径里没找到,又向下退回类的
加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找到了自己写的类,然后
就自己加载了
为什么会有双亲委派机制:
(3)沙箱安全机制,防止核心API类库被随意篡改,用户自己写的类如果和原有的类相同,则不会加载
(4)避免类的重复加载:当父类已经加载了该类时,就没有必要子类加载器再加载一次,保证加载类的唯一性

(1)tomcat底层类加载打破了双亲委派机制
(2)一个 tomcat web 容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的
不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,
保证相互隔离

(1)类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到
一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。
如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是:new关键字、对象克隆、对象序列化等
(2)分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载
完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来
(3)初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
(4)设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何
才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象
的对象头中。
(5)执行init方法:即对象按照程序员的医院进行初始化,对应到语言层面上讲就是为属性赋值和执行构造方法
(1)该类的所有对象实例都已经被回收,也就是java堆中不存在该类的任何实例
(2)加载该类的类加载器已经被回收
(3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
(1)字符串的分配和其他对象的分配一样,消耗高昂的时间与空间代价,作为最基础的数据类型,
大量频繁的创建字符串,极大程度的影响程序的性能
(2)JVM为了提高性能和减少内存开销,为字符串开辟一个字符串常量池,创建字符串常量时,首先查询
字符串常量池是否存在该字符串;如果存在该字符串,则返回引用实例,不存在,则实例化该字符串
并放入池中
(1)java源代码被编译器编译成字节码文件
(2)加载:将字节码文件加载进入内存,在堆上生成一个代表这个类的Class对象,将这个字节流所
代表的静态存储结构转化为方法区的运行时数据结构
连接:验证、准备、解析
(3)验证:这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会
危害虚拟机自身的安全
(4)准备:正式为静态变量分配内存并设置静态变量初始值的阶段,这些变量所使用的内存都将在方法
区中进行分配。这时候进行内存分配的仅包括静态变量,而不包括实例变量,实例变量将会
在对象实例化时随着对象一起分配在java堆中
(5)解析:虚拟机将常量池内的符号引用替换为直接引用的过程
(6)初始化:对类的静态变量进行初始化
(1)类加载检查:首先检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个
符号引用代表的类是否已经进行过类加载过程,如果没有,那必须先执行相应的类加载
过程
(2)分配内存:类加载检查通过后,接下来JVM将为新生对象分配内存。对象所需内存的大小在类加载
完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分
出来
(3)初始化零值:内存分配完成后,JVM需要将分配到的内存空间初始化为零值(不包括对象头),这一
操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问
到这些字段的数据类型所对应的零值
(4)设置对象头:初始化零值完成之后,JVM要对对象进行必要的设置,例如这个对象是哪个类的实例、
如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息
存放在对象头中。另外,根据JVM当前运行状态的不同,如是否启用偏向锁等,对象头
会有不同的设置方式
(5)执行init():在上述工作都完成之后,从JVM视角来看,一个新的对象已经产生了,但从程序员角度
来看,对象创建才刚开始,init()还没有执行,所有的字段都还为零。所以一般来说
执行new指令之后会介者执行init(),把对象按照程序员的意愿进行初始化,这样
一个真正可用的对象才算完全产生出来
(1)允许开发人员提供对象被销毁之前的自定义处理逻辑
(2)在垃圾回收某个对象之前,总会先调用这个对象的finalize(),finalize()只会被调用一次
(3)finalize()允许在子类中被重写,用于在对象被回收时进行一些资源释放和清理的工作,比如
关闭文件、套接字和数据库连接等

(1)记录正在执行的JVM字节码指令的地址
(2)为了线程切换后能恢复到正确的位置,每个线程都需要一个独立的程序计数器,各个线程之间
互不影响,独立存储
(1)描述方法执行的内存模型,每个方法在执行时都会创建一个帧栈,每个帧栈存放的是局部变量表,
操作数栈,动态链接,方法出口等信息
(2)方法被调用到执行完成对应的是一个栈帧从入栈到出栈的过程,是线程私有的
存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
(1)堆中存储的是对象实例,只需要不断地创建对象,并保证 gc roots 到对象之间有可达路径来
避免垃圾回收机制清除这些对象,当对象数量到达最大堆容量限制之后,产生内存溢出
当线程请求的栈深度超出了JVM允许的最大深度时,就会抛出StackOverFlowError异常
(1)引用计数法:给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将引用计数器
+1;引用失效时,计数器-1。当一个对象的引用计数器为0时,说明此对象没有被引用,
将会被垃圾回收
(2)可达性算法:从 GC Roots 开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,
说明此对象不可以用
(1)Minor GC的触发条件:伊甸园区满时
(2)Full GC的触发条件:调用System.gc时,系统建议执行Full GC,到那时不必然执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(1)多数的java应用不需要再服务器上进行GC优化
(2)多数导致GC问题的java应用,都不是因为参数设置错误,而是代码问题
(3)在应用上线前,先考虑将JVM参数设置到最优
(4)减少对象创建的数量
(5)减少全局变量和大对象
(6)GC优化是最后不得已才使用的手段,在实际应用中,分析GC情况优化代码比优化GC参数要多