运行时数据区
红色的为线程共享区域,一个虚拟机实例对应一份
蓝色的是一个线程对应一份
程序计数器(PC计数器)
实际上,JVM中的程序计数器(PC计数器)是对物理pc寄存器的一种抽象模拟,并非是广义上的物理寄存器。
程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令,它也是内存区域中唯一一个不会出现OutOfMemoryError的区域,而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
如果执行的是native方法,那这个指针就不工作了。
程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令
每个线程都有自己的程序计数器,生命周期与线程一致。
作用: PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
下面是一个java代码的字节码文件main方法的一部分:
使用PC寄存器存储字节码指令地址有什么用呢?
(为什么使用PC寄存器记录当前线程的执行地址呢?)
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
cpu在执行多线程的时候,实际上会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
并行 并发
- 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- 并行: 单位时间内,多个任务同时执行(可以与串行相理解)
虚拟机栈
栈管运行,堆管存储。则虚拟机栈负责运行代码,而虚拟机堆负责存储数据。
Java虚拟机栈( Java virtua1 Machine Stack),早期也叫Java栈。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧( Stack Frame),对应着一次次的Java方法调用。是线程私有的
-
作用:主管java程序的运行,它保存方法的局部变量(8中基本数据类型/对象的引用)、部分结果,并参与方法的调用和返回。
-
**优点:**1. 栈是一种快速有效的分配存储方式,访问速度仪次于程序计数器。
2.JVM直接对Java栈的操作只有两个:每个方法执行,伴随着进(入栈、压栈)、执行结束后的出栈工作
3.对于栈来说不存在垃圾回收问题
异常
如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报 StackOverflowError (这种错误经常出现在递归中)。
Java虚拟机也可以动态扩展,但随着扩展会不断地申请内存,当无法申请足够内存时就会报错 OutOfMemoryError(OOM)。
设置栈内存大小
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈大小直接决定了函数调用的最大可达深度。
栈的存储单元-栈帧
- 每个线程都有自己的栈,栈中的数据都是以**栈帧( Stack Frame)**格式存在。
- 在这个线程上正在执行的每个方法都各自对应一个栈帧( Stack Frame)
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。
- 即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧( Current Frame),
- 与当前栈帧相对应的方法就是当前方法( CurrentMethod),
- 定义这个方法的类就是当前类( Current Class).
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用 return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
栈帧的内部结构【ToDo】
(这里我并没有仔细看,所以只粗略了写了一小点内容)
-
局部变量表( Local Variables)
定义为数字数组
局部变量表,最基本的存储单元是Slot(变量槽)
-
操作数栈( operand stack)(或表达式)
操作数栈,在方法执行过程中,根据字节码指令,在栈中写入数据或者提取数据,即入栈、出栈
-
动态链接( Dynamic Linking)(或指向运行时常量池的方法引用)
每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( Symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用 -
方法返回地址( Return Address)(或方法正常退出或者异常退出的定义)
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
-
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
静态变量和局部变量
局部变量在使用前一定要显式赋值,否则编译不通过
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,
另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
补充:
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
java中方法重写的本质?
举例栈溢出的情况?
如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报 StackOverflowError (这种错误经常出现在递归中)。
调整栈大小,就能保证不出现溢出吗?
使用参数-Xss选项来设置线程的最大栈空间,但不一定保证不出现溢出,如果碰到递归,可能会出现溢出。
分配的栈内存越大越好吗?
并不是,栈是私有的,如果设置的每一个栈的空间太大了,就会导致最后可分配的线程数量变少,甚至出OOM
垃圾回收是否会涉及到虚拟机栈?
不会,他直接操作的就是出栈、入栈,不存在GC(垃圾回收),存在Error
方法中定义的局部变量是否线程安全?【ToDo】
视情况而定
本地方法接口的理解
本地方法(Native Method):一个 Native Method就是一个Java调用非Java代码的接口。
一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C,这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中你可以用 extern"C"告知C++编译器去调用一个C的函数。
本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。
如果JVM产品不打算支持 native方法,也可以无需实现本地方法栈。
在 Hotspot JVM中,直接将本地方法栈和虚拟机合二为一。
堆(heap)
堆是线程共享区域
核心概念
-
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
即,每个Java应用程序都使用一个独立的 JVM。
-
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。 -
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
-
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer, TLAB)
-
所有的对象实例以及数组都应当在运行时分配在堆上。( The heap is the run- time data area from
which memory for all class instances and arrays is allocated
周老师说的是:“几乎”所有的对象实例都在这里分配内存。---------从实际使用角度看的。 -
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
-
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
-
堆,是GC( Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
栈 、堆、方法区之间的关系
堆的细分内存结构
在JDK7以及之前的版本,堆通常被分为下面三个部分:
- 新生区(Yong Generation)
- 养生区(Old Generation)
- 永久区/永久代(Permanent Generation)
Java8及之后堆内存逻辑上分为三部分:
-
新生区
年轻代又可以划分为Eden空间、 Survivor0空间和 Survivor1空间(有时也叫做from区、to区)。
-
养老区
-
元空间
由于翻译不同 叫法也不尽相同,下面是常见的叫法:
新生区=新生代= 年轻代
养老区 =老年区 =老年代
永久区 =永久代
设置堆空间大小
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,
可以通过选项"-Xmx"和"Xms"来进行设置。
“-Xms"用于表示堆区的起始内存,等价于ーXX: Initialheapsize
“-Xmx"则用于表示堆区的最大内存,等价于-XX: Maxheaps1ze
年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代和老年代
其中年轻代又可以划分为Eden空间、 Survivor0空间和 Survivor1空间(有时也叫做from区、to区)。
几乎所有的java对象都是在Eden区被new出来的。
绝大部分的java对象的销毁都在新生代进行
调参与占比
新生代:老年代=1:2
新生代:Eden:s0:s1=8:1:1
对象分配的一般过程
1.new的对象先放伊甸园区。此区有大小限制。
【注意】大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收( Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
【注意】只有伊甸园区的空间满的时候才会触发垃圾回收( Minor GC又叫YGC【Young GC】),幸存区满了并不会触发
3.然后将伊甸园中的剩余对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6,啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数:-XX: Maxtenuringthreshold=进行设置
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
对象分配的特殊情况
配合P73来理解
Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 Hotspot JVM的实现,它里面的GC按照回收区域又分为两大种类型:
一种是部分收集(Partial GC)
一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个java堆的垃圾收集。其中又分为:
- 新生代收集( Minor GC/ Young GC): 只是新生代的垃圾收集
- 老年代收集( Major GC/Old GC): 只是老年代的垃圾收集。
目前,只有 CMS GC会有单独收集老年代的行为。
注意,很多时候 Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。 - 混合收集( Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
- 整堆收集(Full GC): 收集整个java堆和方法区的垃圾收集。
年轻代GC( Minor GC)触发机制
- 当年轻代空间不足时,就会触发 Minor GC,这里的年轻代满指的是Eden代满, Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存)
- 因为java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
- Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
老年代GC(Major GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,我们说“ Major GC”或“Full GC”发生了
- 出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行 Major GC的策略选择过程)。也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发 Major Gc
- Major GC的速度一般会比 Minor GC慢10倍以上,STW的时间更长。
- 如果 Major GC后,内存还不足,就报OOM了
Full GC触发机制
触发Full GC执行的情况有如下五种:
(1)调用 System. gc()时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过 Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、 survivor spacee( From Space)区向 survivor space1(To Space)区复制时,对象大小大于 To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
一般来说 出现OOM,则极大可能进行了Full GC
为什么要把Java堆分代?不分代就不能正常工作了吗?
-
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
- 新生代:有Eden、两块大小相同的Survivor(又称作 s0/s1或from/to )构成,to总为空。
- 老年代:存放新生代中经历多次GC依旧存活的对象
-
其实不分代完全可以,分代的唯一理由就是优化GC性能。
- 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
- 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
针对不同年龄段的对象分配原则
-
优先分配到Eden
-
大对象直接分配到老年代:
尽量避免程序中出现过多的大对象 -
长期存活的对象分配到老年代
-
动态对象年龄判断:
如果Survivor区中相同年龄的所有对象大小的总和 大于 Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。【比如】:Survivor区中有6个相同年龄的对象,都是10岁,而且这6个对象占用的空间大于 Survivor空间的一半,则其他大于10岁的对象可以直接进入老年代。
-
空间分配担保:
-XX:HandlePromotionFailure ,也就是经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。
TLAB【ToDo】
TLAB(Thread Local Allocation Buffer)中文意思是线程本地分配缓冲区
堆空间都是共享的么?
不一定,因为还有TLAB这个概念,在堆中划分出一块区域,为每个线程所独占
为什么要有TLAB?
- TLAB:Thread Local Allocation Buffer,也就是为每个线程单独分配了一个缓冲区
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
小结
-
年轻代是对象的诞生、成长和消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
-
老年代放置长生命周期的对象。通常都是从Survivor区域筛选拷贝过来的java对象。
普通的对象会被分配在TLAB上,当对象较大的,JVM会试图直接分配在Eden其他位置上
当对象太大,无法在新生代找到足够长的连续空闲空间,JVM就会把对象直接分配到老年代。
-
当GC只发生在年轻代,回收年轻代对象的行为称为Minor GC。
当GC发生在老年代,则被成为Major GC或者Full GC。
一般来说,Minor GC的发生频率比Major GC要高,即年轻代中垃圾回收频率大大高于年轻代。
方法区
栈 堆 方法区的交互关系
方法区 是用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,编译后代码···等
类加载器将 .class 文件搬过来就是先丢到这一块上
堆 主要放了一些存储的数据,比如对象实例,数组···等,
栈 这是我们的代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。
上面的图中,new PerSon()相当于创建了一个对象实例,则放在堆中
Person存放了这个类的信息,存放在方法区中
person是存放在Java栈的局部变量表中
方法区的基本理解
《java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpot JVM而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是独立于java堆的内存空间。
- 方法区和堆一样,是各个线程共享的内存区域
- 方法区在jvm启动的时候被创建,而且他的实际物理内存空间中和java堆区一样都可以是不连续的
- 方法区的大小跟堆空间大小都可以选择固定大小或者可拓展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,悉尼及同样会抛出内存溢出错误,
- 关闭JVM就会释放这个区域的内存
方法区的演进
在 JDK7 及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代
本质上,方法区和永久代并不等价
当年使用永久代,不是好的idea,因为实在JVM的内存中运行,导致Java程序更容易OOM
而jdk1.8及以后,元空间不在虚拟机设置的内存中,而是使用本地内存
方法区的内部结构
方法区主要存放的信息如下:
《深入理解java虚拟机》中对方法区存储的内容描述如下:
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译之后的代码缓存等。
类型信息:
包含了下面几个信息:
- 这个类的完整有效名称(全名=包名+类名)
- 这个类型直接父类的完整有效名(对于interface或者Object,都没有父类)
- 这个类型的修饰符(public,abstract,final)
- 这个类型直接接口的一个有序列表
域信息(成员变量):
- JVM必须在方法区中保存类型的所有域相关的信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public、static、final、volatile等)
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上一部分
- 类变量被所有的实例共享,即使没有类实例时也可以访问
【注意】被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了
方法信息:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符( public, private, protected, static,final,synchronized, native, abstract的一个子集)
- 方法的字节码( bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表( abstract和 native方法除外)
常量池
在字节码文件内部,包含了常量池。
当通过类的加载器加载运行之后,就叫做运行时常量池。
- 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外
- 还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
为什么需要常量池?
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,所以我们将所需用到的结构信息记录在常量池中,并通过引用的方式,来加载、调用所需的结构。
常量池里有什么?
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类
名、方法名、参数类型、字面量等类型。
方法区的演进
JDK版本 | 演变细节 |
---|---|
JDK1.6及以前 | 有永久代,静态变量存储在永久代上 |
JDK1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量从永久代中移除,保存在堆中 |
JDK1.8 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。 |
- JDK1.6
- JDK1.7
- JDK1.8
为什么永久代要被元空间替代?
JRockit是和HotSpot融合后的结果,因为JRockit没有永久代,所以他们不需要配置永久代
随着Java8的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
- 为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的oom。比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
“Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space”
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
- 对永久代进行调优是很困难的。
- 主要是为了降低Full GC
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型
String Table为什么要调整?
JDK7中将StringTable放在堆空间中。
因为永久代的回收效率很低,在Full GC时才会被执行永久代(方法区)的垃圾回收,而Full GC是老年代空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。
放到堆里,能及时回收内存。
静态变量存放在哪里?
静态引用对应的对象实体始终都存在堆空间。
方法区的垃圾回收
实际上java虚拟机规范中并没有规定方法区一定要垃圾回收
方法区的垃圾收集主要回收两个部分:常量池中废弃的常量和不再使用的类型
常量池之中主要存放的两大类常量:字面量和符号引用。
字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.C1ass对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。I Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
- 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
小结
大厂面试题
百度 三面:说一下JVM内存模型吧,有哪些区?分别干什么的?
蚂蚁金服: Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么? 一面:JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区? 二面:Eden和survior的比例分配
小米: jvm内存分区,为什么要有新生代和老年代
字节跳动: 二面:Java的内存分区 二面:讲讲vm运行时数据库区 什么时候对象会进入老年代?
京东: JVM的内存结构,Eden和Survivor比例。 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和survivor。
天猫: 一面:Jvm内存模型以及分区,需要详细到每个区放什么。 一面:JVM的内存模型,Java8做了什么改
拼多多: JVM内存分哪几个区,每个区的作用是什么?
美团: java内存分配 jvm的永久代中会发生垃圾回收吗? 一面:jvm内存分区,为什么要有新生代和老年代?