Java与C++之间有一堵由内存动态分配(加载)和垃圾收集技术所围成的高墙,墙外面的1人想进去,墙内边的人想出去。
方法区的定义:方法区是Jvm规范中定义的规范,主要用于存储已被虚拟机加载的【Class】类型信息、常量、静态变量、即时编译器编译后的代码缓存等。其大小可通过参数调节。
方法区的垃圾回收:方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
1)这个类型的完整有效名称(全名=包名.类名)。
2)这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)。
3)这个类型的修饰符(public,abstract,final的某个子集)。
4)这个类型直接接口的一个有序列表。
域信息:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序:
1)域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)。
方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
1)方法名称
2)方法的返回类型(或void)
3)方法参数的数量和类型(按顺序)
4)方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
5)方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
6)异常表(abstract和native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
静态类变量:静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
1)静态类变量被类的所有实例共享,即使没有类实例时,你也可以访问它。
方法区的实现:JDK1.7及以前:Hotspot虚拟机对方法区的实现是永久代,方法区和永久代的关系就类比如Java中的接口和实现类。永久代就是HotSpot虚拟机对Jvm虚拟机规范中方法区的一种实现方式。此时的方法区是堆的逻辑组成部分。《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java堆的内存空间。
JDK1.7方法区(永久代)的特点:
1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
2)方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
3)方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace。
5)关闭JVM就会释放这个区域的内存。
方法区的实现:JDK1.8:Hotspot虚拟机对方法区的实现是元空间,以前的方法区实现永久代是在Jvm分配的内存中,且设置好的固定大小,所以溢出的可能性比较大。JDK1.8方法区的实现是元空间,不在虚拟机内存中,而是使用本地内存。降低了内存溢出。-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
永久代和元空间最大的区别:永久代是在Jvm分配的内存中,元空间不在Jvm分配的内存中,而是使用的本地内存。方法区的大小不必设置,JVM可以根据应用的需要动态调整。
JDK1.8方法区(元空间)的特点:
1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
2)方法区的大小可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定,默认是本地内存,JVM可以根据引用动态调整。
3)如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
4)方法区不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。
5)关闭JVM就会释放这个区域的内存。
-XX:MetaspaceSize
class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,
同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,
那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize
可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。
1、java.lang.StackOverflowError:栈内存异常:栈内存具有一定的深度,栈内存的大小也可以指定。
无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况
方法内声明了海量的局部变量,方法中的变量存储在栈帧中的局部变量表中。
执行了大量方法,导致线程栈空间耗尽
2、java.lang.OutOfMemoryError: Java heap space:堆内存:存储对象实例
请求创建一个超大对象,通常是一个大数组,堆内存大小是有限制的。
内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收(程序结束Connection类的连接没有关闭,无法被GC)
超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
过度使用终结器(Finalizer),该对象没有立即被 GC。
3、java.lang.OutOfMemoryError:GC overhead limit exceeded :重复GC无效上线
- Jvm默认配置中如果进程花费98%以上的时间执行GC,但是回收的内存不到%2,且该动作连续重复5次,就会抛出重复GC上线(俗称垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。假如不抛出
GC overhead limit exceeded
错误,那GC清理的内存很少,很快就会被再次填满,迫使 GC 再次执行,这样恶性循环,CPU 使用率 100%,而 GC 没什么效果。4、java.lang.OutOfMemoryError:Direct buffer memory :直接内存OOM
- 我们使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。
- Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
- 如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。Direct Memory
5、java.lang.OutOfMemoryError:unable to create new native thread 线程数上线
- 每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 Thread线程时,如果没有足够的资源分配就会报此类错误。
6、java.lang.OutOfMemoryError:MetaSpace 元空间OOM,实质是本地内存溢出Direct Memory
- JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。
- Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常
诊断工具:要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析.
区分问题:是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow);
内存泄漏问题:就是有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题。如果是内存泄漏,通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
内存溢出问题:如果内存溢出问题,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
11.常量池:
Java中的常量池分为三种类型:https://blog.csdn.net/weixin_42030357/article/details/104015150 https://blog.csdn.net/luanlouis/article/details/39960815
1)类文件中常量池(The Constant Pool)也叫 Class 常量池,也叫常量池。 编译时存在于Class文件中
2)运行时常量池(The Run-Time Constant Pool) 运行时
3)String常量池 运行时
类文件常量池:也叫 Class常量池(常量池==Class常量池) 存在于Class文件中 javap -v xxx.class
1)常量池的结构:
魔数:一串固定的值,用来进行Class文件的身份认证。不使用扩展名称是更安全。
版本号:是Class文件的版本号,用来标识JDK的版本。
常量池:简单理解就是Class文件的资源库。
访问标识:是否是Public、是否是Final、是否是接口、枚举、注解、等等。
......还有更多
2)一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。 常量池包含:数量值、字符串值、类引用、字段引用、方法引用。
3)为什么需要常量池:
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会加载到运行时常量池。
public class SimpleClass {
public void sayHello() {
System.out.println("hello");
}
}
//虽然上述代码只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。
private String name;
public static void main(String[] args) {
Object o = new Object();
}
public void setName(String name) {
this.name = name;
}
//将会被翻译成如下字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."":()V
7: astore_1
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 o Ljava/lang/Object;
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #3 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
line 12: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/aop/demo/annotation/JClassDemo;
0 6 1 name Ljava/lang/String;
4)总结:常量池是.java文件被编译后产生于.class文件中。可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。当Class文件被Jvm虚拟机加载进内存后,Class类信息存储于元空间,常量池也存储在元空间,每个对象拥有一份。常量池是当Class文件被Java虚拟机加载进来后存放各种字面量 (Literal)和符号引用。
运行时常量池:存在于内存的元空间中
1)所处区域:内存的元空间
2)诞生时间:JVM运行时
3)内容概要:常量池(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区运行时常量池中。
4)JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
5)运行时常量池是当Class文件被加载到内存后,Java虚拟机会将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
字符串常量池:存在与堆内存中 https://www.cnblogs.com/liangyueyuan/p/9796992.html
1)所处区域:堆内存
2)诞生时间:JVM运行时
3)JDK1.7将字符串常量从永久代移动到了堆内存中是因为:。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
4)JDK1.7分别将字符串常量池和静态成员变量迁移到了堆内存中。
字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
常量赋值
代码:String s1 = "abc";在字符串常量池中创建,全局唯一共享。
代码:String s1 = new String("abc");
首先先来考虑一下这一句的执行过程,这句一共生成了两个对象,分别是"abc" 和 new String("abc"),在new String("abc")之前字符串常量池中就已经驻留了"abc"。如果字符串常量池中没有"abc",那么就在常量池创建,如果已经存在就不需要重复创建。然后在堆内存中创建String("abc")对象,这个对象指向常量池的所对应的字符串。返回堆内存中的地址引用。驻留的字符串是放在全局共享的字符串常量池中的。
代码:String s1 = new String("a") + new String("b");
这句话一共生成了5个对象,首先会先在池子生成“a”和“b”,然后会在堆里生成对象new String("a") 和 new String("b),这两个对象分别指向常量池的所对应的字符串,接着由于“+”的作用下,创建了新的对象,这个对象通过利用之前的对象所指向的字符进行拼接生成“ab”,即这波操作是在堆里实现的,不会将生成的"ab"放在常量池里,此时之前的两个对象已经没有作用了,需要等待垃圾回收。
总结:
不管是new String("XXX")和直接常量赋值, 都会在字符串常量池创建.只是new String("XXX")方式会在堆中创建一个中转站去指向常量池的对象, 而常量赋值的变量直接赋值给变量
当使用了变量字符串的拼接(+, sb.append)都只会在堆区创建该字符串对象, 并不会在常量池创建新生成的字符串。
String intern() 方法:当调用intern方法时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。
静态引用对应的对象实体始终都存在堆空间。JDK1.7之后1.8从方法区迁移到了堆内存中。
声明:本文是本人学习,阅读深入理解JVM第三版,和查看部分优秀博主文章总结。https://www.jianshu.com/p/87354570f362 https://blog.csdn.net/luanlouis/article/details/39960815