jvm运行时的数据区分析

程序计数器

  1. 生存周期:每个线程独有,相互间不影响,生命周期与所属线程相同
  2. 作用:JVM字节码解释器通过改变这个计数器的值来选取线程的下一条执行指令
  3. 存储内容:在任意时刻一个线程只会执行一个方法(当前方法),这是因为 JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,如果当前方法是Java方法,那PC计数器就保存JVM正在执行的字节码指令的地址;如果是native方法,那PC计数器的值是undefined(原因是native方法是java通过jni调用本地方法来实现,非java字节码实现,所以无法统计);
  4. 内存分配特点:占用较小的内存空间;
  5. 异常情况: 唯一一个JVM规范中没有规定会抛出OutOfMemoryError情况的区域;

java虚拟机栈

  1. 生存周期:每个线程独有,生命周期与所属线程相同;
  2. 作用:描述的是Java方法执行的内存模型,在方法调用和返回中也扮演了很重要的角色;
  3. 存储内容:方法的栈帧
    1. 每个方法执行时都会创建一个栈帧,对应着每次方法调用时所占用的内存,随着方法调用而创建(入栈),随着方法结束而销毁(出栈);
    2. 栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;
      1. 局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。它的大小在编译期就可以知道,所以在JVM栈中分配给该方法的局部变量空间是确定的,运行中不改变;它的最大容量由Class文件中该方法的Code属性的max_locals数据项确定;
      2. 操作数栈:在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容(任意类型的值),也就是入栈/出栈操作;在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果;长度由Class文件中该方法的Code属性的max_stacks数据项确定;
      3. 动态连接: 每一个栈帧内部都包含一个指向运行时常量池的引用,来支持当前方法的执行过程中实现动态链接;在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量通过符号引用来表示的;动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用(除了在类加载阶段解析的一部分符号)
      4. 方法出口:无论方法正常退出还是异常退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态
    3. 内存分配特点: 因为除了栈帧的出栈和入栈之外,JVM栈从来不被直接操作,所以栈帧可以在堆中分配; JVM规范允许JVM栈被实现成固定大小的或者是根据计算动态扩展和收缩的:
    4. 异常情况StackOverflowError:线程请求分配的栈深度超过JVM栈允许的最大深度和OutOfMemoryError
    5. 优点:访问速度快,只存在两个操作入栈和出栈,不存在垃圾回收问题,指令集小;
    6. 缺点:性能下降,实现同样的功能需要更多的指令

参数设置:

  1. HotSpot VM通过"-Xss"参数设置JVM栈内存空间大小

  2. 通过递归调用方法判断由多少个方法入栈,通过-Xss修改栈大小,观察count变化

    1. ```
       public class StackXssTest {
           static int count = 0;
           static void recursion(){
               count++;
               recursion();
           }
       
           public static void main(String[] args) {
               try {
                   recursion();
               } catch (Throwable e) {
                   System.out.println(count);//默认23252 -Xss200m 13072649
                   e.printStackTrace();
               }
           }
       }
       ```
    

