JVM内存模型篇
1、JVM的位置
包含在JRE中,在操作系统之上。
-
结构模型图:
-
JDK7和JDK8的区别
2、JAVA程序执行顺序
-
.java源代码文件
-
通过编译器编译为字节码.class文件
-
通过类加载器(class loader)把字节码文件加载到内存当中
-
通过字节码校验器传递给解释器
-
解释器对字节码进行逐行翻译,翻译为系统可以理解的机器码
-
将机器码交给操作系统,操作系统以main方法作为入口开始执行程序。
3、类加载器
作用:加载字节码.class文件。
3.1、根加载器(Bootstart Class Loader)
用来加载java的核心类,由C++实现,不是java.lang.ClassLoader的子类。负责加载$JAVA_HOME中jre/lib/rt.jar里的所有class。
3.2、扩展类加载器(Extensions Class Loader)
负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由JAVA语言实现。
3.3、系统(应用)类加载器(System Class Loader)
负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或者$CLASSPATH将变量所指定的JAR包和类路径。没有特别指定,则用户自定义的类加载器都以此类加载器作为附加在其。由java语言实现,父类加载器为ExtClassLoader。
4、双亲委派机制
说明:一个类加载器收到了类加载请求并不会自己先去加载,而是把这个请求委托给父类加载器去执行,父类加载器若还存在其父类加载器则依次向上委托,直到到达顶层启动类加载器。若父类加载器可以完成类加载任务,就直接返回,否者子加载器才会尝试自己去加载。
优势:具备了优先级的层级关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,就没必要让子类加载器再加载一次了。其次,考虑到安全因素,java核心api中定义的类不会被随意替换,当出现自定义的类与java核心api中的类类型一样时,通过双亲委派机制传递到启动类加载器,而启动类加载器在java核心api中发现同类型的类已经被加载,就不会加载自定义的类,而是直接返回已经加载了的java核心api中的类,这样便可以防止java核心api库被随意篡改了。
5、沙箱安全机制
将java代码限定在jvm特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
6、Native
用作java和其他语言(如C++)进行协作时用的。
由于java是跨平台的语言,所以就牺牲了一些对系统底层的控制,而要实现对底层的控制,就需要一些其他语言的帮助,这就是native的作用了。
7、 程序计数器(PC寄存器)
在jvm中,多线程是通过线程轮流切换来获取CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令。因此,为了能够使得每个线程在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且互不干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出(OutMemory)现象的。
8、方法区
线程共享的区域。存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中。
9、栈
作为数据结构具有先进后出的一个特性,与之该对应的结构是队列(先进先出)。
存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、
指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、
方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
其实大都存放的一些具体内容的引用(比如对象和实例方法)。
栈是线程级别的。对于栈来说,不存在垃圾回收的问题,因为线程结束,栈内存就释放。
10、堆
堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。
-
堆调优设置常用参数
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小 |
-Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:NewRatio | 新生代和老年代的比值 |
-XX:SurvivorRatio=8 | 值为4 表示 新生代:老年代=1:4,即年轻代占堆的1/5年轻代中Eden区与Survivor区的容量比例值,默认为8 |
-XX:+HeapDumpOnOutOfMemoryError | 表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10内存溢出时,导出堆信息到文件 |
-XX:+HeapDumpPath | 堆Dump路径-Xmx20m -Xms5m-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=d:/a.dump |
-XX:OnOutOfMemoryError | 当发生OOM内存溢出时,执行一个脚本-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p%p表示线程的id pid |
-XX:MaxTenuringThreshold=7 | 表示如果在幸存区移动多少次没有被垃圾回收,进入老年代 |
10.1、新生区
新生区主要用来存放新生的对象。一般占据堆空间的1/3。在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行MinorGC,进行垃圾回收。新生代又细分为三个区:Eden区、SurvivorFrom、ServivorTo区,三个区的默认比例为:8:1:1。
-
Eden区:Java新创建的对象绝大部分会分配在Eden区(如果对象太大,则直接分配到老年代)。当Eden区内存不够的时候,就会触发MinorGC(新生代采用的是复制算法),对新生代进行一次垃圾回收。
-
SurvivorFrom区和To区:在GC开始的时候,对象只会存在于Eden区和名为From的Survivor区,To区是空的,一次MinorGC过后,Eden区和SurvivorFrom区存活的对象会移动到SurvivorTo区中,然后会清空Eden区和SurvivorFrom区,并对存活的对象的年龄+1,如果对象的年龄达到15,则直接分配到老年代。MinorGC完成后,SurvivorFrom区和SurvivorTo区的功能进行互换。下一次MinorGC时,会把SurvivorTo区和Eden区存活的对象放入SurvivorFrom区中,并计算对象存活的年龄。
10.2、老年区
老年区主要存放应用中生命周期长的内存对象。老年区比较稳定,不会频繁的进行MajorGC。而在MaiorGC之前才会先进行一次MinorGc,使得新生的对象进入老年代而导致空间不够才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次MajorGC进行垃圾回收腾出空间。
在老年区中,MajorGC采用了标记—清除算法:首先扫描一次所有老年代里的对象,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长。因为要扫描再回收。MajorGC会产生内存碎片,当老年代也没有内存分配给新来的对象的时候,就会抛出OOM(Out of Memory)异常。
10.3、永久区(方法区)
永久区在逻辑上是和堆分开的,但物理上永久代是属于堆的。永久代指的是永久保存区域。主要存放Class和Meta(元数据)的信息。Class在被加载的时候被放入永久区域,它和存放的实例的区域不同。
注意:堆=新生代+老年代,不包括永久代(方法区)。
10.4、JDK7和JDK8的区别
在jdk1.7中开始了为对方法区的移除做准备,jdkk1.7中将常量池移到堆中。
在jdk1.8中彻底移除了方法区,增加元空间MetaSpace(其作用与方法区相似)
在Java8中,永久区已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久区类似,都是对JVM中规范中方法的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
采用元空间而不用永久区的原因:
-
为了解决永久代的OOM问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出。
-
类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
-
永久代会为GC带来不必要的复杂度,并且回收效率偏低。
11、GC垃圾回收器
11.1、三种GC方式
11.1.1、Minor GC
清理新生区。
11.1.2、Major GC
清理老年区。
11.1.3、Full GC
清理整个堆内存,包括新生区和老年区。
11.2、GC算法
11.2.1、判断对象是否存活的算法
11.2.1.1、引用计数法
在一个对象被引用时+1,被去除引用时-1,当计数器值为零说明对象没地方被使用则视为垃圾。Java语言判断对象是否存活不是用的这样的方法,因为有个致命的问题就是:当两个对象相互引用的时候,而这两个对象在其它任何地方都没有被引用时,这两个对象就不会被垃圾回收掉。
11.2.1.2、根搜索算法
实有点像一种树的数据结构,通过一系列名为“GC Roots”的对象(这类对象在下面会给出)作为起始点(根),从这些起点向下搜索,搜索的路径称为引用链,在这条链中能搜索到的对象就代表是存活不会被回收的,当某个对象与这条链中不相连的时候就代表该对象需要被回收了,可以看下面的图,白色区块的几个对象是将要被回收掉的。
能作为“GC Roots”的对象包括以下几种:
-
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
-
方法区中的类静态属性引用的对象。
-
方法区中的常量引用的对象。
-
本地方法栈中JNI(即一般说的Native方法)的引用的对象。
11.2.2、垃圾回收算法
11.2.2.1、标记清除算法(适用老年代)
它将垃圾回收分为两个阶段:标记阶段和清除阶段。
标记可存活对象,清除没有被标记的对象。
11.2.2.2、复制算法(适合年轻代)
11.2.2.2、复制算法(适合年轻代)
将内存分为两部分,每次只使用其中一部分。在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。(From区与To区)
11.2.2.3、标记整理算法(适合老年代)
对比于标记清除算法,在清除阶段前,它会将所有的存活对象移动到内存的另一端进行一个整理。之后清理存活对象端边界之外的空间。
本文来自博客园,作者:是老胡啊,转载请注明原文链接:https://www.cnblogs.com/solar-9527/p/15906434.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?