Waktunya telah tiba untuk Java 12! Ulasan JEP panas


Enam bulan telah berlalu, yang berarti sudah waktunya untuk menginstal Java baru ! Itu adalah perjalanan yang panjang, dan sedikit yang mencapai akhir. Garis mentah jatuh dari JEP yang menarik, tetapi kita akan berbicara tentang sisanya di bawah potongan.


Bagaimana semuanya berjalan


Rilis versi baru Jawa berlangsung sesuai dengan siklus rilis baru "dipercepat" dengan panjang sekitar enam bulan. Tanggal pasti ditentukan pada halaman proyek . Ada beberapa fase utama untuk JDK 12:


  • 2018/12/13 - Fase pertama pelambatan (saat ini, garpu dibuat dari cabang utama di repositori);
  • 2019/01/17 - Fase kedua dari pelambatan (lengkapi semua yang mungkin);
  • 2019/02/07 - Kandidat rilis (hanya bug paling penting yang diperbaiki);
  • 2019/03/19 - Rilis, Ketersediaan Umum. <- kamu di sini

Apa yang kita dapatkan dari jadwal ini? Ya, sebenarnya, tidak ada apa-apa - kami baru saja tiba di garis finish, dan kami menyaksikan para pecinta warisan dari ketinggian JDK 12 yang baru.


Bug! Panik! Semua ke bawah!



Ketika versi non-LTS baru keluar, biasanya semua orang tidak peduli tentang fitur baru. Akan lebih menarik jika semuanya akan hancur berantakan.


Tentu saja, ada bug, banyak, tetapi tidak di JDK 12 :) Dilihat oleh jir, semuanya normal:



Saya akan mengutip permintaan itu sehingga Anda mengerti persis apa "norma" itu:


project = JDK AND issuetype = Bug AND status in (Open, "In Progress", New) AND priority in (P1) AND (fixVersion in (12) OR fixVersion is EMPTY AND affectedVersion in (12) AND affectedVersion not in regexVersion("11.*", "10.*", "9.*", "8.*", "7.*", "6.*")) AND (labels is EMPTY OR labels not in (jdk12-defer-request, noreg-demo, noreg-doc, noreg-self)) AND (component not in (docs, globalization, infrastructure) OR component = infrastructure AND subcomponent = build) AND reporter != "Shadow Bug" ORDER BY priority, component, subcomponent, assignee 

Tentu saja, secara umum bug ada tempatnya, mereka tidak akan pergi ke mana pun dalam proyek sebesar ini. Hanya diklaim bahwa bug P1 saat ini belum diketahui.


Komunikasi yang lebih formal dengan bug dideklarasikan dalam dokumen khusus, JEP 3: Proses Pelepasan JDK , yang dimiliki oleh pelayan abadi kita pada gelombang turbulen di Laut Jawa - Mark Reinhold.


Dan khususnya, ada baiknya menggali paragraf jitu siapa yang harus disalahkan dan apa yang harus dilakukan cara mentransfer tiket jika Anda tidak punya waktu untuk rilis ke-12. Penting untuk memasukkan bugtracker label jdk$N-defer-request di mana N menunjukkan rilis mana yang ingin Anda transfer, dan tinggalkan komentar, baris pertama di antaranya adalah Deferral Request . Selanjutnya, peninjauan semua permintaan tersebut diambil oleh pimpinan dari masing-masing bidang dan proyek.


Masalah lulus TCK tidak dapat diabaikan dengan cara ini - dijamin bahwa Java tetap Java, dan bukan sesuatu yang seperti katak. jdk$N-defer-request label tidak pernah hilang. Sangat menarik apa yang mereka lakukan dengan orang yang melanggar aturan untuk tidak menghapus tag - Saya sarankan memberi makan marmut.


Namun, dengan cara ini Anda dapat melihat berapa banyak bug yang telah porting ke JDK 13. Mari kita coba pertanyaan ini:


 project = JDK AND issuetype = Bug AND status in (Open, "In Progress", New) AND (labels in (jdk12-defer-request) AND labels not in (noreg-demo, noreg-doc, noreg-self)) AND (component not in (docs, globalization, infrastructure) OR component = infrastructure AND subcomponent = build) AND reporter != "Shadow Bug" ORDER BY priority, component, subcomponent, assignee 

