学习JVM---入门

打印 上一主题 下一主题

主题 517|帖子 517|积分 1551

1.JVM体系结构

JVM的位置


JVM体系结构


2.类加载器

双亲委派机制
  1. package java.lang;
  2. /**
  3. * 测试自定义java.lang.String类能否运行成功
  4. * 体会双亲委派机制
  5. *
  6. * 类加载器逐级向上检查:app->ext->boot
  7. * 发现boot类加载器中也有String类,但是没有main方法,于是报错
  8. * app:应用程序加载器
  9. * ext:扩展类加载器
  10. * boot:启动类(根)加载器
  11. *
  12. * 检查什么?每一级类加载器能够加载的类是固定的,不能越级加载。
  13. * boot能加载的类,app,ext就不能加载;同理,exit能加载的,app就不能加载。
  14. * 一个形象的比喻:类,app,ext,boot分别对应平民,村长,乡长,县长。
  15. * 村长会向乡长汇报,乡长向县长汇报。如果这个案子很特殊,应该有县长来处理,那么村长和乡长当然不能管了。
  16. *
  17. * 通过验证“hello”是否会输出,可以知道:先加载类,再去执行main方法。
  18. * 类是方法的载体,包括main方法,想要用方法,就得先加载类
  19. */
  20. public class String {
  21.     public static void main(String[] args) {
  22.         System.out.println("hello");        //这一行会输出吗?
  23.         String s = "";
  24.         System.out.println(s.getClass().getClassLoader());
  25.     }
  26. }
复制代码
运行结果:
  1. 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
  2.    public static void main(String[] args)
  3. 否则 JavaFX 应用程序类必须扩展javafx.application.Application
复制代码
  1. public class Cat {
  2.     /**
  3.      * 测试自定义普通类使用哪个类加载器
  4.      *
  5.      * 从app到boot检查是否有Cat类,发现ext和boot中都没有Cat类
  6.      * 所有直接还是用app加载器
  7.      */
  8.     public static void main(String[] args) {
  9.         System.out.println("Hello");
  10.         System.out.println(Cat.class.getClassLoader());     //运行结果:sun.misc.Launcher$AppClassLoader@18b4aac2
  11.     }
  12. }
复制代码
3.沙箱安全机制

4.native

native是Java的一个关键字,使用它可以调用本地方法,访问本地资源。
Thread类中的一个用法:
private native void start0();
执行到start0()时,start0()进入本地方法栈,然后调用本地方法接口JNI(Java Native Interface),然后调用本地方法库。
5.方法区

存哪些东西


  • static变量
  • final变量
  • Class对象
  • 常量池
实例变量存放在堆中。
面试题:一个实例的创建过程?

  • 类加载,Class对象存放在方法区中。

    • 父类静态成员
    • 子类静态成员
    • 父类代码块
    • 父类构造器
    • 子类代码块
    • 子类构造器

  • 实例的声明 Object obj,存放在栈中。
  • 实例的创建 new Object(),存放在堆中。
  • 将对象实例的地址赋给对象的引用(栈中的变量名指向堆中具体的对象)。obj = new Object();
  • 对对象的属性赋值。
  • 调用方法。

6.栈

特点:先进后出,后进先出
为什么main方法先执行,后结束?


进栈顺序:main(),test1(),test2()
出栈顺序:test2(),test1(),main()
为什么递归会引起栈溢出?


当调用递归出现死循环的情况时,栈溢出也就出现了。
测试代码:
  1. package stack_;
  2. public class TestStack {
  3.     public static void main(String[] args) {
  4.         test1();
  5.     }
  6.     static void test1(){
  7.         test2();
  8.     }
  9.     static void test2(){
  10.         test1();
  11.     }
  12. }
复制代码
运行结果:
  1. Exception in thread "main" java.lang.StackOverflowError
  2.         at stack_.TestStack.test2(TestStack.java:12)
  3.         at stack_.TestStack.test1(TestStack.java:8)
  4.         ……
  5.         此处省略1000多行(马德,typora软件都干卡死了)
  6.         ……
  7.         at stack_.TestStack.test2(TestStack.java:12)
  8.         at stack_.TestStack.test1(TestStack.java:8)
  9. Process finished with exit code 1