本地方法栈:

  1. 与 Java虚拟机栈类似;
  2. 与Java虚拟机栈的区别:Java虚拟机栈是为JVM执行的Java方法服务;本地方法栈则为Native方法(指使用Java以外的其他语言编写的方法服务
  3. JVM规范中没有规定本地方法栈中方法使用的语言、方式和数据结构,JVM可以自由实现;
  4. HotSpot VM实现方式:HotSpot VM直接把本地方法栈和Java虚拟机栈合并为一个;

JNI实例

java代码:

public class Testjni
{
	public native void hello();
	static{
		System.setProperty("java.library.path",".");
		//需要跟生产的DLL文件名相同
		System.loadLibrary("dllhelloworld");
	}
	public static void main(String[] args){
		new Testjni().hello();
	}
}

然后通过javac编译得到字节码文件,再经过javah -jni Testjni得到头文件Testjni.h;并记住头文件中的这个名字。
在这里插入图片描述
然后再Dev-c++中创建DLL项目
在这里插入图片描述
并将Testjni.h和%JAVA_HOME%\include目录下的jni.h,%JAVA_HOME%\include\win32目录下的jni_md.h和jawt_md.h加入到DLL项目中,并修改ddlmain.c中的代码添加如下代码:

//						与前面记录的相同
JNIEXPORT void JNICALL Java_Testjni_hello(JNIEnv * env,jobject thiz){
	printf("HELLO");
	return;
}

在这里插入图片描述
修改生成的.dll文件的文件名与 System.loadLibrary(“dllhelloworld”)一致;
在这里插入图片描述
然后将Testjni.class拷贝到与dllhelloworld.dl和Testjni.h同一目录下,然后运行java Testjni得到输出结果;

堆:

  1. 生存周期:所有线程共享;生命周期与JVM相同;
  2. 作用: 为**"new"创建的实例对象提供存储空间**;
  3. 存储内容: 用于存放几乎所有对象实例;随JIT编译技术和逃逸分析技术发展,少量对象实例可能在栈上分配;
  4. 异常情况:oom

内存分配特点:

  1. 可以通过 -Xms来指定堆初始化空间大小,通过 -Xmx来知道堆内存分配池的最大空间大小
  2. 从垃圾收集器的角度来看:如果JVM采用分代收集算法,Java堆可以分为:新生代、老年代,内存比例一般为 1:2,可以通过-XX:NewRatio=n来设置默认为2,表示新生代占1老年代n,
  3. 从内存分配角度来看:为解决分配内存线程不安全问题,效率低下(同一时间可能会有很多线程申请空间分配,在这种情况下要加锁处理,如此一来就会造成分配效率下降);Java堆划分出每个线程私有的分配缓冲区,减少线程同步,通过-XX:+/-UseTLAB指定是否使用TLAB,它占用的是Eden区并且基本上大小只是Eden的1%;

新生代和老年代

  1. 几乎所有的java对象都是在新生代的Eden区被new出来的,绝大部分java对象的销毁也是在新生代中。
分代垃圾回收

请添加图片描述

  1. 新创建的对象首先分配在 eden 区;
  2. 如果eden 区内存不足会触发minor GC,eden区和from区存活的对象会被复制到to中,存活的对象年龄加一,然后下次minor GC时,会将eden区和to区的对象复制到from区,然后就一直form to from to;
    通过Java自带的jvisualvm,并下载visual GC观察Java堆空间的变化,还可以通过配置-XX:+PrintGCDetails参数观察控制台打印的信息来分析每次GC的内存大小;
public class HeapTest2 {
    public static void main(String[] args) throws IOException {
        List<HT2> list = new ArrayList();
        while (true) {
            list.add(new HT2());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class HT2 {
    private byte[] bytes = new byte[1024 * 1024];
}
  1. minor gc会引发STW(Stop the World)暂停其他线程,等垃圾回收结束后,恢复用户线程运行;
  2. 老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发Full gc(Major GC) ,如果内存还不够就报OOM。
  3. Full GC触发机制
    1. 调用System.gc();
    2. 老年代空间不足;
    3. 方法区空间不足;
    4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    5. 由Eden区,幸存者0区向幸存者1区复制时,对象大小大于1区可用内存,则把该对象转存到老年代,且老年代的可用内存大小小于该对象大小。
新生代到老年代
  1. **大对象(Eden区的剩余空间存储不下)**直接进入老年代;
  2. 长期存活的对象将进入到老年代(默认阈值为15,进行一次GC后,如果这个对象没有被垃圾回收掉,就将这个对象的年代加一)
  3. 空间分配担保,当MinorGC时,如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,已完成MinorGC;
  4. 动态对象年龄判定,如果Survivor区中相同年龄所有对象的大小总和大于Survivor区空间一半,年龄大于或者等于该年龄的对象在MinorGC时将复制到老年代;
新生代的内存设置
  1. 新生代又可以分为 Eden、From、To,内存比例为 8:1:1,可以通过 -Xmn来设置新生代的最大内存大小,还可以通过 -XX:SurvivorRatio=n调整空间比例,表示Eden区占n;
  2. 各年代内存的占用空间与可用空间的比例: 通过"-XX:MinHeapFreeRatio"和"-XX:MaxHeapFreeRatio"参数设置堆中各年代内存的占用空间与可用空间的比例保持在特定范围内
  3. -XX:MinHeapFreeRatio=40”:即一个年代(新生代或老年代)内存空余小于40%时,JVM会从未分配的堆内存中分配给该年代,以保持该年代40%的空余内存,直到分配完"-Xmx"指定的堆内存最大限制;
  4. -XX:MaxHeapFreeRatio=70”:即一个年代(新生代或老年代)内存空余大于70%时,JVM会缩减该年代内存,以保持该年代70%的空余内存,直到缩减到"-Xms"指定的堆内存最小限制;

内存逃逸

  1. 参数配置-XX:+DoEscapeAnalysis
  2. 实例,虚拟机参数配置:-XX:+PrintCommandLineFlags -XX:-DoEscapeAnalysis -XX:+PrintGCDetails观察-XX: + DoEscapeAnalysis和-XX: - DoEscapeAnalysis的运行时长变化:
public class HeapTest4 {
    public static void main(String[] args) throws IOException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费时长:" + (end - start));
        System.in.read();
    }
    private static void alloc() {
        User user = new User();//未发生逃逸
    }
    static class User {
    }
}
  1. 对代码优化
    1. 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除
    2. 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    3. 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。而经过标量替换后就不需要创建对象了,那么就不再需要分配堆内存了。
  2. 缺点:
    1. 逃逸分析并不成熟,关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现;
    2. 无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
    3. 如果经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

StringTable

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

  1. 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  2. 利用串池的机制,来避免重复创建字符串对象
  3. 字符串变量拼接的原理是StringBuilder
  4. 字符串常量拼接的原理是编译器优化
  5. 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中,如果串池中没有该字符串对象,则放入成功,如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象
public class Main {
	public static void main(String[] args) {
		// "a" "b" 被放入串池中,str 则存在于堆内存之中
		String str = new String("a") + new String("b");
		// 调用 str 的 intern 方法,这时串池中没有 "ab" ,则会将该字符串对象放入到串池中,此时堆内存与串池中的 "ab" 是同一个对象
		String st2 = str.intern();
		// 给 str3 赋值,因为此时串池中已有 "ab" ,则直接将串池中的内容返回
		String str3 = "ab";
		// 因为堆内存与串池中的 "ab" 是同一个对象,所以以下两条语句打印的都为 true
		System.out.println(str == st2);
		System.out.println(str == str3);
	}
}
public class Main {
	public static void main(String[] args) {
        // 此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
		String str3 = "ab";
        // "a" "b" 被放入串池中,str 则存在于堆内存之中
		String str = new String("a") + new String("b");
        // 此时因为在创建 str3 时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab" 
		String str2 = str.intern();
        // false
		System.out.println(str == str2);
        // false
		System.out.println(str == str3);
        // true
		System.out.println(str2 == str3);
	}
}

垃圾回收

虚拟机参数配置:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
观察循环在100次和两百次的情况下控制台GC信息的打印和StringTable statistics:中的Number of entries 的变化

public class TestStringTable {
    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

GC日志分析

[GC (Allocation Failure) [PSYoungGen: 153600K->25593K(179200K)] 153600K->122684K(588800K), 0.0200653 secs] [Times: user=0.08 sys=0.23, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 179193K->25594K(179200K)] 276284K->268990K(588800K), 0.0258396 secs] [Times: user=0.02 sys=0.30, real=0.03 secs] 
[GC (Allocation Failure) [PSYoungGen: 179194K->25585K(179200K)] 422590K->416406K(588800K), 0.0256167 secs] [Times: user=0.02 sys=0.14, real=0.02 secs] 
GC表明这是一次小型GC(Minor GC),即年轻代GC
Allocation Failure 表示触发GC的原因。本次GC事件是由于对象分配失败,即年轻代中没有空间来存放新生成的对象引起的
PSYoungGen: 垃圾收集器的名称,这个名称表示的是在年轻代中使用的:并行的标记-复制,STW垃圾收集器
153600K->25593K(179200K)  表示年轻代回收前->回收后(年轻代内存大小)
153600K->122684K(588800K), 堆内存回收前大小->堆内存回收后大小(堆内存大小)
 user=0.02 sys=0.14, real=0.02 secs GC事件的持续时间,通过三个部分衡量,user表示GC线程所消耗的总CPU时间,sys表示操作系统和系统等待事件所消耗的时间,real则表示应用程序实际暂停时间。
  由于并不是所有的操作过程都能全部并行,所以在并行GC中,real约等于user+system/GC线程数
老年代大小= 588800 - 179200 

[Full GC (Ergonomics) [PSYoungGen: 25585K->7119K(179200K)] [ParOldGen: 390821K->409060K(409600K)] 416406K->416179K(588800K), [Metaspace: 9320K->9320K(1058816K)], 0.0532837 secs] [Times: user=0.06 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 160703K->155199K(179200K)] [ParOldGen: 409060K->409325K(409600K)] 569763K->564525K(588800K), [Metaspace: 9345K->9345K(1058816K)], 0.0216715 secs] [Times: user=0.16 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 155496K->154768K(179200K)] [ParOldGen: 409325K->409594K(409600K)] 564822K->564363K(588800K), [Metaspace: 9345K->9275K(1058816K)], 0.0625979 secs] [Times: user=0.44 sys=0.00, real=0.06 secs] 
[Full GC (Ergonomics) [PSYoungGen: 154927K->154884K(179200K)] [ParOldGen: 409594K->409594K(409600K)] 564522K->564479K(588800K), [Metaspace: 9277K->9277K(1058816K)], 0.0087468 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 154884K->154884K(179200K)] [ParOldGen: 409594K->409561K(409600K)] 564479K->564446K(588800K), [Metaspace: 9277K->9199K(1058816K)], 0.0454640 secs] [Times: user=0.33 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 154927K->0K(179200K)] [ParOldGen: 409567K->2318K(409600K)] 564494K->2318K(588800K), [Metaspace: 9201K->9201K(1058816K)], 0.0095533 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Ergonomics: 自动的调解gc暂停时间和吞吐量之间的平衡,然后你的虚拟机性能更好的一种做法。
对于注重吞吐量的收集器来说,在某个generation被过渡使用之前,GC ergonomics就会启动一次GC。发生本次full gc正是在使用Parallel Scavenge收集器的情况下发生的。
 PSYoungGen: 只收集young gen的GC
 ParOldGen: 用于清理老年代的垃圾收集器类型,在这里使用的是名为ParOldGen的垃圾收集器,这是一款并行STW垃圾收集器,算法为标记-清除-整理