Hanya 1 buah, JDK-8216039 : "TLS dengan BC dan RSASSA-PSS memecah ECDHServerKeyExchange". Tidak tebal. Jika argumen ini masih tidak membantu, maka, sebagai pengacara Anda, saya sarankan mencoba obat penenang.


Dan apa intinya?



Jelas bahwa sebagian besar fitur tidak mempengaruhi pengguna (pemrogram Java), tetapi pengembang OpenJDK itu sendiri. Oleh karena itu, untuk berjaga-jaga, saya membagi fitur menjadi eksternal dan internal . Anda dapat melewati yang internal, tetapi saya tersinggung, saya menulis begitu banyak teks.


189: Shenandoah: Pengumpul Sampah Jeda Waktu Rendah (Eksperimental)


Fitur eksternal . Singkatnya, orang-orang tidak menyukainya ketika Jawa melambat, terutama jika SLA membutuhkan respons terhadap urutan 10-500 milidetik. Kami sekarang memiliki GC low-punch gratis yang mencoba bekerja lebih dekat ke tepi kiri kisaran ini. Imbalannya adalah kami menukar CPU dan RAM untuk mengurangi latensi. Penandaan pinggul dan fase pemadatan bekerja secara paralel dengan utas aplikasi langsung. Jeda kecil yang tersisa disebabkan oleh fakta bahwa Anda masih perlu mencari dan memperbarui akar grafik objek.


Jika tidak ada di atas yang masuk akal bagi Anda - tidak masalah, Shenandoah hanya berfungsi , terlepas dari memahami atau tidak memahami proses yang mendasarinya.


Alexei Shipilev, Christina Flood dan Roman Kennke sedang mengerjakannya - Anda harus berusaha keras untuk tidak tahu tentang orang-orang ini. Jika Anda umumnya memahami cara kerja GC tetapi tidak tahu apa yang bisa dilakukan pengembang di sana, saya sarankan Anda melihat terjemahan indah dari artikel Leshina yang luar biasa "Pengumpul Sampah Homemade untuk OpenJDK" atau seri JVM Anatomy Quark . Ini sangat menarik .


Sangat penting. Oracle memutuskan untuk tidak mengirimkan Sheandoah dengan rilis apa pun - baik yang ada di jdk.java.net maupun yang ada di oracle.com. Mengingat Shenandoah adalah salah satu fitur paling penting dari JDK 12, ada baiknya memasang beberapa perakitan resmi lainnya, misalnya, dari Azul .



230: Microbenchmark Suite


Fitur batin . Jika Anda pernah mencoba menulis microbenchmarks, maka Anda tahu bahwa ini dilakukan pada JMH. JMH adalah kerangka kerja untuk membuat, merakit, meluncurkan dan menganalisis microbenchmark untuk Java dan bahasa JVM lainnya, Anda sendiri mengerti siapa yang menulisnya (semua kebetulan adalah acak). Sayangnya, tidak semua yang dilakukan di dunia aplikasi "normal" dapat diterapkan di dalam JDK. Sebagai contoh, kita tidak akan pernah melihat kode Spring Framework normal di sana.


Untungnya, mulai dari versi 12, Anda dapat menggunakan setidaknya JMH, dan sudah ada serangkaian tes yang tertulis di sana. Anda dapat melihatnya di jdk/jdk/test/micro/org/openjdk/bench (Anda dapat langsung melihat di browser, jalur ini adalah tautan).


Misalnya, beginilah tampilan tes GC .


