前言
JVM总是开始于main()方法,方法必须是公有、静态、void、参数为String数组; 被初始化线程执行启动。
public static void main(String[] args) {}
线程分为: 守护线程(daemon) 和 普通线程, main()的初始化线程必定不是守护线程。在setDaemon(true)时如果isAlive,则抛出IllegalThreadStateException。
形参 & 实参
形式参数指在定义函数名后小括号里定义的args,用于接受来自调用函数的参数,作用域只在函数内有效;实际参数指在调用时传递给函数的真正的参数。形参的本质是一个名字,不占用内存空间;实参的本质是一个变量,已经占用内存空间。
在函数体内
- 对于基本类型,改变不会影响实参;
- 对于引用类型,改变会直接影响实参,无论改变的是对象中的基础类型或引用类型变量,修改的最终是堆内地址实际的对象。
package com.noob.learn.netty;import lombok.AllArgsConstructor;import lombok.Data;public class Test { private static void function(String a, int b) { a = "100a"; b = 1000; } private static void function2(Arg arg) { arg.arg1 = "function2"; arg.arg2 = 100; } public static void main(String[] args) { String arg1 = "arg1"; int arg2 = 0; function(arg1, arg2); System.out.println(String.format("arg1=%s, arg2=%s", arg1, arg2)); Arg arg = new Arg("arg1", 0); function2(arg); System.out.println(arg.toString()); }}@Data@AllArgsConstructorclass Arg { public String arg1; public int arg2;}
测试结果:
arg1=arg1, arg2=0 // 没变Arg(arg1=function2, arg2=100) // 改变
内存模型JMM(Java Memory Model)
存在一个主内存(Main Memory),储存ava中所有实例变量,对于所有线程都是共享的。每个线程都有一个私有的工作内存(Local Memory), 存储了该线程以读/写共享变量的副本,(本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化)工作内存由缓存 和堆栈 <理解为:栈>两部分组成:
- 缓存中保存的是主存中变量的拷贝,缓存并不总能时刻与主存同步,修改变量可能没有立刻写入到主存中; (关联的知识点<待写>: 内存屏障 & 指令重排 & volatile )
- 堆栈中保存的是线程的局部变量,线程之间无法相互直接访问栈中的变量。JVM对堆栈只进行两种操作: 以帧为单位的压栈和出栈操作。当线程调用方法时,虚拟机将堆栈块压入堆栈中,执行结束后,弹出对应块抛弃。
JMM决定一个线程对共享变量的写入何时对另一个线程可见。根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分:
虚拟机栈
每个线程有一个私有的栈,随着线程的创建而创建,虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧(大小可以固定也可以动态扩展),用于存放局部变量表(基本数据类型和对象引用<句柄>),操作数栈,动态链接,方法出口等信息。< 对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象)或驻留于常规RAM(随机访问存储器)区域,可通过 “堆栈指针” 获得。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。>
每一个方法从调用直到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。把内存分为堆内存和栈内存,其中的栈内存就指的是虚拟机栈的局部变量表部分。局部变量表存放了编译期可以知道的基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置),和返回后所指向的字节码的地址。其中64 位长度的long 和double 类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。当递归层次太深时,会引发java.lang.StackOverflowError,这是虚拟机栈抛出的异常。
还有另一种错误,那就是当申请不到空间时,会抛出 OutOfMemoryError。这里有一个小细节需要注意,catch 捕获的是 Throwable,而不是 Exception。因为 StackOverflowError 和 OutOfMemoryError 都不属于 Exception 的子类
优点:存取速度比堆要快,仅次于寄存器。
本地方法栈
保存native方法进入区域的地址。
寄存器
指令寄存器、地址寄存器、程序计数器。位于处理器内部,处理速度最快。寄存器是根据需要由编译器分配。
程序计数器
在CPU的寄存器中有一个PC寄存器,存放下一条指令地址,这里,虚拟机不使用CPU的程序计数器,自己在内存中设立一片区域来模拟CPU的程序计数器。只有一个程序计数器是不够的,当多个线程切换执行时,那就单个程序计数器就没办法了,虚拟机规范中指出,每一条线程都有一个独立的程序计数器。注意,Java虚拟机中的程序计数器指向正在执行的字节码地址,而不是下一条。
堆(heap)
堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建, 存放所有new出来的对象和数组。堆中的对象的由垃圾回收器负责回收,当申请不到空间时会抛出 OutOfMemoryError。
优点:可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。 缺点:由于要在运行时动态分配内存,存取速度较慢。方法区
保存虚拟机加载并解析类后的信息、常量、静态变量(JDK7中被移到Java堆),即时编译期编译后的代码(类方法)等数据。所有的线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。
虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。同时,由于类class是JVM实现的一部分,并不是由应用创建的,所以又被认为是“非堆(non-heap)”内存。《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范标准,而永久代是一种实现,其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收.
常量池
官方文档说明:https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html
In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application<字符字面量(字符串常量池)不再分配在永久代中,代替的是分配来堆内存中的年轻代与年老代中>. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.<大部分应用对于堆的变化感知很少,但是有太多classes或需要大量使用动态添加String进常量池的方法的大应用感知强烈>
在JDK1.7中的HotASpot中,已经把原本放在方法区的字符串常量池移出。
- 将interned String移到Java堆中
- 将符号Symbols移到native memory(不受GC管理的内存)
从JDK7开始永久代的移除工作,贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap,但永久代仍然存在于JDK7,并没有完全的移除:
- 符号引用(Symbols)转移到了native memory。
- 字面量(interned strings)转移到了java heap。
- 类的静态变量(class statics)转移到了java heap。
随着JDK8的到来,JVM不再有PermGen, 但类的元数据信息(metadata)还在,只不过不再是存储在堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。