参数设置:

  1. 初始空间大小
    通过"-Xms"参数指定Java堆初始空间大小;默认为1/64的物理内存空间
  2. 最大空间大小
    通过"-Xmx"参数指定java堆内存分配池的最大空间大小;默认为1/4的物理内存空间;
  3. Parallel垃圾收集器默认的最大堆大小是当小于等于192MB物理内存时,为物理内存的一半,否则为物理内存的四分之一;
  4. 各年代内存的占用空间与可用空间的比例
    1. 通过"-XX:MinHeapFreeRatio"和"-XX:MaxHeapFreeRatio"参数设置堆中各年代内存的占用空间与可用空间的比例保持在特定范围内;
    2. 新生代与老年代的大小比例
      3. 通过"-XX:NewRatio":控制新生代与老年代的大小比例; 默认设置"-XX:NewRatio=2"表新生代和老年代之间的比例为1:2; 换句话说,eden和survivor空间组合的年轻代大小将是总堆大小的三分之一;
      4. 新生代空间大小,通过"-Xmn"参数指定新生代(nursery)的堆的初始和最大大小;或通过"-XX:NewSize"和"-XX:MaxNewSize"限制年轻代的最小大小和最大大小
  5. 打印GC的日志详情-XX:+PrintGCDetails
  6. 内存逃逸配置-XX:+DoEscapeAnalysis
  7. 打印字符串常量池信息 : -XX:+PrintStringTableStatistics
  8. 打印 gc 的次数,耗费时间等信息 : -verbose:gc
  9. StringTable桶的个数-XX:StringTableSize=109桶个数(最少设置为 1009 以上)