Biarkan saya mengingatkan Anda bahwa kami tidak memiliki StackOverflow di sini, dan dilarang menggunakan kode dari copy-paste, di sini dan selanjutnya, tanpa membaca dan mengamati semua lisensi dari file yang sesuai dan proyek OpenJDK secara umum, jika tidak, Anda akan dengan mudah meminta kaus kaki terakhir untuk menuntut.

 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class Alloc { public static final int LENGTH = 400; public static final int ARR_LEN = 100; public int largeLen = 100; public int smalllen = 6; @Benchmark public void testLargeConstArray(Blackhole bh) throws Exception { int localArrlen = ARR_LEN; for (int i = 0; i < LENGTH; i++) { Object[] tmp = new Object[localArrlen]; bh.consume(tmp); } } //... } 



325: Alihkan Ekspresi (Pratinjau)


Fitur eksternal . Ini secara fundamental akan mengubah pendekatan Anda untuk menulis switch tanpa akhir dengan panjang lebih dari dua layar. Lihat:


Virgin Java Switch vs ...


 int dayNum = -1; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: dayNum = 6; break; case TUESDAY: dayNum = 7; break; case THURSDAY: case SATURDAY: dayNum = 8; break; case WEDNESDAY: dayNum = 9; break; } 

Mengapa ini buruk : ada banyak surat, Anda dapat melewatkan waktu istirahat (terutama jika Anda seorang pecandu narkoba atau Anda menderita ADHD).


... vs Chad Java Swtich Expression!


 int dayNum = switch (day) { case MONDAY -> 0; case TUESDAY -> 1; default -> { int k = day.toString().length(); int result = f(k); break result; } }; 

Mengapa bagus : beberapa huruf, aman, nyaman, fitur keren baru.


Bonus : jika Anda seorang sadis, itu akan memberi Anda kepuasan terdalam, karena ribuan pengembang IDE sekarang tersiksa dengan dukungan fitur ini. Ya, ya? Anda dapat menangkapnya setelah laporan pada 6 April dan dengan lembut meminta untuk memberikan semua rincian kotor.


Ini adalah fitur pratinjau, itu tidak akan berfungsi! Saat mengkompilasi, di javac Anda harus melewati opsi baris perintah --enable-preview --release 12 , dan untuk menjalankan melalui java - hanya flag --enable-preview .



334: JVM Constants API


Fitur batin . Pengembang ingin memanipulasi file kelas. Anda perlu melakukan ini dengan nyaman, dan ini adalah pernyataan masalahnya. Setidaknya, itulah yang dikatakan Brian Goetz, yang memiliki JEP ini, :-) Semua ini adalah bagian dari medan perang yang lebih besar, tetapi untuk saat ini kami tidak akan terlalu dalam.


Setiap kelas Java memiliki apa yang disebut "kolam konstan" di mana ada dump dari beberapa nilai (seperti string dan int), atau entitas runtime seperti kelas dan metode. Anda dapat menggali ke dalam dump ini menggunakan instruksi ldc - "load costant", sehingga semua sampah ini disebut konstanta yang dapat dimuat. Masih ada kasus khusus untuk invokedynamic, tetapi tidak apa-apa.


Jika kita bekerja dengan classfile, maka kita ingin dengan mudah mensimulasikan instrumen bytecode, dan karenanya - konstanta yang dapat dimuat. Keinginan pertama adalah hanya membuat tipe Java yang sesuai, tetapi bagaimana Anda menyajikannya dengan kelas "hidup", struktur CONSTANT_Class_info ? Objek Class tergantung pada kebenaran dan konsistensi pemuatan kelas, dan dengan pemuatan kelas di Jawa, bacchanalia neraka dibuat. Untuk memulainya, tidak semua kelas dapat dimuat ke dalam VM, tetapi Anda masih perlu menjelaskannya!


Saya ingin entah bagaimana mengelola hal-hal seperti kelas, metode, dan binatang yang kurang dikenal seperti pegangan metode dan konstanta dinamis, dengan mempertimbangkan semua seluk-beluk ini.


Ini dipecahkan dengan memperkenalkan tipe baru dari tautan simbolik berbasis nilai (dalam arti JVMS 5.1 ), yang masing-masing menggambarkan tipe konstan tertentu. Menjelaskan secara murni nominal, terpisah dari kelas pemuatan atau masalah akses. Mereka tinggal dalam paket-paket seperti java.lang.invoke.constant dan tidak memintanya, tetapi Anda dapat melihat tambalan di sini .



