JVM加载Class文件的原理机制
- 编译阶段,Java源文件被编译成.class文件,包含字节码和元数据。
- 类加载阶段,.class文件由类加载器加载进JVM。类加载器通常采用双亲委派模型,首先请求父类加载器加载,如果父类加载器无法加载则自己加载。
- 链接阶段,将类的二进制数据合并到JVM运行状态之中的过程。包含验证、准备和解析三个步骤。
- 验证:确保加载的类满足JVM规范,没有安全方面的问题。
- 准备:为类的静态变量分配内存,并设置默认初始化值。
- 解析:将类中的符号引用替换为直接引用。
- 初始化阶段,如果该类具有超类,则对其超类进行初始化。然后执行()方法,完成类的静态变量初始化。
- 使用阶段,实例化对象和调用方法等,Class对象在JVM中一直有效,直到JVM退出。
- 卸载阶段,不会主动卸载Class对象。但是如果一个类的所有实例都被回收,对应的Class对象有可能被卸载。
这个加载过程可以保证Class文件被正确加载到JVM,并且在加载过程中做必要的校验和转换,为后续使用阶段做好准备工作。整个加载过程遵循双亲委派模型,先让父类加载器试图加载,父类加载器无法加载时子加载器才会尝试加载。
每个加载的Class在JVM中都有一个对应的Class对象,存储了类的结构信息,方法,变量等数据。这个Class对象一直存在于JVM中,为后续的实例化,反射等提供支持。
GC是什么?为什么要有GC?
GC是Garbage Collection的简称,即垃圾收集。它是Java内存管理的一大特点。
为什么需要GC:
- 程序运行时会不断地生成新的对象,这些对象都需要申请内存空间。如果对象无法被回收,将导致内存占用过高,甚至OOM。
- 有些对象在使用完成后,并不再被引用,但其占用的内存空间无法自动释放。这些无法访问的对象就是"垃圾",需要手动回收其占用的内存空间。
- 手动回收无法访问的对象非常困难和低效。每个对象需要人工判断是否还可访问,然后决定是否回收内存。这几乎是不可能完成的任务。
所以,Java引入了GC来自动管理内存,回收无法访问的对象所占用的内存空间。它可以自动判断哪些对象不可访问,并回收其内存,从而解决手动内存管理的问题,防止内存泄漏。
GC的工作原理:
- GC会自动监控对象的引用关系,当一个对象只被不可访问的对象引用时,它也会成为不可访问的"垃圾"对象。
- 垃圾收集器会在内存即将用尽或JVM空闲时,自动运行,回收不可访问对象的内存空间。
- 通常使用引用计数或者可达性分析等算法来判断对象是否可访问。如果一个对象可以从GC Roots节点可达,那么它就是可访问的。
- 常见的GC算法有标记清除、标记压缩、分代收集等。不同的算法适用于不同的场景。
GC就是JVM自动内存管理的一种方式,它可以自动回收不可访问对象占用的内存空间,从而防止内存泄漏,保证程序的正常运行。
Java垃圾回收机制
- GC Roots:包括活动线程栈(栈帧中的局部变量)、方法区中类静态属性引用的对象、方法区中常量引用的对象等。
- 可达性分析:从GC Roots出发向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则该对象是不可用的。
- 垃圾收集算法:
- 标记清除:标记出不可达对象,然后统一回收。会产生大量内存碎片。
- 标记压缩:标记不可达对象,然后移动可达对象,最后清理掉边界以外的内存空间。可以减少内存碎片。
- 分代收集:将内存分为新生代和老年代,新生代采用复制算法,老年代采用标记清除/压缩算法。新生代对象容易回收,老年代对象存活时间长。
- 垃圾收集器:
- Serial:单线程收集器,STW(Stop The World)时间长,但简单高效。适用于小内存和单CPU环境。
- Parallel:多线程收集器,STW时间短,CPU密集型,适用于中等并发的服务器环境。
- CMS:并发收集器,心臟不停跳。并发收集消度高但碎片多,适用于快速响应的环境。
- G1:分代收集器,并行和并发相结合,整理碎片和回收效率高,适用于大内存和并发环境。
- 垃圾收集调优:根据应用的特点选择合适的垃圾收集器和相应的参数配置,达到一个短GC停顿时间和高吞吐量的平衡。
概括来说,Java垃圾回收机制是通过GC Roots与对象之间的可达性分析来判断对象是否存活,然后采用不同的垃圾收集算法和收集器来回收不可用对象的内存,实现自动内存管理。
如何判断一个对象是否存活?(或者GC对象的判定方法)
在Java中,对象是否存活主要通过可达性分析来判断。可达性分析的基本思想是:
如果一个对象可以被GC Roots直接或间接引用到,则该对象是存活的。
如果一个对象无法被GC Roots可达,则该对象是不可用的,可以被回收。
GC Roots包括:
- 虚拟机栈(栈帧中的局部变量区)中的引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
那么,判断一个对象是否存活的基本步骤如下:
- 找出所有的GC Roots对象。这些对象是存活的,并作为开始搜索的起点。
- 从GC Roots开始向下搜索,如果一个对象与GC Roots之间有直接或间接的引用关系,那么这个对象也是存活的。
- 重复步骤2,直到没有更多可达的对象。可达的对象都属于存活对象。
- 没有被搜索到,或者无法从GC Roots开始搜索到的对象就是不可用对象,可以被垃圾收集器回收。
- 垃圾收集器通过搜索GC Roots开始的对象网,把所有可达对象都标记为存活,然后清理掉那些没有标记的对象。
举个例子:
java
obj1 → obj2 → obj3 → obj4
- 如果obj1被GC Roots引用,那么obj1、obj2、obj3和obj4都是存活的。
- 如果只有obj3被GC Roots引用,那么只有obj3和obj4是存活的,obj1和obj2会被回收。
- 如果整个对象网中没有一个对象被GC Roots引用,那么所有对象都会被回收。
所以,判断对象是否存活,实际上就是判断该对象是否能被GC Roots直接或间接可达到。如果一个对象与GC Roots之间完全没有引用链相连,那么该对象将会被垃圾收集器回收。
垃圾回收的优点和原理
- 自动内存管理:程序员不需要手动释放内存,GC可以自动回收不再使用的对象,简化编程难度。
- 避免内存泄漏:忘记手动释放内存会导致内存泄漏,GC可以自动释放不可达对象,避免内存泄漏。
- 分配内存效率更高:GC自动管理内存,通过复用回收得来的内存,分配内存时无需从操作系统获取,更加高效。
垃圾回收的基本原理:
- 找出GC Roots:栈帧中的局部变量、静态属性、常量等。
- 从GC Roots开始向下搜索,找到所有可达对象。可达对象就是存活对象。
- 搜索完成后,剩下的对象就是不可达对象,可以被回收。
- 垃圾收集器采用不同算法对内存进行回收,常见算法有:- 标记清除:标记出不可达对象,统一回收。会产生大量内存碎片。
- 标记压缩:标记不可达对象,移动可达对象,回收碎片内存。减少内存碎片。
- 分代回收:新生代使用复制算法,老年代使用标记清除/压缩算法。
考虑2种回收机制:
- 标记清除:简单,运行速度快,但会产生大量内存碎片。适用于新生代。
public void gc() {
mark(); // 标记所有可达对象
sweep(); // 清除所有未标记对象
}
- 标记压缩:需要付出额外代价移动内存,但可以减少内存碎片。适用于老年代。
public void gc() {
mark(); // 标记所有可达对象
compact(); // 移动可达对象,回收内存碎片
}
标记清除通过直接释放不可达对象来回收内存,速度快但会有大量内存碎片。标记压缩需要移动可达对象,额外付出性能代价,但可以减少内存碎片。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回啊收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
垃圾回收器的基本原理:
- 找出GC Roots:栈帧中的局部变量、静态属性、常量等。
- 从GC Roots开始向下搜索,找到所有可达对象。可达对象就是存活对象。
- 搜索完成后,剩下的对象就是不可达对象,可以被回收。
- 垃圾收集器采用不同算法对内存进行回收,常见算法有:标记清除、标记压缩、分代回收等。
垃圾回收器不能马上回收内存,主要有以下原因:
- 垃圾收集需要付出较高的性能开销,频繁回收会严重影响程序性能。
- 回收内存需要STW(Stop The World),停止用户线程,如果回收太频繁会影响程序响应性。
- 部分垃圾收集器(CMS)是并发的,需要等待用户线程释放对象引用,否则会导致既存的可达对象被错误回收。
主动通知虚拟机进行垃圾回收的方法:
- System.gc():建议JVM进行垃圾回收。JVM会根据系统运行情况自行决定是否执行回收。
- Runtime.getRuntime().gc():同上,尝试建议JVM进行回收。
- Xms和Xmx:可以设置初始大小和最大大小来产生内存溢出,触发full gc。但这种方式不可靠,不推荐。
- Profile触发:通过开启GC Profiling,让Profile决定何时触发Young/Full GC。但这也依赖于运行环境,不可靠。
- 调整垃圾收集器及内存参数:可以通过-XX选项设置垃圾收集器类型和内存参数来间接影响GC频率。这是比较可靠的方式之一。
-XX:+UseParallelGC # 设置为并行收集器
-XX:MinHeapFreeRatio=40 # 最小空闲比例触发YGC
-XX:MaxHeapFreeRatio=70 # 最大空闲比例触发Full GC
所以,总结来说,虚拟机会根据系统运行情况决定何时进行垃圾回收,我们无法完全主动控制GC的时机。但是,我们可以通过调整JVM内存参数或垃圾收集器类型等方式间接地影响垃圾回收的频率,在一定程度上主动通知虚拟机进行垃圾回收。
Java中会存在内存泄漏吗
Java中由于有垃圾回收机制,可以减少很大一部分内存泄漏的可能,但是仍存在一些场景会产生内存泄漏:
- 忘记释放非GC管理的内存:像NIO的DirectByteBuffer等需要手动释放,忘记释放会内存泄漏。
- 静态引用的对象:如果一个对象的生命周期很长,但只有静态引用,容易内存泄漏。因为静态引用的生命周期跟类一样长。
- 发生在线程或线程池使用过程中:如果线程池中某个线程一直持有一个对象的引用,而这个对象很大,容易内存泄漏。
- 网络连接或数据库连接没有正确关闭:这些连接如果长期未关闭,会持有大量资源导致内存泄漏。
- 监听器使用不当:如果监听器注册后没有正确unregister,会对资源持有引用而内存泄漏。
- 映射使用不当:像Map等映射如果keyDisappear后value没有释放,会产生内存泄漏。
解决办法:
- 及时关闭外部资源:如数据库连接、网络连接、文件流等。
- 尽量不要使用静态引用持有大对象。如果必须使用,应及时设置为null释放。
- ThreadLocal及时remove:如果线程结束,应移除ThreadLocal中的引用以释放内存。
- 注册监听器后及时unregister:监听器不再使用应立即unregister以释放资源。
- 使用WeakReference或SoftReference:如果对象生命周期不确定,可以使用弱引用或软引用封装,避免内存泄漏。
- 映射中使用WeakHashMap:如果key可能消失,可以使用WeakHashMap,其entry会自动gc,防止内存泄漏。
7.及时显式调用资源的close方法:像NIO的DirectByteBuffer就需要手动调用free释放内存。
- 避免高强度的对象层级:对象的层级关系太深,层与层之间相互引用,使得整体对象链条难以被回收,容易发生内存泄漏。
所以,总结来说,虽然Java的GC可以减少大部分内存泄漏,但是仍需程序员在设计和编码时多加留意,及时释放无用资源,避免产生难以被GC的对象,从而彻底解决内存泄漏问题。良好的编码习惯可以极大的减少内存泄漏