jvm的内存基本就是这五个分区, 不同版本中方法区是有变动的。
堆(Heap)
java 中最大的一块内存空间, 内部会进行分代。
新生代和老年代。
新生代和老年代的大小比例建议为1:2。 如果比例不对会出现问题,我就遇到过xmx=512M xmn=384M的配置的情况。 运行一段时间后,jvm进程自动结束了。 查看GC日志发现频繁的由于内存分配失败的GC. 最后一次GC达到了72秒后程序挂了。
xmx=512M xmn=384M 这样的配置会是新生代384M 老年代只有128M. 新生代相对偏大。 这样新生代的中对象就会延迟GC, 但是每次GC的时候要复制的对象太多了,反而加大了复制时间。
另外一个问题,随着对象的存活,慢慢转到老年代中, 老年代会很快占满,当一次Minor GC后需要移动大量存活对象到老年代中, 但是此时老年代快要占满了,这个时候就会OOM. 程序无法运行下去。
新生代的垃圾回收使用的是复制清除的方式, 而老年代使用的是标记整理方式。 但是最新的G1和ZGC垃圾收集器对于垃圾收集的方式都有很大的变化, 并且最新的jdk15中ZGC收集器已经成为正式版了。
程序计数器
一小块线程独享的空间, 记录当前线程执行的自己吗的计数。 使用这个可以实现分支跳转,循环跳转, 异常跳转,线程上下文切换后的回复等。
方法区
用来存放已经被虚拟机加载的类的相关信息, 运行时常量池, 字符串常量池等等。 我们java中经常说的类加载机制中加载的类信息就是存在这里。 由于类被加载进去了后就是不可变的,所以方法区的信息是被所有的线程共享的,方法区内部存储的都是一些不可变的信息。
但是java8中已经把方法区拆了, 把运行时常量池和静态变量放入了堆中,把类的信息等其余部分放入了堆外空间中, 这个堆外空间称为元数据空间。
为何拆分迁移方法区:
- 方法区容易出现内存泄漏, 并且方法区的内存回收只能通过Full GC, 效率不高。
- java的类支持动态加载, 因此方法区的大小不易固定,放在堆外空间就可以直接扩张使用物理内存,不用受永久代大小的限制
- 有些虚拟机的实现中没有永久代的概念
虚拟机栈
线程私有的空间, 方法的调用就是栈帧的入栈, 返回就是调用的出栈, 栈的先进后出的特点很好的支持了方法的调用。
本地方法栈
jdk中包含很多的native的方法的调用,这些方法的调用也会伴随栈的使用, 这个栈是单独的。
java程序运行过程
启动申请内存
java程序启动了后,第一步就是根据配置(或者默认配置)向操作系统申请内存。 并根据配置把内存分配成堆栈, 方法区等
加载class
根据类加载机制,区加载class文件,初始化类生成类对象,常量等,并将这些信息放入方法区。
当然类信息并不是一次全部加载到内存中的,而是根据需要加载的。
执行main方案
main方法是一个静态方法, 任何程序都是由main方法最先启动的, 在根据main方法中的逻辑逐步生成对象,调用方法等。 在这个过程中伴随着堆的扩张, GC的回收等。
思考
java的最重要的几大特性:
跨平台
jvm很好的屏蔽了底层的差异性,使得java编译的class文件在各个平台的虚拟机上运行都能得到一致的运行结果。 为了跨平台java定义了自己的JMM模型和先行发生原则。
Mac电脑的系统中, 文件名默认是不区分大小写的,但是一些linux中文件名是区分大小写的,我就碰到过在Mac中没有问题的代码在服务器上就会报找不到文件错误。
垃圾回收
垃圾回收极大的解放了程序员的负担, 使得java程序员不用过多的关注对象内存的回收,只要写出能运行的程序,一般都不会有什么内存问题。
但是GC也不是万能的,它会掩盖很多的内存问题,使得很多的代码缺陷不容易显露出来, 直到线上爆雷了才发现。
严谨的语言格式规范
java是一个规范严格而有严谨的语言, 为了兼容以前的版本, 语言的变化有慢,而这又常常遭到别人的诟病。 但是严谨的语言可以保证程序不容易出错, 并且不同人写出的程序也大同小异, 也容易构建大型的系统而不出错。 这既是缺点也是优点吧。
最新的java版本已经是6个月发布一次, jdk15已经发布,并且很多的新的语言特性已经支持, 比如文本块, switch语句等等, java会越变越好的。