340: Satu Port AArch64, Bukan Dua
Fitur eksternal . Sudah di JDK 9 ada situasi aneh ketika Oracle dan Red Hat secara bersamaan membuat port ARM mereka dalam keadaan siaga. Dan sekarang kita melihat akhir dari cerita: bagian 64-bit dari pelabuhan Oraklov telah dihapus dari hulu.


Anda bisa menggali sejarah untuk waktu yang lama, tetapi ada cara yang lebih baik. BellSoft berpartisipasi dalam pengembangan JEP ini, dan kantornya berlokasi di St. Petersburg, di sebelah bekas kantor Oracle.


Karena itu, saya segera beralih ke Alexey Voitilov, CTO dari BellSoft:


"BellSoft meluncurkan Liberica JDK, yang, selain x86 Linux / Windows / Mac dan Solaris / SPARC, juga mendukung ARM. Dimulai dengan JDK 9 untuk ARM, kami berfokus pada peningkatan kinerja port AARCH64 untuk aplikasi server dan terus mendukung port ARM 32-bit untuk Jadi, pada saat rilis JDK 11, ada situasi di mana tidak ada yang mendukung bagian port 64-bit dari Oracle (termasuk Oracle), dan komunitas OpenJDK memutuskan untuk menghapusnya untuk fokus pada port AARCH64. Saat ini, lebih produktif ( lihat, misalnya, JEP 315 , yang kita diintegrasikan ke dalam JDK 11) dan, mulai dari JDK 12, mendukung semua fitur yang ada di port dari Oracle (yang terakhir, Minimal VM, saya terintegrasi pada bulan September). Oleh karena itu, saya senang membantu Bob Vandette menghapus rudiment ini di JDK 12. Akibatnya, OpenJDK komunitas menerima satu port pada AARCH64 dan satu port ARM32, yang tentunya membuat mereka lebih mudah untuk didukung. "



341: Arsip CDS Default


Fitur batin . Masalahnya adalah bahwa pada awal aplikasi Java, ribuan kelas dimuat, yang menciptakan perasaan bahwa Java melambat secara signifikan pada saat startup. Tetapi siapa yang ada di sana untuk berbohong, ini bukan hanya "sensasi" - memang begitu. Untuk memperbaiki masalah dari zaman kuno, berbagai ritual dipraktikkan.


Class Data Sharing adalah fitur yang telah datang kepada kami sejak berabad-abad yang lalu, seperti fitur komersial dari JDK 8 Update 40. Ini memungkinkan Anda untuk mengemas semua sampah startup ini ke dalam arsip dengan format Anda sendiri (Anda tidak perlu tahu yang mana), setelah itu kecepatan peluncuran aplikasi meningkat. Dan setelah beberapa saat, JEP 310 muncul: Application Class-Data Sharing, yang memungkinkan kami untuk bekerja dengan cara yang sama tidak hanya dengan kelas sistem, tetapi juga kelas aplikasi.


Untuk kelas JDK, terlihat seperti ini. Pertama, kita membuang kelas dengan perintah java -Xshare:dump , dan kemudian menjalankan aplikasi, menyuruhnya menggunakan cache ini: java -Xshare:on -jar app.jar . Semuanya, startup telah sedikit membaik. Tahukah Anda tentang fitur ini? Banyak yang masih belum tahu!


Kelihatannya aneh di sini: mengapa setiap kali ritual menulis -Xshare:dump jika hasil default dari perintah ini sedikit dapat diprediksi bahkan pada tahap membuat distribusi JDK? Menurut dokumentasi , jika distribusi Java 8 diinstal menggunakan installer, maka tepat pada saat instalasi itu harus menjalankan perintah yang diperlukan untuk Anda. Seperti, penginstal diam-diam menambang di sudut. Tapi mengapa? Dan apa yang harus dilakukan dengan distribusi, yang didistribusikan bukan sebagai installer, tetapi sebagai file zip?


Sederhana: dimulai dengan JDK 12, arsip CDS akan dihasilkan oleh pembuat kit distribusi, segera setelah menautkan. Bahkan untuk build malam (asalkan 64-bit dan asli, bukan untuk kompilasi silang).