复制代码
7.堆

堆:heap
堆中的三个区域


  • 新生区

    • 伊甸园
    • 幸存区0
    • 幸存区1

  • 养老区
  • 永久区
新生区


伊甸园:类的创建,应用,甚至消亡。
伊甸园满了之后,触发GC,有一部分被销毁,有一部分进入幸存区。幸存区0,1又会发生数据交换。
老年区

新生区满了之后,触发Full GC,进入老年区。
老年区和新生区都满了,就会发生OOM。
一般不会出现OOM,因为99%的数据都是临时的,用完就不再使用,在伊甸园或幸存区就被回收了。
永久区

JDK1.6及以前:永久代,常量池位于方法区
JDK1.7:永久代,常量池位于堆
JDK1.8及以后,永久区更名为:元空间,常量池位于元空间
内存调优
  1. //虚拟机需要的最大内存
  2.         long max = Runtime.getRuntime().maxMemory();
  3.         //虚拟机初始化时的总内存
  4.         long original = Runtime.getRuntime().totalMemory();
  5.         System.out.println("max:" + max + "Byte," + max / 1024 / 1024 + "MB");
  6.         System.out.println("original:" + original + "Byte," + original / 1024 / 1024 + "MB");
  7. /**
  8. * 调优之前:
  9. * max / 电脑内存 ≈ 1 / 4
  10. * original / 电脑内存 ≈ 1 / 64
  11. *内存调优:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
  12. */
复制代码
运行结果:
  1. max:1029177344Byte,981MB
  2. original:1029177344Byte,981MB
  3. Heap
  4. PSYoungGen      total 305664K, used 20971K
  5.   eden space 262144K, 8% used
  6.   from space 43520K, 0% used
  7.   to   space 43520K, 0% used
  8. ParOldGen       total 699392K, used 0K
  9.   object space 699392K, 0% used
  10. Metaspace       used 3282K, capacity 4496K, committed 4864K, reserved 1056768K
  11.   class space    used 356K, capacity 388K, committed 512K, reserved 1048576K
  12. Process finished with exit code 0
复制代码
(305664K+699392K)/ 1024 = 981M
元空间的内存逻辑上存在,物理上不存在。
OOM 内存溢出

案例演示
  1. package heap_;
  2. import java.util.Random;
  3. /**
  4. * 测试堆内存溢出
  5. */
  6. public class TestOOM {
  7.     public static void main(String[] args) {
  8.         String s = "hello";
  9.         while (true){
  10.             s +=  s + s + new Random().nextInt(999999999);
  11.         }
  12.     }
  13. }
复制代码
运行结果:
  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  2.         at java.util.Arrays.copyOf(Arrays.java:3332)
  3.         at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  4.         at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
  5.         at java.lang.StringBuilder.append(StringBuilder.java:136)
  6.         at heap_.TestOOM.main(TestOOM.java:9)
  7. Process finished with exit code 1
复制代码
Java中的字符串可以不停的增加长度,但是JVM中的堆内存空间是有限的。
对虚拟机参数进行调整(-Xms8m -Xmx8m -XX:+PrintGCDetails),并观察运行结果。
可以看到很快就会运行结束,并报OOM错误。
出现OOM,如何解决?


  • 内存调大一点
  • 如果还是有问题,就要研究代码,是否有bug
使用Jprofiler分析内存

Jprofiler安装教程:https://www.cnblogs.com/zhangxl1016/articles/16220183.html
测试代码:
  1. package heap_;
  2. import java.util.ArrayList;
  3. public class TestDump {
  4.     byte[] arr = new byte[1024 * 1024];     //共1MB的空间
  5.     public static void main(String[] args) {
  6.         ArrayList<TestDump> list = new ArrayList<>();
  7.         int count = 0;
  8.         try {
  9.             while (true) {
  10.                 list.add(new TestDump());
  11.                 count++;
  12.             }
  13.         }
  14.         //OOM要用Error来捕获
  15.         catch (Error error) {
  16.             System.out.println("count=" + count);
  17.             error.printStackTrace();
  18.         }
  19.     }
  20. }
