https://blog.csdn.net/wei_lei/article/details/70738379
https://blog.csdn.net/bingduanlbd/article/details/8363734

java虚拟机内存区域的划分以及作用详解

下面我根据《深入理解Java虚拟机》(周志明著),对上面的博客作一下补充和总结。

内存区域的划分

Java虚拟机管理的内存也叫运行时数据区域,Java虚拟机规范一共将内存划分为5个块,虚拟机栈、本地方法栈、程序计数器、Java堆、方法区。其中,虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程而生,随线程而灭;而Java堆、方法区则是线程共享的,随着虚拟机进程的启动而存在。

各个内存区域的作用

上面博客整理得很详细,我就不多说了。

1)程序计数器

线程执行相关

2)栈内存

分为本地方法栈与虚拟机栈
本地方法栈:存放native方法相关的信息
虚拟机栈:存放java方法相关的信息,每个方法(不包括native方法)执行的时候都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。在函数中定义的一些基本类型的
```变量和对象```的引用都是在函数的栈内存中分配。当在一段代码库中定义一个变量是,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java就会自动释放为该变量分配的内存空间。(所以局部变量占点内存没有关系,方法执行完它就行释放。)

3)堆内存

堆内存用于存放所有由new创建的对象(包括该对象其中的所有的非静态成员变量)和数组。
#####4)方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件。.class)等数据

对象

对象的创建

Java语法new就是创建一个对象,虚拟机是如何处理这个指令的:

1)类加载

首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用的代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

2)分配内存

内存分配方式

如何为新生的对象分配内存?有2种方式:
- 指针碰撞
这种方式有一个大前提:假设Java堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放在另一边,中间放着一个指令作为分界点的指示器,分配内存就是就是将指针向空闲内存移动与对象大小相等的距离。
- 空闲列表
维护一个列表记录哪些内存可用与空闲。

而Java堆是否规整,是由Java虚拟机所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的并发问题

Java虚拟机中创建对象是非常频繁的行为,即使是使用指针碰撞方式来分配内存,在并发情况也并不是线程安全的。如给对象A分配内存,指针还没有来得及修改,对象B又使用了原来的指针来分配内存的情况。解决方案有2种:
1)对内存分配的动作进行同步处理
CAS配上失败重试
关于什么是CAS:https://blog.csdn.net/tanga842428/article/details/52742698
2)TLAB(本地线程分配缓冲)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
所以以前编程就听见有人说创建线程十分消耗内存,这里算是明白了,这也是为什么有线程池来复用线程的原因。

3)对象初始化

Java对象的字段都有默认值,就是Java虚拟机在这里为对象初始化的。

4)对象设置

对象头:类的元数据信息、对象的哈希码、对象的GC分代年龄

对象的内存布局

在Hotspot虚拟机中,对象在内存中存储的布局分为3块区域:对象头、实例数据、对齐填充。
- 对象头
对象头包括2部分信息,第一部分用于存储对象自身的运行时数据(Mark Word);第二部分是类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例,但不是所有的虚拟机都有类型指针。
- 实例数据
实例数据是对象真正存储的有效信息,也是代码中定义的各种类型的的字段内容。
- 对齐填充
保证整个对象的大小是8的整数倍,假如实例数据没有对齐,对齐填充部分会自动补齐。

对象的访问定位

取决于虚拟机的实现,主流的访问方式有句柄和直接指针两种。
- 句柄
Java堆中划分一部分内存来作为句柄池,reference会存储对象的句柄地址,而句柄包含对象实例和类型数据各自的地址信息。
image.png
使用句柄的好处就是对象被移动,只要改变句柄指向对象实例的指针,reference不需要作改变。
- 直接指针
reference直接指存储对象的地址
image.png
使用直接指针的好处就是访问速度快,节省一次指针定位的时间开销。因为对象的访问在Java中十分频繁,Sun Hotspot虚拟机就是采用的这种方式访问对象。

异常

分析Java虚拟机的异常,是为了让我们在遇到这些异常时,能清晰地定位到可能产生这些异常的代码。

OutOfMemoryError异常

java虚拟机各个内存部分,除了程序计算器之外,都会抛出OutOfMemoryError异常。

一、Java堆溢出

不断的创建对象,并且保证对象被引用(不被垃圾回收器回收),当对象数量达到最大堆的容量限制就会抛出内存溢出异常。在Java堆中,这个异常其实包涵两个方面的层义:内存泄漏(Memory Leak)和内存溢出(Memory Overflow)。
- 内存泄漏(Memory Leak)
对象被引用,垃圾回收器无法回收,导致内存溢出的原因之一。

堆内存中的
```长生命周期对象持有短生命周期对象的强/软引用```,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。
- 内存溢出(Memory Overflow)
Java堆再也无法为新创建的对象分配内存了,是结果。

制造异常

public class HeapOOM {
    public static class OOMObject{

    }
    public static void main(String[] args){
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true){
            list.add(new OOMObject());
        }
    }
}

在Android端运行HeapOOM.main(null),等待一段时间抛出异常:

java.lang.OutOfMemoryError: Failed to allocate a 68706640 byte allocation with 16777216 free bytes and 36MB until OOM

这个异常是由Android虚拟机抛出的,所以和作者讲的Java虚拟机输出日志有所不同。之所以直接运行main方法,是我没有找到限制堆容量大小的方法,等了很久也没有异常产生。

二、栈溢出

当线程请求的栈深度大于虚拟机所允许的最大深度时,就会抛出StackOverflow异常。
当虚拟机扩展栈时,无法申请到足够的内存,就会抛出OutofMemoryError异常。

制造StackOverflow异常

写一个死循环