Pengguna bahkan tidak perlu tahu tentang keberadaan fitur ini, karena, dimulai dengan JDK 11, -Xshare:auto diaktifkan secara default, dan arsip semacam itu akan mengambil secara otomatis. Jadi, fakta memperbarui ke JDK 12 mempercepat peluncuran aplikasi!



344: Koleksi Campuran yang Diabaikan untuk G1


Fitur batin . Sejujurnya Saya tidak mengerti apa pun dalam karya G1 Penjelasan tentang fitur-fitur GC adalah tugas tanpa pamrih. Itu membutuhkan pemahaman tentang detail karyanya baik dari penjelajah dan pemahaman. Bagi kebanyakan orang, GC adalah semacam kotak tembakau yang bisa Anda curang jika terjadi sesuatu. Karena itu, masalahnya harus dijelaskan dengan cara yang lebih sederhana.


Masalah : G1 mungkin bekerja lebih baik.


Nah, masalahnya adalah bahwa GC adalah kompromi dari banyak parameter, salah satunya adalah panjang jeda. Terkadang jeda terlalu panjang, dan menyenangkan untuk membatalkannya.


Kapan ini terjadi? G1 benar-benar menganalisis perilaku aplikasi dan memilih bagian depan pekerjaan (dinyatakan sebagai kumpulan koleksi ) berdasarkan kesimpulannya. Ketika ruang lingkup pekerjaan disetujui, G1 berjanji untuk mengumpulkan semua benda hidup dalam kumpulan koleksi, keras kepala dan tanpa henti, dalam satu kali duduk. Terkadang butuh terlalu banyak waktu. Intinya, ini berarti bahwa G1 salah menghitung jumlah pekerjaan. Anda dapat membodohinya dengan tiba-tiba mengubah perilaku aplikasi Anda sehingga heuristik akan bekerja di atas data buruk ketika terlalu banyak daerah lama masuk ke kumpulan set.


Untuk keluar dari situasi ini, G1 diselesaikan dengan mekanisme berikut: jika heuristik secara teratur memilih jumlah pekerjaan yang salah, G1 beralih ke pengumpulan sampah tambahan, langkah demi langkah, dan setiap langkah berikutnya (jika tidak sesuai dengan waktu pelaksanaan target) dapat dibatalkan. Tidak masuk akal untuk mengumpulkan sesuatu secara bertahap (wilayah muda), oleh karena itu, semua pekerjaan tersebut disorot dalam blok "wajib", yang masih dilakukan terus menerus.


Apa yang harus dilakukan dengan pengguna akhir? Tidak ada, Anda perlu meningkatkan ke JDK 12, semuanya akan menjadi lebih baik dengan sendirinya.



346: Segera Kembalikan Memori yang Tidak Dipakai Sebelumnya dari G1


Fitur batin . Masalahnya adalah jika kita memiliki pinggul besar yang tidak digunakan siapa pun secara aktif, tampaknya adil untuk mendapatkan semua memori tidak aktif ini kembali ke sistem operasi. Namun sebelum JDK 12, ini tidak terjadi.


Untuk mencapai tujuannya dalam hal panjang jeda yang diizinkan, G1 melakukan serangkaian siklus bertahap, paralel, dan multi-tahap. Dalam JDK 11, ini memberikan memori yang dikomit ke sistem operasi hanya dengan GC penuh, atau selama fase penandaan paralel. Jika Anda menghubungkan logging (-Xloggc: /home/gc.log -XX: + PrintGCDetails -XX: + PrintGCDateStamps), maka fase ini akan ditampilkan seperti ini:


 8801.974: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 12582912000 bytes, allocation request: 0 bytes, threshold: 12562779330 bytes (45.00 %), source: end of GC] 8804.670: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested] 8805.612: [GC concurrent-mark-start] 8820.483: [GC concurrent-mark-end, 14.8711620 secs] 

Yang lucu adalah bahwa G1, seperti itu, berjuang dengan berhenti total, dan siklus bersamaan dimulai hanya dengan alokasi yang sering dan tumpukan tersumbat. Situasi kita, ketika tidak ada yang menyentuh pinggul, justru sebaliknya. Situasi ketika G1 tergores untuk memberikan memori ke sistem operasi akan terjadi sangat jarang!