复制代码
运行结果:
  1. java.lang.OutOfMemoryError: Java heap space
  2. Dumping heap to java_pid3996.hprof ...
  3. Heap dump file created [7778880 bytes in 0.024 secs]
  4. count=6
  5. java.lang.OutOfMemoryError: Java heap space
  6.         at heap_.TestDump.<init>(TestDump.java:6)
  7.         at heap_.TestDump.main(TestDump.java:13)
  8. Process finished with exit code 0
复制代码
并在当前项目的根目录下生成了Jprofiler文件:

双击打开:

可以看到最上边的列表项(ArrayList)占用的内存是最高的,说明是它出了问题。
那么具体是代码中的哪一行有问题呢?

因为这个案例只有main方法一个线程,所以直接点main,然后在下方可以看到是源代码第13行出了问题。
为什么到count=6时就报错了呢?因为在第13行每加一个对象,就会增加1MB的空间,我们分配的最大空间是8MB,所以加到6的时候,就会发生内存溢出。
记得删除生成的Jprofiler文件,因为比较占用空间。

虚拟机参数

-Xms 设置初始化内存大小 默认1/64
-Xmx 设置最大分配内存 默认1/4
-XXrintGCDetails 打印GC垃圾回收信息
-XX:HeapDumpOnOutOfMemoryError 转储OOM异常信息
上一个案例设置为:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError


垃圾回收

哪些东西是垃圾?

不需要的,而且占了空间的对象。
在哪里回收?


为什么不在栈回收?因为栈里没有垃圾,栈不存对象,只存储变量的引用和方法。
举个例子:假设堆中存的是具体的人,那么栈中存的是人的姓名。人去世之后,这个具体的对象依然存在堆中,而且占用空间,所以它需要被回收。栈里存的名字,用完了就自动出栈了,所以栈中不存在垃圾,亦无需回收。
如何回收?

引用计数法


每一个对象都有一个计数器,某个对象被使用一次,相应的计数器就会加1。垃圾回收时,计数器为0的对象被回收。
缺点:当对象很多时,计数器也会占用大量的资源。
复制算法

主要针对新生区
假设当前伊甸园有两个对象:o1,o2,GC之后,o1被销毁,o2进入幸存区。幸存区有两个,要去哪个呢?哪个是空的,就去哪个。另外一个幸存区中,如果有对象,也会进入to区。然后当前的to区又变成from区,from区又变成to区。一定要保证有一个幸存区是空的,这个区就是to区。
”谁空谁是to“

一个对象在新生区经历15次(默认次数)还没有消亡,那么它会进入老年区,类似于久经沙场的老兵,活到最后就可以养老了。
这里涉及到一个JVM参数:-XX:MaxTenuringThreshold=15,默认值是15,如果这个值调到很大,那么新生区的对象会很难进入到老年区中。

  • 优点:没有内存碎片。
  • 缺点:浪费空间。

    • 总是要保证to区是空的。
    • 假设对象100%存活,to区就要面临无法容纳所有对象的情况。to区同时要面临伊甸园和form区两个方向的对象。

  • 最佳使用场景:对象存活度较低的区域---新生区。
标记压缩清除算法

第一次扫描:标记存活的对象,没被标记的就是不需要的
第二次扫描:清除没用的对象,会产生碎片
最后一次扫描:被标记的对象向一侧移动,另一侧就是被清除掉的

优点:不会增加额外的空间
缺点:扫描会浪费时间,会有内存碎片产生
优化:先进行几次标记清除,最后再统一压缩
算法比较


  • 内存效率:复制算法>标记清除>标记压缩(时间复杂度)

    • 复制是一个动作,清除是两个动作,压缩有三个动作

  • 内存整齐度:复制算法=标记压缩>标记清除

    • 复制和压缩都没有内存碎片

  • 内存利用率:标记清除=标记压缩>复制

    • 清除和压缩都在原有空间中操作,复制算法总是要保证to区是空的

有没有最优算法呢?
没有,只有最合适的:分代收集算法
新生代:存活率低,适合复制算法。不用担心to区的空间不够。
老年代:区域大,存活率高,适合标记清除+标记压缩混合实现。碎片不是很多的时候用标记清除,碎片积累到一定程度,就需要压缩,这其中就要涉及到内存调优。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我爱普洱茶

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表