发布于2021-06-08 12:55 阅读(1129) 评论(0) 点赞(5) 收藏(4)
个人博客欢迎访问
总结不易,如果对你有帮助,请点赞关注支持一下
微信搜索程序dunk,关注公众号,获取以下整张完整图和思维导图
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来
JVM:Java Virtual Machine,也就是Java虚拟机,所谓虚拟机是指:通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统,JVM是通过软件来模拟Java字节码的指令集,是Java程序的运行环境
JVM包含两个子系统和两个组件,两个子系统为Class Loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)
JNI(Java Native Interface):Java本地开发接口
JNI是一个协议,这个协议用来沟通Java代码和外部的本地代码(C/C++),外部的代码也可以调用Java代码
在本地方法栈中,登记了native方法,在最终执行的时候加载本地方法库中的方法通过JNI调用
栈由一系列帧(栈帧(Frame))组成,因此Java栈也叫做帧栈,是线程私有的
栈帧用来保存一个方法的局部变量、操作数栈(Java没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回等
每一次方法调用创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容毁掉
栈的优点: 存取速度比堆块,仅次于寄存器
栈的缺点:存在栈中的数据大小、生存区是在编译器决定的,缺乏灵活性
静态变量、常量、类信息(构造方法、接口定义)、运行时常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区的发展
JDK8以前HotSpot设计团队选择吧收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去了专门为方法区编写内存管理代码的工作。
JDK6,常量池、静态变变量在方法区中
JDK7后,放弃弃了永久代的概念,逐步采用本地内存(Native Memory)来实现方法区的计划,把原本放在永久代的字符串常量池、静态变量等移除
到了JDK8后从,彻底废弃了永久代,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)代替,把JDK7中永久代剩余的内容(主要是类型信息)全部移到元空间
首先通过编译器把Java代码转化为字节码文件,类加载器(ClassLoader)再把字节码文件加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是JVM的一套指令规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言额本地库接口(Runtime data area)来实现整个程序的功能
类的加载指的是将.class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
Header | 解释 |
---|---|
使用new关键字 | 调用了构造函数 |
使用Class的newInstance方法 | 调用了构造函数 |
使用Constructor类的newInstance方法 | 调用了构造函数 |
使用clone方法 | 没有调用构造函数 |
使用反序列化 | 没有调用构造函数 |
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象,reference类型只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问的方式由虚拟机的实现而定
除了程序计数器以外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能
Java堆用于储存对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么随着对象的增加,总容量处理最大堆的容量限制后就会产生溢出异常。
解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具,对Dump出来的堆转储快照进行分析,首先要分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
内存泄漏是指被是使用的对象或者变量一直被占据在内存中,理论上来说,Java是有GC垃圾回收机制的,也就是说,不在被使用的对象,会被GC自动回收掉,自动从内存中清除
Java仍然存在内存泄漏的情况,Java导致内存泄漏的原因很明确:长生命周期的对象持有短生命周期对象的引用就可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景
由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss
参数来设定,存在两种异常
String的intern()
方法是一个本地方法,作用是如果字符串常量池已经包含一个等于此String对象的字符串,则返回池中这个字符串的String对象的引用,否则将次String对象包含的字符串添加到常量池并返回此String对象的引用
在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize
和 -XX:MaxPermSize
限制永久代大小,间接限制常量池。在 while 死循环中调用 intern
方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述等,只要不断在运行时生成大量类,方法区就会溢出例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出
编写的java文件都是保存着业务逻辑代码,java编译器将.java
文件编译成扩展名为.class
的文件。.class
文件保存着java转换后,虚拟机要执行的指令。当需要某个类的时候,java虚拟机会加载.class
文件,并创建对应的class对象,将class文件加载到虚拟机内存中,这个过程被称为类的加载
forName和loaderClass区别
类的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGuiXUh8-1622715816775)(C:/Users/dell/AppData/Roaming/Typora/typora-user-images/image-20210528203645415.png)]
类加载器的任务:根据类的全限定名来读取此类的二进制字节流到JVM中,然后转换成一个与目标类对象的相同的java.lang.Class 对象的实例,在java 虚拟机提供四种类加载器,启动类加载器、扩展加载器、系统加载器、用户自定义加载器
实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器
站在Java虚拟机的角度看,只存在两种不同的类加载器
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的福类加载器。不过这个类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父类加载器的代码,是Java设计者给开发者一种类加载器实现的最佳实践
如果一个类加载收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有加载器请求最终都应该传送到最顶层的启动类加载器中,只要父加载器反馈给自己无法完成这个加载请求(它的搜索范围没哟找到所需的类)时,子加载器才会尝试自己去完成加载
java.lang.ClassLoader的loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
//如果父类加载器抛出ClassNotFoundException异常
//说明父类加载器无法完成加载请求
// from the non-null parent class loader
}
if (c == null) {
//父类加载器无法加载时
// If still not found, then invoke findClass in order
//子类尝试加载当前类
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
先检查请求加载的类是否已经被加载,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父类加载器
垃圾收集(Gerbage Collection,简称GC)
垃圾收集需要完成的三件事情:
经过半个世纪的发展,今天的内存动态分配和内存回收技术已经相当成熟了,一切看起来都进入了“自动化”时代,那为什么还需要了解垃圾收集和内存分配?答案很简单:当需要排查各种内存溢出、内存泄漏的问题时,当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些‘“自动化”的技术实施必要的监控和调节
Java内存运行区域的各部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,线程灭而灭,每一个栈帧中需要分配多少内存基本上在类结构确定下来时就已知了。(即使编译器会进行一些优化)。大体上可以认为这几个区域的内存分配和回收都具备确定性,所以就不需要考虑过多的内存回收问题。
而Java堆和方法区这两个区域则有显著的不确定性,一个接口的多个实现类的内存可能会不同,一个方法所执行的不同条件分支所需要的内存可能不太一样。垃圾收集器所关注的正是这部分内存如何管理
简述Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
堆里面存放着所有的Java实例对象,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象指针还有哪些存活着,哪些已经死去了
一种判断对象是否存活的算法:在对象中添加一个引用计数器,每有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1,任何时刻引用计数器值为0的对象就是不可能被使用到的对象。引用计数器只占用了一些额外的内存空间来进行计数,但他原理简单,判定效率也很高
但是Java虚拟机没有采用这种算法来管理内存,主要原因是:这个看似简单的算法有很多例外情况需要考虑,必须要配合大量的额外处理才能保证正确的工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题
Java使用可达性分析算法来判定对象是否存活,这个算法的基本思路就是通过一系列的“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索的路径称为引用链(Reference Chain),如果这个对象到GC间没有任何引用链相连,则证明这个对象不可能再被使用
目前主流的垃圾收集器都具备了局部回收的特征,为了避免GC Roots的包含过多的对象而过度膨胀,他们在实现上做出了各种优化
既然回收方法区不是必须的,虽然效率低下,但是当内存不够使用的时候依然是会抛出OOM的,那么我们需要知道什么场景下需要去回收方法区。首先我们需要弄明白方法区会回收那些对象。在JDK1.7之前,常量池是在方法区中的,在此版本及以后则将其移到堆中。基于目前版本主要是1.8及以上,故我们以1.8为准。在此版本上,主要回收的是无用的类。如何判定一个类是无用的:
因此,在大量使用反射,动态代理,CGlib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代也就是方法区内存不会溢出。
从判定对象消亡的角度出发,垃圾收集算法可以分为
这两个分代假说共同鉴定了多款常用的垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域去存储。显而易见,如果一个区域内的大多数对象都是朝生夕灭,难以熬过垃圾收集的过程,那么将他们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;如果剩下的都是难以消亡的对象,那么把他们集中放在一块,虚拟机边可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用
在Java堆划分出不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域,因而又了回收类型分类:
也才能针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法
这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来
实现简单,不需要对象进行移动
标记-清除算法的执行的过程如下图所示
复制算法是将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,浪费较大
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”
对于老年代,回收的垃圾较少时,如果采用复制算法,则效率较低。标记整理算法的标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代如图所示:
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器
CMS 使用的是标记-清除的算法实现的,所以在 GC的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
分代回收器有两个分区:老年代和新生代,新生代默认的空间占比总空间的1/3,老年代默认占比2/3
新生代使用的是复制算法,新生代里面有三个分区:Eden、To Survivor、From Survivor,他们默认的占比是8:1:1,执行流程
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代
老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程
原则:
-XX:PretenureSizeThreshold
参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor间来回复制-XX:MaxTenuringThreshold
设置-XX:HandlePromotionFailure
参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGCJDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole 和 jvisualvm 这两款视图监控工具
-Xms1024m -Xmx1024m -XX:+PrintGCDetails:打印堆的信息
/**
* @author :zsy
* @date :Created 2021/6/1 20:47
* @description:测试
*/
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = " + max + "字节;" + (double)(max / 1024 / 1024) + "MB");
System.out.println("max = " + total + "字节;" + (double)(total / 1024 / 1024) + "MB");
}
}
输出
max = 1029177344字节;981.0MB
max = 1029177344字节;981.0MB
Heap
PSYoungGen total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3277K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 358K, capacity 388K, committed 512K, reserved 1048576K
下载Jprofile9.2版分析工具
注册码:L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
在IDEA中进行配置
编写测试
/**
* @author :zsy
* @date :Created 2021/6/3 15:12
* @description:测试Jprofile工具
*/
public class Demo01 {
int[] nums = new int[1 * 1024 * 1024];
public static void main(String[] args) {
ArrayList<Demo01> list = new ArrayList<>();
int count = 0;
try {
while(true) {
list.add(new Demo01());
count++;
}
} catch (Error error) {
error.printStackTrace();
} finally {
System.out.println(count);
}
}
}
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid15800.hprof ...
Heap dump file created [5679620 bytes in 0.030 secs]
1
根路径下查看文件,可以看到具体是什么情况导致了OutOfMemoryError异常
虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干分不同的数据区,这些区域有各自的用途、创建和销毁时间
线程私有:程序计数器、Java虚拟机栈、本地方法栈
线程共享:Java堆、方法区
程序计数器是一块较小的内存空间,可以看做当前线程执行字节码的行号指示器,是唯一没有内存溢出的区域,字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器
如果线程正在执行java方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为Undefined
Java虚拟机栈来描述Java方法的内存模型,每当有新的线程创建时就会分配一个栈空间,线程结束后栈空间被回收的,栈和线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储局部变量表、操作栈、动态链接和方法出口等信息,每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程
有两类异常:
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法
虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一
堆是虚拟机所管理的内存中最大的一块,所有线程共享,在虚拟机启动时创建。堆用来存放对象实例,Java中所有的对象实例都在堆上分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间
堆既可以被实现成固定大小,也可以是可扩展的,可通过 -Xms
和 -Xmx
设置堆的最小和最大容量,当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出 OutOfMemoryError
运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用来存放编译器生成的各种字面量与符号,这部分内容在类加载后存放到运行时常量池,一般除了保存Class文件描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池
运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern
方法
直接内内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出
JDK1.4 中新加入了 NIO 这种基于通道与缓冲区的 IO,它可以使用 Native 函数库直接分配堆外内存,通过一个堆里的 DirectByteBuffer 对象作为内存的引用进行操作,避免了在 Java 堆和 Native堆来回复制数据。
直接内存的分配不受 Java 堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置 -Xmx
等参数信息,但经常忽略直接内存,使存区域总和大于物理内存限制,导致动态扩展时出现 OOM
由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因
区别 | 堆 | 栈 |
---|---|---|
物理地址 | 物理地址分配对对象是不连续的,在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) | 数据结构中的栈,新进后出的原则,物理地址分配是连续的 |
性能 | 慢一些 | 快 |
内存分配时间 | 运行期 | 编译器 |
存放内容 | 对象的实例数据和数组,因此该区更关注的是数据的存储 | 局部变量,操作数栈,返回结果,该区更关注的是程序方法的执行 |
程序可见度 | 堆对整个应用程序都是共享、可见的 | 栈只对于线程可见,私有的 |
永久代和元空间都是方法区的一种实现
永久代更规范的名字的叫做方法区,永久代是方法区的一种实现方式,方法区是java规范中定义的,只有hotspot才有永久代。之所以叫它永久代是因为垃圾回收效果很差,大部分的数据会一直存在直到程序停止运行
永久代里面一般存储类相关信息,比如类常量、字符串常量、方法代码、类的定义数据等,如果要回收永久代的空间,需要将类卸载,而类卸载的条件非常苛刻,所以空间一般回收很难。当程序中有大量动态生成类时,这些类信息都要存储到永久代,很容易造成方法区溢出
JDK6的时候HotSpot设计团队已经开始打算放弃永久代,直到JDK8,彻底的废弃了永久代的概念
元空间替代了永久代,原来存放于永久代的类信息现在放到了元空间,我们再也不会看到java.lang.OutOfMemoryError:PermGenSpace(Permanent Generationspace)的异常了,本质上来说,元空间也是方法区的一种实现
元空间是用来存放class metadata的,class metadata用于记录一个Java 类在JVM中的信息,包括但不限于JVM class file format的运行时数据:
特点:
元空间逻辑上存在,物理上不存在
任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性
两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等
参考文档
原文链接:https://blog.csdn.net/qq_45796208/article/details/117531920
作者:小胖子爱java
链接:http://www.javaheidong.com/blog/article/219614/4d6b53696b8779ac472d/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!