Jadi, setiap orang akan mendapat skor pada masalah ini ("beli lebih banyak RAM, yang seperti bajingan!"), Jika bukan hanya satu tapi - ada segala macam awan dan wadah di mana ini berarti pemanfaatan yang tidak memadai dan kehilangan uang yang serius. Lihat, laporan yang keren , dipenuhi sampai penuh dengan rasa sakit.


Solusinya adalah mengajar G1 untuk berperilaku baik dalam kasus khusus ini, seperti yang sudah diketahui oleh Shenanda atau GenCon dari OpenJ9. Hal ini diperlukan untuk menentukan pemanfaatan pinggul yang tidak memadai dan, karenanya, mengurangi penggunaannya. Pada beberapa tes di Tomcat, ini memungkinkan untuk mengurangi konsumsi memori hampir setengahnya.


Intinya adalah bahwa aplikasi dianggap tidak aktif, atau jika interval (dalam milidetik) telah getloadavg() dari build terakhir dan tidak ada siklus bersamaan, atau jika getloadavg() untuk periode satu menit menunjukkan beban di bawah ambang tertentu. Begitu salah satu dari ini terjadi, pengumpulan sampah secara berkala dimulai - itu pasti tidak akan membersihkan serta perakitan penuh, tetapi itu akan mempengaruhi aplikasi minimal.


Anda dapat mendorongnya ke dalam log ini:


 (1) [6.084s][debug][gc,periodic ] Checking for periodic GC. [6.086s][info ][gc ] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms (2) [9.087s][debug][gc,periodic ] Checking for periodic GC. [9.088s][info ][gc ] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms (3) [12.089s][debug][gc,periodic ] Checking for periodic GC. [12.091s][info ][gc ] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms (4) [15.092s][debug][gc,periodic ] Checking for periodic GC. [15.097s][info ][gc ] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms (5) [18.098s][debug][gc,periodic ] Checking for periodic GC. [18.100s][info ][gc ] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms (6) [21.101s][debug][gc,periodic ] Checking for periodic GC. [21.102s][info ][gc ] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms (7) [24.104s][debug][gc,periodic ] Checking for periodic GC. [24.104s][info ][gc ] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms 

Mencari tahu? Saya tidak. Di JEP, ada terjemahan bahasa isyarat terperinci dari setiap baris log, dan bagaimana algoritma bekerja, dan yang lainnya.


"Jadi apa, mengapa aku mencari tahu?" - kamu bertanya. Sekarang kami memiliki dua pegangan tambahan: G1PeriodicGCInterval dan G1PeriodicGCSystemLoadThreshold , yang dapat diputar ketika menjadi buruk. Pasti suatu hari akan buruk, ini Jawa, sayang!



Ringkasan


Akibatnya, kami memiliki rilis yang kuat di tangan kami - bukan revolusi, tetapi evolusi yang berfokus pada peningkatan kinerja. Setengah dari peningkatan terkait dengan kinerja: tiga JEPs tentang GC dan satu tentang CDS, yang berjanji untuk dihidupkan sendiri, hanya perlu ditingkatkan ke JDK 12. Selain itu, kami menerima satu fitur bahasa (sakelar ekspresi), dua alat baru untuk pengembang JDK ( Uji Konstanta API dan JMH), dan sekarang komunitas dapat lebih fokus pada port 64-bit tunggal pada ARM.


Secara umum, tingkatkan ke JDK 12 sekarang, dan mungkin the Force bersama Anda. Anda akan membutuhkannya.


Menit periklanan. Segera, pada tanggal 5-6 April, konferensi JPoint akan diadakan, yang akan mempertemukan sejumlah besar orang yang tahu banyak tentang JDK dan semua jenis fitur baru. Sebagai contoh, pasti akan ada Simon Ritter dari Azul dengan kuliah tentang "JDK 12: Perangkap bagi yang tidak waspada" . Tempat paling tepat untuk mendiskusikan rilis terbaru! Anda dapat mempelajari lebih lanjut tentang JPoint di situs web resmi .

Source: https://habr.com/ru/post/id444434/


All Articles