public class StackOverflow {
    public static void main(){
        Log.e("CZ", "main: test");
        main();
    }
}

在Android端运行StackOverflow.main(),等待一段时间抛出异常:

java.lang.StackOverflowError: stack size 8MB

制造OutofMemoryError异常

在Java栈中,一般很难出现OutofMemoryError异常,尝试用下面代码:

public class ThreadTest {
    private static  void dontStop(){
        while (true){

        }
    }

    public static void stackLeakByThread(){
        while (true){
            Thread thread = new Thread(){
                @Override
                public void run() {
                    Log.e("cz", "run: ");
                    dontStop();
                }
            };
            thread.start();
        }
    }
}

但是上面的代码,最终会导致adb offline,手机卡死。由于Java线程会和操作系统的内核线程产生关联,导致系统卡死,应用也不会崩溃。
https://blog.csdn.net/qq_27035123/article/details/77651534

三、方法区溢出

制造OutofMemoryError异常

String的intern()方法:判断运行时常量池是否存在这个常量,不存在则添加。利用这个方法可以制造出异常

public class RuntimeConstantPoolOOM {
    public static void main(){
        List<String> list = new ArrayList<String>();
        int i = 0;
        String pre = "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
                "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
                "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
                "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
                "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss";

        while (true){
            Log.e("CZ", "main: test");
            list.add(String.valueOf(pre + (i++)).intern());
        }
    }
}

这个等到出现异常会需要很久的过程,抛出异常如下:

java.lang.OutOfMemoryError: Failed to allocate a 1576 byte allocation with 4291168 free bytes and 4MB until OOM; failed due to fragmentation (required continguous free 131072 bytes for a new buffer where largest contiguous free 40960 bytes)

另外对String的intern()这个方法再作一下补充,JDK1.6和JDK1.7的底层实现不一样。

制造OutofMemoryError异常2

通过一些动态代理的框架,循环的加载增强的类,也能在方法区产生异常。

垃圾收集器

GC(Garbage Collection)的概念并不是Java独有,很多年前人们就在思考GC要做的3件事情:
1)对象已死吗?
2)什么时候回收?
3)何时回收?

对象已死吗

  • 引用计数法
    优点:实现简单,效率高。
    缺点:无法解决对象循环引用的问题。
  • 可达性分析算法
    GCRoots对象到对象是否可达

引用

《深入理解Java虚拟机》(周志明著)书中第65页对这个部分做了很详细的说明,Java引用在JDK1.2分为强、软、弱、虚4种引用,强度依次减少。

https://zhuanlan.zhihu.com/p/138021686

  • 强引用
    直接用Object obj = new Object()这样形式new出来的对象,如果引用还在,垃圾回收器永远不会回收。
  • 弱引用
    使用SoftReference创建,在系统发生内存溢出之前会回收软引用关联的对象,如果回收之后还没有足够的内存才会抛出异常。
  • 软引用
    使用WeakReference创建,当垃圾回收器工作时,无论内存是否紧张,都会回收软引用关联的对象。
  • 虚引用
    使用PhantomReference创建,不会对对象的生存时间造成影响,也无法通过虚引用获取对象实例,虚引用只起到对象被垃圾回收器回收时收到一个系统通知的作用而已。ReferenceQueue引用队列作用在于跟踪垃圾回收过程。当垃圾回收器回收对象时,如果发现它还有虚引用,就会在回收后销毁这个对象,并且将虚引用指向的对象加入到引用队列。只能通过虚引用是否被加入到ReferenceQueue来判断虚引用是否为GC回收,这也是判断对象是否为回收的唯一途径。

关于软引用与虚引用我要强调的一个重点:

软引用与虚引用在相关情况下能被回收的前提是,它们所引用的对象,没有在其它的地方被强引用(不同的作用域的强引用,包括方法内、成员、静态),如果在其他的地方被强引用,它们所引用的对象是不会被回收的!!!

4种引用的应用场景:
https://blog.csdn.net/qq_40434646/article/details/92569183
https://www.jianshu.com/p/825cca41d962

finalize方法

当对象不可达时,不代表它就一定会死,看书中第66页对这个方法的说明。
https://www.jianshu.com/p/0618241f9f44

方法区(永久代)回收

主要回收废弃常量和无用的类

垃圾收集算法

  • 标记-清除算法
    效率不高,标记、清除的过程的效率不高。
    空间问题,产生大量的不连续的内存碎片。
  • 复制算法
    优点:解决了内存碎片问题
    缺点:内存只能使用一半,实际虚拟机并不是1:1划分。
    新生代分配担保机制,老年代。
  • 标记、整理算法
    在标记-清除算法基础上,最后不直接清除回收对象,而是先整理移动存活对象到内存一端,再清除另一端,这样就没有内存碎片了。
  • 分代收集算法
    将Java堆分成新生代和老年代,新生代采用复制算法,老年代采用标记-清除算法或者标记、整理算法。

Hopspot虚拟机算法实现

GC卡顿

为了保证可达性分析的准确性,虚拟机会停顿所有的Java线程。

概念:OopMap、安全点、安全区域

减少GC开销的措施

  • (1)不要显式调用System.gc()

  此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

  • (2)尽量减少临时对象的使用

  临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

  • (3)对象不用时最好显式置为Null

  一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

  • (4)尽量使用StringBuffer,而不用String来累加字符串

  由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

  • (5)能用基本类型如Int,Long,就不用Integer,Long对象

  基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

  • (6)尽量少用静态对象变量

  静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

  • (7)分散对象创建或删除的时间

  集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

垃圾收集器

内存回收的具体实现者。

分类: Java

0 条评论

发表回复

您的电子邮箱地址不会被公开。