方法区:

在JDK1.8之前使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;
JDK1.8, 永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并用显式管理元数据的空间:

  1. 从OS请求空间,然后分成块;
  2. 类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);
  3. 当为类加载器卸载类时,它的块被回收再使用或返回到操作系统;
  4. 元数据使用由mmap分配的空间,而不是由malloc分配的空间;
  5. (JDK8) 通过"-XX:MetaspaceSize" 参数指定类元数据区的内存阈值–超过将触发垃圾回收;
    请添加图片描述
  6. 生存周期:所有线程共享;生命周期与JVM相同;
  7. 作用:堆的逻辑组成部分,为类加载器加载Class文件并解析后的类结构信息提供存储空间; 以及提供JVM运行时常量存储的空间
  8. 存储内容:JVM加载的每一个类的结构信息
    1. **运行时常量池(Runtime Constant Pool)**字段和方法数据;
    2. 构造函数、普通方法的字节码内容以及JIT编译后的代码;
    3. 还包括一些在类、实例、接口初始化时用到的特殊方法;
  9. 内存分配特点
    1. JVM的规范没有限定实现方法区的内存位置;
    2. 固定大小的或者是根据计算动态扩展和收缩的;

运行时常量池;

运行常量池(Runtime Constant Pool)是方法区的一部分;

  1. 存储内容
    每一个类或接口的常量池(Constant_Pool)的运行时表示形式;
    包括了若干种不同的常量:
    1. 编译期可知的字面量和符号引用,也即Class文件结构中的常量池;当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
    2. 必须运行期解析后才能获得的方法或字段的直接引用
    3. 还包括运行时可能创建的新常量(如JDK1.6中的String类intern()方法)
  2. 例子:
public class ConstantPool {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

然后使用 javap -v ConstantPool.class 命令反编译查看结果。也可通过 jclasslib Bytecode Viewer idea插件查看;
**常量池表:**就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值